From f683eef5f97a3ae8624e11d1c197e0b503007f5b Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 21 Jul 2026 20:43:10 -0400 Subject: [PATCH 001/150] feat(core): add session warming --- packages/core/src/config.ts | 4 ++ packages/core/src/config/warming.ts | 17 +++++ packages/core/src/plugin/internal.ts | 2 + packages/core/src/plugin/warming.ts | 80 +++++++++++++++++++++++ packages/core/test/config/warming.test.ts | 24 +++++++ 5 files changed, 127 insertions(+) create mode 100644 packages/core/src/config/warming.ts create mode 100644 packages/core/src/plugin/warming.ts create mode 100644 packages/core/test/config/warming.test.ts diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 4c63972c39c2..2f338b6b35ee 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -30,6 +30,7 @@ import { ConfigReference } from "./config/reference" import { ConfigToolOutput } from "./config/tool-output" import { ConfigVariable } from "./config/variable" import { ConfigWatcher } from "./config/watcher" +import { ConfigWarming } from "./config/warming" import { ConfigV1 } from "./v1/config/config" import { ConfigMigrateV1 } from "./v1/config/migrate" import { WellKnown } from "./wellknown" @@ -110,6 +111,9 @@ export class Info extends Schema.Class("Config.Info")({ plugins: ConfigPlugin.Plugins.pipe(Schema.optional).annotate({ description: "Ordered plugin enablement directives and external package declarations", }), + warming: ConfigWarming.Warming.pipe(Schema.optional).annotate({ + description: "Keep recently active sessions warm with transient model requests (default: false)", + }), providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional), experimental: ConfigExperimental.Info.pipe(Schema.optional), }) {} diff --git a/packages/core/src/config/warming.ts b/packages/core/src/config/warming.ts new file mode 100644 index 000000000000..5e3b7d4a913a --- /dev/null +++ b/packages/core/src/config/warming.ts @@ -0,0 +1,17 @@ +export * as ConfigWarming from "./warming" + +import { Schema } from "effect" + +export class Info extends Schema.Class("ConfigV2.Warming")({ + prompt: Schema.String.pipe(Schema.optional).annotate({ + description: "Prompt sent for keep-alive requests", + }), + interval: Schema.DurationFromString.pipe(Schema.optional).annotate({ + description: 'Idle time between keep-alive requests (default: "4 minutes")', + }), + duration: Schema.DurationFromString.pipe(Schema.optional).annotate({ + description: 'Time after the last active request to keep a session warm (default: "30 minutes")', + }), +}) {} + +export const Warming = Schema.Union([Schema.Boolean, Info]) diff --git a/packages/core/src/plugin/internal.ts b/packages/core/src/plugin/internal.ts index 2488c1c6fd95..5457a13d3da3 100644 --- a/packages/core/src/plugin/internal.ts +++ b/packages/core/src/plugin/internal.ts @@ -54,6 +54,7 @@ import { PluginRuntime } from "./runtime" import { SkillPlugin } from "./skill" import { SystemPromptPlugin } from "./system-prompt" import { VariantPlugin } from "./variant" +import { WarmingPlugin } from "./warming" import { WellKnownPlugin } from "../wellknown/plugin" const services = Effect.fn("PluginInternal.services")(function* () { @@ -143,6 +144,7 @@ const pre = [ WebFetchTool.Plugin, WebSearchTool.Plugin, WriteTool.Plugin, + WarmingPlugin.Plugin, ] as const satisfies readonly InternalPlugin[] const post = [ diff --git a/packages/core/src/plugin/warming.ts b/packages/core/src/plugin/warming.ts new file mode 100644 index 000000000000..3ecc11714422 --- /dev/null +++ b/packages/core/src/plugin/warming.ts @@ -0,0 +1,80 @@ +export * as WarmingPlugin from "./warming" + +import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { Clock, Duration, Effect, Scope } from "effect" +import { Config } from "../config" +import { SessionSchema } from "../session/schema" + +const defaults = { + prompt: "This is a keep-alive request. Do not perform any work or use tools. Reply with exactly: OK", + interval: Duration.minutes(4), + duration: Duration.minutes(30), +} + +export const Plugin = define({ + id: "opencode.warming", + effect: Effect.fn(function* (ctx) { + const config = yield* Config.Service + const warming = Config.latest(yield* config.entries(), "warming") + if (!warming) return + const settings = warming === true ? defaults : { ...defaults, ...warming } + const interval = Duration.toMillis(settings.interval) + const duration = Duration.toMillis(settings.duration) + if (!Number.isFinite(interval) || interval <= 0 || !Number.isFinite(duration) || duration <= 0) { + yield* Effect.logWarning("warming interval and duration must be finite positive durations") + return + } + + const scope = yield* Scope.Scope + const sessions = new Map() + const loop: (sessionID: SessionSchema.ID) => Effect.Effect = Effect.fn("WarmingPlugin.loop")(function* ( + sessionID, + ) { + const current = sessions.get(sessionID) + if (!current) return + + const now = yield* Clock.currentTimeMillis + const next = Math.min(current.last + interval, current.expires) + if (now < next) { + yield* Effect.sleep(Duration.millis(next - now)) + return yield* loop(sessionID) + } + if (now >= current.expires) { + sessions.delete(sessionID) + return + } + + const last = current.last + yield* ctx.session.generate({ sessionID, prompt: settings.prompt }).pipe( + Effect.catchCause((cause) => Effect.logWarning("failed to warm session", { sessionID, cause })), + ) + const latest = sessions.get(sessionID) + if (latest === current && latest.last === last) latest.last = yield* Clock.currentTimeMillis + return yield* loop(sessionID) + }) + + yield* ctx.session.hook("context", (event) => + Effect.gen(function* () { + // Once generate exposes request metadata to context hooks, tag warm requests instead of matching the prompt. + const message = event.messages.at(-1) + if ( + message?.role === "user" && + message.content.length === 1 && + message.content[0]?.type === "text" && + message.content[0].text === settings.prompt + ) + return + + const now = yield* Clock.currentTimeMillis + const active = sessions.get(event.sessionID) + if (active) { + active.last = now + active.expires = now + duration + return + } + sessions.set(event.sessionID, { last: now, expires: now + duration }) + yield* loop(event.sessionID).pipe(Effect.forkIn(scope)) + }), + ) + }), +}) diff --git a/packages/core/test/config/warming.test.ts b/packages/core/test/config/warming.test.ts new file mode 100644 index 000000000000..d0ecbf3a914f --- /dev/null +++ b/packages/core/test/config/warming.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from "bun:test" +import { Duration, Schema } from "effect" +import { Config } from "../../src/config" + +const decode = Schema.decodeUnknownSync(Config.Info) + +describe("config warming", () => { + test("accepts boolean enablement", () => { + expect(decode({}).warming).toBeUndefined() + expect(decode({ warming: false }).warming).toBe(false) + expect(decode({ warming: true }).warming).toBe(true) + }) + + test("decodes custom durations", () => { + const warming = decode({ + warming: { prompt: "Reply pong", interval: "2 minutes", duration: "1 hour" }, + }).warming + expect(typeof warming).toBe("object") + if (typeof warming !== "object") return + expect(warming.prompt).toBe("Reply pong") + expect(warming.interval).toEqual(Duration.minutes(2)) + expect(warming.duration).toEqual(Duration.hours(1)) + }) +}) From c821d49386bc6e59c183282d56405156550a4365 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 21 Jul 2026 21:29:42 -0400 Subject: [PATCH 002/150] feat(updates): add artifact build endpoints --- packages/updates/README.md | 2 + .../migrations/0002_artifact_time_created.sql | 3 + packages/updates/src/index.ts | 150 ++++++++++-------- 3 files changed, 91 insertions(+), 64 deletions(-) create mode 100644 packages/updates/migrations/0002_artifact_time_created.sql diff --git a/packages/updates/README.md b/packages/updates/README.md index 05366d56e311..e4ca686b4043 100644 --- a/packages/updates/README.md +++ b/packages/updates/README.md @@ -4,6 +4,8 @@ The updates Worker serves all selected artifacts for a channel. ```sh curl 'https://update.opencode.ai/api/latest' +curl 'https://update.opencode.ai/api/latest/cli' +curl 'https://update.opencode.ai/api/latest/cli/npm' ``` The `/admin*` route must be protected by a Cloudflare Access self-hosted application. Configure the application with: diff --git a/packages/updates/migrations/0002_artifact_time_created.sql b/packages/updates/migrations/0002_artifact_time_created.sql new file mode 100644 index 000000000000..c3bd52023c6c --- /dev/null +++ b/packages/updates/migrations/0002_artifact_time_created.sql @@ -0,0 +1,3 @@ +ALTER TABLE artifact ADD COLUMN time_created INTEGER NOT NULL DEFAULT 0; + +UPDATE artifact SET time_created = time_updated WHERE time_created = 0; diff --git a/packages/updates/src/index.ts b/packages/updates/src/index.ts index 796837b143d6..2ec5f64d2b9a 100644 --- a/packages/updates/src/index.ts +++ b/packages/updates/src/index.ts @@ -11,6 +11,7 @@ type ArtifactRow = { version: string metadata: string active: number + time_created: number time_updated: number } @@ -25,7 +26,7 @@ type ArtifactInput = Pick @@ -57,8 +74,36 @@ async function channel(db: D1Database, channel: string) { return cached({ channel, artifacts: result.results.map(decodeArtifact) }) } +async function artifactName(db: D1Database, channel: string, name: string) { + const result = await db + .prepare(`${select} WHERE channel = ? AND name = ? AND active = 1 ORDER BY distribution`) + .bind(channel, name) + .all() + if (!result.results.length) return json({ error: "Artifact not found" }, 404) + return cached({ channel, name, artifacts: result.results.map(decodeArtifact) }) +} + +async function artifactDistribution(db: D1Database, channel: string, name: string, distribution: string) { + const artifact = await db + .prepare(`${select} WHERE channel = ? AND name = ? AND distribution = ? AND active = 1`) + .bind(channel, name, distribution) + .first() + if (!artifact) return json({ error: "Artifact not found" }, 404) + return cached(decodeArtifact(artifact)) +} + async function admin(request: Request, env: Env) { - const result = await env.DB.prepare(`${select} ORDER BY channel, name, distribution, active DESC, time_updated DESC`).all() + const url = new URL(request.url) + const requestedPage = Number.parseInt(url.searchParams.get("page") ?? "1", 10) + const page = Number.isSafeInteger(requestedPage) && requestedPage > 0 ? requestedPage : 1 + const pageSize = 100 + const count = await env.DB.prepare("SELECT COUNT(*) AS total FROM artifact").first<{ total: number }>() + const pages = Math.max(1, Math.ceil((count?.total ?? 0) / pageSize)) + const currentPage = Math.min(page, pages) + const result = await env.DB + .prepare(`${select} ORDER BY time_created DESC LIMIT ? OFFSET ?`) + .bind(pageSize, (currentPage - 1) * pageSize) + .all() const rows = result.results .map( (artifact) => ` @@ -66,8 +111,8 @@ async function admin(request: Request, env: Env) { ${escape(artifact.name)} ${escape(artifact.distribution)} ${escape(artifact.version)} - ${artifact.active ? 'Active' : 'History'} - ${new Date(artifact.time_updated).toISOString()} + ${new Date(artifact.time_created).toISOString()} + ${artifact.active ? 'Active' : 'Inactive'} ${ artifact.active @@ -105,11 +150,10 @@ async function admin(request: Request, env: Env) { th { color: var(--muted-foreground); font-size: .75rem; font-weight: 500; text-transform: uppercase; letter-spacing: .08em; } tbody tr:last-child td { border-bottom: 0; } td form { margin: 0; } - .publish { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1rem; } - .publish .metadata { grid-column: 1 / -1; } - textarea { min-height: 9rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } - .actions { display: flex; justify-content: end; grid-column: 1 / -1; } - @media (max-width: 760px) { main { padding: 2rem 0; } .masthead { align-items: start; flex-direction: column; } .publish { grid-template-columns: 1fr; } .publish .metadata, .actions { grid-column: 1; } } + .pagination { display: flex; align-items: center; justify-content: space-between; gap: 1rem; border-top: 1px solid var(--border); padding: 1rem; } + .pagination p { color: var(--muted-foreground); font-size: .875rem; } + .pagination nav { display: flex; gap: .5rem; } + @media (max-width: 760px) { main { padding: 2rem 0; } .masthead { align-items: start; flex-direction: column; } } @@ -119,26 +163,20 @@ async function admin(request: Request, env: Env) { ${escape(request.headers.get("Cf-Access-Authenticated-User-Email") ?? "Cloudflare Access pending")}
-

Published artifacts

Activate any successfully published version without changing its metadata.

+

Published builds

Every build received from the trusted publishing workflow, newest first.

- - ${rows || ''} + + ${rows || ''}
ChannelNameDistributionVersionStatusTime updated
No artifacts published yet.
ChannelNameDistributionVersionCreatedStatus
No builds have been published yet.
-
-
-

Publish artifact

Publishing stores the metadata and activates this version for its distribution.

-
-
- ${field("Channel", '')} - ${field("Name", '')} - ${field("Distribution", '')} - ${field("Version", '')} - -
-
-
+
+

Page ${currentPage} of ${pages} · ${count?.total ?? 0} builds

+ +
@@ -147,22 +185,6 @@ async function admin(request: Request, env: Env) { ) } -async function registerArtifact(request: Request, env: Env) { - const invalid = validMutation(request) - if (invalid) return invalid - const form = await request.formData() - const artifact = parseArtifact({ - channel: form.get("channel"), - name: form.get("name"), - distribution: form.get("distribution"), - version: form.get("version"), - metadata: form.get("metadata"), - }) - if (artifact instanceof Response) return artifact - await activate(env.DB, [artifact]) - return Response.redirect(new URL("/admin", request.url), 303) -} - async function publishArtifact(request: Request, env: Env) { const claims = await verifyGitHub(request) if (claims instanceof Response) return claims @@ -219,24 +241,28 @@ async function activateArtifact(request: Request, env: Env) { function activate(db: D1Database, artifacts: ArtifactInput[]) { return db.batch( - artifacts.flatMap((artifact) => [ - deactivateStatement(db, artifact), - db - .prepare( - `INSERT INTO artifact (channel, name, distribution, version, metadata, active, time_updated) - VALUES (?, ?, ?, ?, ?, 1, ?) - ON CONFLICT (channel, name, distribution, version) DO UPDATE SET - metadata = excluded.metadata, active = 1, time_updated = excluded.time_updated`, - ) - .bind( - artifact.channel, - artifact.name, - artifact.distribution, - artifact.version, - JSON.stringify(artifact.metadata), - Date.now(), - ), - ]), + artifacts.flatMap((artifact) => { + const time = Date.now() + return [ + deactivateStatement(db, artifact), + db + .prepare( + `INSERT INTO artifact (channel, name, distribution, version, metadata, active, time_created, time_updated) + VALUES (?, ?, ?, ?, ?, 1, ?, ?) + ON CONFLICT (channel, name, distribution, version) DO UPDATE SET + metadata = excluded.metadata, active = 1, time_updated = excluded.time_updated`, + ) + .bind( + artifact.channel, + artifact.name, + artifact.distribution, + artifact.version, + JSON.stringify(artifact.metadata), + time, + time, + ), + ] + }), ) } @@ -354,10 +380,6 @@ function json(value: unknown, status = 200, headers?: HeadersInit) { return Response.json(value, { status, headers }) } -function field(label: string, input: string) { - return `
${input}
` -} - function escape(value: string) { return value.replace(/[&<>"']/g, (character) => { if (character === "&") return "&" From e84938b3097757145eb00842b4eb12adf1b9d9db Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 21 Jul 2026 21:30:05 -0400 Subject: [PATCH 003/150] fix(core): make session warming observable --- packages/core/src/plugin/warming.ts | 95 +++++++++++++--------- packages/core/src/session/generate-node.ts | 5 ++ packages/docs/config.mdx | 19 +++++ packages/docs/docs.json | 1 + packages/docs/warming.mdx | 76 +++++++++++++++++ packages/server/src/routes.ts | 15 ++-- 6 files changed, 166 insertions(+), 45 deletions(-) create mode 100644 packages/docs/warming.mdx diff --git a/packages/core/src/plugin/warming.ts b/packages/core/src/plugin/warming.ts index 3ecc11714422..d7bb9d0fd84d 100644 --- a/packages/core/src/plugin/warming.ts +++ b/packages/core/src/plugin/warming.ts @@ -15,65 +15,86 @@ export const Plugin = define({ id: "opencode.warming", effect: Effect.fn(function* (ctx) { const config = yield* Config.Service - const warming = Config.latest(yield* config.entries(), "warming") - if (!warming) return - const settings = warming === true ? defaults : { ...defaults, ...warming } - const interval = Duration.toMillis(settings.interval) - const duration = Duration.toMillis(settings.duration) - if (!Number.isFinite(interval) || interval <= 0 || !Number.isFinite(duration) || duration <= 0) { + const loadSettings = Effect.fn("WarmingPlugin.loadSettings")(function* () { + const warming = Config.latest(yield* config.entries(), "warming") + if (!warming) return + const settings = warming === true ? defaults : { ...defaults, ...warming } + const interval = Duration.toMillis(settings.interval) + const duration = Duration.toMillis(settings.duration) + if (Number.isFinite(interval) && interval > 0 && Number.isFinite(duration) && duration > 0) return settings yield* Effect.logWarning("warming interval and duration must be finite positive durations") - return - } + }) const scope = yield* Scope.Scope - const sessions = new Map() - const loop: (sessionID: SessionSchema.ID) => Effect.Effect = Effect.fn("WarmingPlugin.loop")(function* ( - sessionID, - ) { - const current = sessions.get(sessionID) - if (!current) return + const sessions = new Map() + const loop: (sessionID: SessionSchema.ID) => Effect.Effect = Effect.fn("WarmingPlugin.loop")( + function* (sessionID) { + const current = sessions.get(sessionID) + if (!current) return - const now = yield* Clock.currentTimeMillis - const next = Math.min(current.last + interval, current.expires) - if (now < next) { - yield* Effect.sleep(Duration.millis(next - now)) - return yield* loop(sessionID) - } - if (now >= current.expires) { - sessions.delete(sessionID) - return - } + const now = yield* Clock.currentTimeMillis + const next = Math.min(current.last + Duration.toMillis(current.settings.interval), current.expires) + if (now < next) { + yield* Effect.sleep(Duration.millis(next - now)) + return yield* loop(sessionID) + } + if (now >= current.expires) { + sessions.delete(sessionID) + return + } - const last = current.last - yield* ctx.session.generate({ sessionID, prompt: settings.prompt }).pipe( - Effect.catchCause((cause) => Effect.logWarning("failed to warm session", { sessionID, cause })), - ) - const latest = sessions.get(sessionID) - if (latest === current && latest.last === last) latest.last = yield* Clock.currentTimeMillis - return yield* loop(sessionID) - }) + const last = current.last + yield* Effect.logInfo("warming session", { sessionID, last }) + yield* ctx.session + .generate({ sessionID, prompt: current.settings.prompt }) + .pipe(Effect.catchCause((cause) => Effect.logWarning("failed to warm session", { sessionID, cause }))) + const latest = sessions.get(sessionID) + if (latest === current && latest.last === last) latest.last = yield* Clock.currentTimeMillis + return yield* loop(sessionID) + }, + ) yield* ctx.session.hook("context", (event) => Effect.gen(function* () { + const active = sessions.get(event.sessionID) + const settings = yield* loadSettings() + if (!settings) { + sessions.delete(event.sessionID) + return + } + // Once generate exposes request metadata to context hooks, tag warm requests instead of matching the prompt. const message = event.messages.at(-1) if ( message?.role === "user" && message.content.length === 1 && message.content[0]?.type === "text" && - message.content[0].text === settings.prompt - ) + (message.content[0].text === active?.settings.prompt || message.content[0].text === settings.prompt) + ) { + if (active) active.settings = settings return + } const now = yield* Clock.currentTimeMillis - const active = sessions.get(event.sessionID) + const duration = Duration.toMillis(settings.duration) if (active) { active.last = now active.expires = now + duration + active.settings = settings return } - sessions.set(event.sessionID, { last: now, expires: now + duration }) - yield* loop(event.sessionID).pipe(Effect.forkIn(scope)) + sessions.set(event.sessionID, { last: now, expires: now + duration, settings }) + yield* Effect.logInfo("scheduled session warming", { + sessionID: event.sessionID, + interval: settings.interval, + expires: now + duration, + }) + yield* loop(event.sessionID).pipe( + Effect.catchCause((cause) => + Effect.logError("session warming loop failed", { sessionID: event.sessionID, cause }), + ), + Effect.forkIn(scope), + ) }), ) }), diff --git a/packages/core/src/session/generate-node.ts b/packages/core/src/session/generate-node.ts index 1ca5fa1ac60e..2214b179c9d8 100644 --- a/packages/core/src/session/generate-node.ts +++ b/packages/core/src/session/generate-node.ts @@ -48,6 +48,11 @@ export const layer = Layer.effect( ], tools: {}, }) + yield* Effect.logInfo("sending session generation request", { + sessionID: selection.session.id, + providerID: model.ref.providerID, + modelID: model.ref.id, + }) return (yield* llm.generate( LLM.request({ model: model.model, diff --git a/packages/docs/config.mdx b/packages/docs/config.mdx index 25d9cb0989c9..bf53d35b4520 100644 --- a/packages/docs/config.mdx +++ b/packages/docs/config.mdx @@ -338,6 +338,25 @@ Control automatic context compaction and how much recent context it preserves. See the [compaction guide](/compaction) for automatic context management. +### Session warming + +Keep recently active model sessions warm with periodic transient requests. +Warming is disabled by default; set it to `true` to use the four-minute idle +interval and 30-minute active window. + +```jsonc +{ + "warming": { + "prompt": "Do not perform any work. Reply with exactly: OK", + "interval": "4 minutes", + "duration": "30 minutes" + } +} +``` + +See the [session warming guide](/warming) for request behavior, customization, +and cost considerations. + ### Skills Add directories or URLs that OpenCode should search for agent skills. diff --git a/packages/docs/docs.json b/packages/docs/docs.json index fbbf84d3fa87..ff41ab4b92ed 100644 --- a/packages/docs/docs.json +++ b/packages/docs/docs.json @@ -37,6 +37,7 @@ "mcp-servers", "attachments", "compaction", + "warming", "formatters", "lsp", "references" diff --git a/packages/docs/warming.mdx b/packages/docs/warming.mdx new file mode 100644 index 000000000000..8b7bd06a59c0 --- /dev/null +++ b/packages/docs/warming.mdx @@ -0,0 +1,76 @@ +--- +title: "Session warming" +description: "Keep recently active model sessions warm with periodic transient requests." +--- + +Session warming sends periodic model requests for recently active sessions. +This can preserve provider-side prompt caches or other short-lived session +state while you pause between prompts. + +Warming is disabled by default. Enable it with the default settings in any +[OpenCode configuration file](/config): + +```jsonc title="opencode.jsonc" +{ + "$schema": "https://opencode.ai/config.json", + "warming": true +} +``` + +With the defaults, OpenCode sends a warming request after a session has made no +model request for four minutes. It repeats this while the session remains idle, +but stops 30 minutes after the last non-warming request. New model activity +starts a new 30-minute window. + +## Configuration + +Use the object form to customize the prompt, idle interval, or active duration: + +```jsonc title="opencode.jsonc" +{ + "$schema": "https://opencode.ai/config.json", + "warming": { + "prompt": "Do not perform any work. Reply with exactly: OK", + "interval": "4 minutes", + "duration": "30 minutes" + } +} +``` + +| Field | Default | Description | +| --- | --- | --- | +| `prompt` | Keep-alive instruction | Prompt sent in each warming request. The default instructs the model to do no work and reply with `OK`. | +| `interval` | `"4 minutes"` | Idle time between warming requests. | +| `duration` | `"30 minutes"` | Maximum warming window after the latest non-warming model request. | + +`interval` and `duration` accept duration strings such as `"30 seconds"`, +`"4 minutes"`, or `"1 hour"`. Both must be finite and greater than zero. + +To disable warming explicitly: + +```jsonc +{ + "warming": false +} +``` + +## Request behavior + +A warming request uses the session's current model, agent, instructions, and +conversation context. Tools are disabled. The configured prompt is appended as +a transient user message, and the response is discarded. + +Warming does not admit input, add messages to session history, or otherwise +mutate durable session state. A warming request resets the idle interval but +does not extend the active duration; without new model activity, warming still +ends when the configured duration expires. + +## Costs and limits + +Warming requests are real provider requests. They can consume tokens, incur +costs, count against rate limits, and fail for the same reasons as other model +requests. OpenCode logs warming failures without failing or changing the +session, then waits for the next interval before trying again. + +Enable warming only when the provider-side benefit is worth the additional +requests. A shorter interval or longer duration increases request volume. diff --git a/packages/server/src/routes.ts b/packages/server/src/routes.ts index 553e91281ee5..edffdbd4f801 100644 --- a/packages/server/src/routes.ts +++ b/packages/server/src/routes.ts @@ -123,6 +123,12 @@ function makeRoutes( }), ) : AppNodeBuilder.build(applicationServices, replacements) + const observability = Observability.layer({ + ...options.observability, + client: options.app?.name, + version: options.app?.version, + channel: options.app?.channel, + }) return serviceLayer.pipe( Layer.flatMap((context) => { @@ -139,18 +145,11 @@ function makeRoutes( Layer.provide(authorizationLayer), Layer.provide(schemaErrorLayer), Layer.provide(auth), - Layer.provide( - Observability.layer({ - ...options.observability, - client: options.app?.name, - version: options.app?.version, - channel: options.app?.channel, - }), - ), HttpRouter.provideRequest(requestServices), Layer.provideMerge(services), Layer.provideMerge(HttpRouter.layer), ) }), + Layer.provide(observability), ) } From c8a40450e57bd5e9925396fb443497c68a2eb800 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 21 Jul 2026 20:36:42 -0500 Subject: [PATCH 004/150] feat(codemode): support generator functions (#38172) --- packages/codemode/interpreter-support.md | 59 +- packages/codemode/src/interpreter/errors.ts | 39 +- packages/codemode/src/interpreter/iterator.ts | 21 + packages/codemode/src/interpreter/methods.ts | 91 +- packages/codemode/src/interpreter/model.ts | 26 + packages/codemode/src/interpreter/promises.ts | 151 +- .../codemode/src/interpreter/references.ts | 5 + packages/codemode/src/interpreter/runtime.ts | 834 ++++++++--- packages/codemode/src/stdlib/math.ts | 40 +- packages/codemode/src/stdlib/object.ts | 76 +- packages/codemode/src/tool-runtime.ts | 2 +- packages/codemode/test/codemode.test.ts | 3 +- .../codemode/test/generator-test262.test.ts | 1271 +++++++++++++++++ 13 files changed, 2229 insertions(+), 389 deletions(-) create mode 100644 packages/codemode/src/interpreter/iterator.ts create mode 100644 packages/codemode/test/generator-test262.test.ts diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index 467f6e0f5b79..7c2a9596652f 100644 --- a/packages/codemode/interpreter-support.md +++ b/packages/codemode/interpreter-support.md @@ -29,7 +29,8 @@ ultimate source of truth. ## Values and literals - [x] `null`, `undefined`, booleans, finite and non-finite numbers, and strings. -- [x] Array literals, including holes and spread from arrays, strings, Maps, Sets, and URLSearchParams. +- [x] Array literals, including holes and spread from arrays, strings, Maps, Sets, URLSearchParams, custom synchronous + iterators, and synchronous generators. - [x] Object literals with shorthand, computed string/number keys, and spread from plain data objects; `null` and `undefined` are no-ops, while arrays are rejected. - [x] Template literals with interpolation. @@ -57,7 +58,9 @@ ultimate source of truth. - [ ] Hoist function declarations accepted directly in switch cases. - [x] Computed object destructuring keys such as `const { [field]: value } = record`. - [x] Object destructuring from arrays, such as `const { length } = values`. -- [x] Array destructuring from supported non-array iterables: strings, Maps, Sets, and URLSearchParams. +- [x] Array binding and assignment destructuring from strings, Maps, Sets, URLSearchParams, custom synchronous + iterators, and synchronous generators, including stepwise elisions/rest and `IteratorClose` on early completion + or binding/default failure. ## Statements and control flow @@ -65,7 +68,8 @@ ultimate source of truth. - [x] `if`/`else` and conditional expressions. - [x] `switch`, including default clauses and fallthrough. - [x] `for`, `while`, and `do...while`. -- [x] `for...of` over arrays, strings, Maps, Sets, and URLSearchParams. +- [x] `for...of` over arrays, strings, Maps, Sets, URLSearchParams, custom synchronous iterators, and confined + synchronous generators. Abrupt completion invokes the iterator's optional `return()`. - [x] `for...in` over own keys of plain objects, arrays, and tool references. - [x] Unlabeled `break` and `continue`. - [x] `try`, `catch`, optional catch bindings, and `finally`. @@ -75,7 +79,7 @@ ultimate source of truth. `Symbol.asyncIterator` or the `Symbol.iterator` fallback. Each iterator step is sequential, yielded promises and plain values from synchronous collections and sync iterators are awaited before binding, and abrupt loop completion invokes the iterator's optional `return()`. Custom async iterators control their yielded values, as in - JavaScript; only their `next()` results are awaited. Async generators remain outside the supported subset. + JavaScript; only their `next()` results are awaited. Confined sync and async generators are iterable here. ## Functions and callbacks @@ -104,7 +108,27 @@ ultimate source of truth. - [ ] User-defined constructor calls. - [ ] `Function.prototype.call`, `apply`, and `bind` for CodeMode functions. - [ ] Classes and private fields. -- [ ] Generator functions and `yield`. +- [x] Synchronous and async generator declarations/expressions, `yield`, and `yield*`, including lazy bodies, + `next(value)`, `return(value)`, `throw(value)`, exhaustion, promise adoption, async request ordering, + `try`/`catch`/`finally`, and sync/async iterator symbols. Async `yield*` awaits values while adapting a sync + iterator but preserves values supplied by a manually implemented async iterator. Generator values are opaque + runtime references. +- [x] Synchronous generators and custom synchronous iterators are consumed stepwise by array/argument spread, array + destructuring, `Array.from`, Map/Set/URLSearchParams construction, `Object.fromEntries`, Object/Map `groupBy`, + Promise combinators, `AggregateError`, and `Math.sumPrecise`. Mapper/grouping callbacks interleave with iterator + steps; synchronous consumers preserve yielded promise objects rather than awaiting them. Async generators are + rejected by every synchronous consumer. +- [x] Synchronous iterator acquisition and result validation follow `IteratorClose` boundaries: consumer errors and + intentional early stops invoke `return()`, acquisition/`next()` failures do not, and an original consumer error + wins over a cleanup failure. Async iterator consumption remains limited to `for await...of` and async `yield*`. +- [x] Portable generator protocol coverage is adapted from pinned Test262 cases for suspended-start, suspended-yield, + and completed states; sync and async `next`/`return`/`throw`; finally yields and completion overrides; rejected + yielded promises; mixed async request queues; sync and async `yield*` forwarding; malformed methods/results; + and declaration, expression, and object-method forms with closure and parameter behavior. The adapted suite + deliberately skips Test262 variants whose observation mechanism requires unsupported getter definitions, + proxies, prototype inspection or mutation, non-arrow `this`, classes, or arbitrary symbols. It also skips tests + asserting exact promise reaction-turn counts beyond the observable ordering guarantee documented below. These + are interpreter-surface boundaries, not claims that the corresponding full Test262 families pass unchanged. ## Expressions and operators @@ -130,8 +154,8 @@ ultimate source of truth. - [x] Tool calls start eagerly and return supervised, run-once CodeMode promises. - [x] Direct `await`, repeated awaits, and implicit resolution when a promise is returned from a function/program. - [x] `Promise.resolve` and `Promise.reject`. -- [x] `Promise.all`, `Promise.allSettled`, `Promise.race`, and `Promise.any` over supported collections containing - promises and plain values. +- [x] `Promise.all`, `Promise.allSettled`, `Promise.race`, and `Promise.any` over finite collections, custom synchronous + iterators, and synchronous generators containing promises and plain values. - [x] `Promise.all` preserves result order and rejects on the first observed failure without cancelling siblings. - [x] `Promise.allSettled` returns plain fulfilled/rejected outcome records. - [x] `Promise.race` settles from the first result without cancelling losers at settlement time. @@ -175,14 +199,16 @@ ultimate source of truth. - [x] `Object()` and `new Object()` return `{}` for nullish arguments and pass objects through unchanged; primitive wrapper objects (`Object(1)`) are rejected explicitly. - [x] Computed property names and object spread. -- [x] `Object.keys`, `Object.values`, `Object.entries`, `Object.hasOwn`, `Object.assign`, and `Object.fromEntries`. +- [x] `Object.keys`, `Object.values`, `Object.entries`, `Object.hasOwn`, `Object.assign`, and `Object.fromEntries`, with + synchronous iterator support for `fromEntries`. - [x] `Object.keys` over arrays and tool references. - [x] Object identity is preserved by in-CodeMode Object helpers. - [x] Prototype traversal and mutation through `__proto__`, `constructor`, and `prototype` are blocked. - [ ] Legal own data fields named `__proto__`, `constructor`, or `prototype` are rejected at JSON/tool boundaries and cannot be created, read, or written in CodeMode; tool path segments with those names remain supported. - [x] `Object.is` for supported data values. -- [x] `Object.groupBy` over supported collection iterables, with string-key coercion and null-prototype results. +- [x] `Object.groupBy` over finite collections and custom synchronous iterators/generators, with string-key coercion + and null-prototype results. ## Arrays @@ -190,7 +216,7 @@ ultimate source of truth. array of that length; invalid lengths throw `RangeError`. Iteration, spread, join, and JSON handle holes like JavaScript, and host results normalize holes to `null`. - [x] Static methods: `Array.isArray`, `Array.of`, and `Array.from`, including the `Array.from` mapper form with - `(value, index)` arguments. + `(value, index)` arguments and stepwise synchronous iterator consumption. - [x] Iteration/transformation: `map`, `filter`, `flatMap`, and `forEach`. - [x] Searching/tests: `find`, `findIndex`, `findLast`, `findLastIndex`, `some`, `every`, `includes`, `indexOf`, and `lastIndexOf`. @@ -245,7 +271,8 @@ ultimate source of truth. for feature detection. Calling any undefined value reports a native-style `TypeError` naming the callee, for example `Math.sum is not a function.` Blocked members (`constructor`, `__proto__`, ...) still throw, and unknown `Promise` statics keep their descriptive error. -- [x] `Math.sumPrecise` over supported collection iterables, rejecting non-number elements without coercion. +- [x] `Math.sumPrecise` over finite collections and custom synchronous iterators/generators, rejecting non-number + elements without coercion. - [x] Global coercing `isFinite` and `isNaN`; opaque runtime references reject as data errors, like `Number(...)`. ## JSON and console @@ -297,10 +324,10 @@ ultimate source of truth. ## Map and Set -- [x] Static `Map.groupBy` over supported collection iterables, preserving key identity. -- [x] `new Map()` from entry arrays or another Map. +- [x] Static `Map.groupBy` over finite collections and custom synchronous iterators/generators, preserving key identity. +- [x] `new Map()` from synchronous iterables of entries. - [x] Map `get`, `set`, `has`, `delete`, `clear`, `size`, and `forEach`. -- [x] `new Set()` from arrays, strings, or another Set. +- [x] `new Set()` from synchronous iterables. - [x] Set `add`, `has`, `delete`, `clear`, `size`, and `forEach`. - [x] Materialized `keys`, `values`, and `entries` arrays for Map and Set. - [x] Spread, `for...of`, `Array.from`, and `Object.fromEntries` integration. @@ -316,7 +343,7 @@ ultimate source of truth. - [x] Readable URL fields: `href`, `origin`, `protocol`, `username`, `password`, `host`, `hostname`, `port`, `pathname`, `search`, and `hash`. - [x] Writable URL fields except `origin`. -- [x] `new URLSearchParams()` from query strings, data objects, pairs, Maps, and URLSearchParams. +- [x] `new URLSearchParams()` from query strings, data objects, synchronous iterables of pairs, and URLSearchParams. - [x] URLSearchParams `append`, `delete`, `get`, `getAll`, `has`, `set`, `sort`, `forEach`, `keys`, `values`, `entries`, `toString`, and `size`. - [x] URL values serialize to their href; URLSearchParams serialize to `{}`. @@ -326,7 +353,7 @@ ultimate source of truth. - [x] `Error`, `TypeError`, `RangeError`, `SyntaxError`, `ReferenceError`, `EvalError`, and `URIError`, callable with or without `new`. - [x] `AggregateError` with the `(errors, message?)` signature and an own `errors` array, constructed directly or by - an all-rejected `Promise.any`. + an all-rejected `Promise.any`; direct construction accepts custom synchronous iterators and generators. - [x] Error `name`/`message`, error inheritance through `instanceof`, and plain-data serialization. - [x] `instanceof` for Date, RegExp, Map, Set, URL, URLSearchParams, Array, Object, Promise, and Error types. - [x] Catchable user throws, runtime failures raised during interpreted evaluation, awaited tool failures, and awaited diff --git a/packages/codemode/src/interpreter/errors.ts b/packages/codemode/src/interpreter/errors.ts index ed621dc7f7ae..14962ce38483 100644 --- a/packages/codemode/src/interpreter/errors.ts +++ b/packages/codemode/src/interpreter/errors.ts @@ -1,9 +1,10 @@ +import { Effect } from "effect" import type { Diagnostic } from "../codemode.js" import { ToolError } from "../tool-error.js" import { copyOut, ToolRuntimeError, type SafeObject } from "../tool-runtime.js" import { type AstNode, formatLocation, InterpreterRuntimeError, ProgramThrow, sourceLocation } from "./model.js" import { containsRuntimeReference } from "./references.js" -import { spreadItems } from "../stdlib/collections.js" +import { type SyncIteratorRunner } from "./iterator.js" import { coerceToString, createAggregateErrorValue, createErrorValue, errorConstructors } from "../stdlib/value.js" export const normalizeError = (error: unknown): Diagnostic => { @@ -79,15 +80,27 @@ export const caughtErrorValue = (thrown: unknown): unknown => { return createErrorValue(name, normalizeError(thrown).message) } -export const constructErrorValue = (name: string, args: Array, node: AstNode): SafeObject => { - if (name !== "AggregateError") return createErrorValue(name, args[0] === undefined ? "" : coerceToString(args[0])) - const errors = spreadItems(args[0]) - if (errors === undefined) { - throw new InterpreterRuntimeError( - "new AggregateError(...) expects an array of errors (e.g. new AggregateError(errors, message?)).", - node, - ).as("TypeError") - } - // Error values must not alias caller-owned arrays. - return createAggregateErrorValue([...errors], args[1] === undefined ? "" : coerceToString(args[1])) -} +export const constructErrorValue = (name: string, args: Array): SafeObject => + createErrorValue(name, args[0] === undefined ? "" : coerceToString(args[0])) + +export const constructAggregateErrorValue = ( + runner: SyncIteratorRunner, + args: Array, + node: AstNode, +): Effect.Effect => + Effect.gen(function* () { + const cursor = yield* runner.syncIterator(args[0], node) + if (cursor === undefined) { + throw new InterpreterRuntimeError("new AggregateError(...) expects a synchronous iterable of errors.", node).as( + "TypeError", + ) + } + const errors: Array = [] + while (true) { + const step = yield* cursor.next + if (step.done) { + return createAggregateErrorValue(errors, args[1] === undefined ? "" : coerceToString(args[1])) + } + errors.push(step.value) + } + }) diff --git a/packages/codemode/src/interpreter/iterator.ts b/packages/codemode/src/interpreter/iterator.ts new file mode 100644 index 000000000000..f352ed159de5 --- /dev/null +++ b/packages/codemode/src/interpreter/iterator.ts @@ -0,0 +1,21 @@ +import { Effect, Exit } from "effect" +import type { AstNode } from "./model.js" + +export type IteratorCursor = { + readonly next: Effect.Effect<{ readonly done: boolean; readonly value: unknown }, unknown, R> + readonly close: Effect.Effect +} + +export type SyncIteratorRunner = { + readonly syncIterator: (value: unknown, node: AstNode) => Effect.Effect | undefined, unknown, R> +} + +export const preserveConsumerError = ( + cursor: IteratorCursor, + effect: Effect.Effect, +): Effect.Effect => + Effect.flatMap(Effect.exit(effect), (exit) => + Exit.isSuccess(exit) + ? Effect.succeed(exit.value) + : Effect.andThen(Effect.exit(cursor.close), Effect.failCause(exit.cause)), + ) diff --git a/packages/codemode/src/interpreter/methods.ts b/packages/codemode/src/interpreter/methods.ts index 79a014ba4fe2..cae4951d9a87 100644 --- a/packages/codemode/src/interpreter/methods.ts +++ b/packages/codemode/src/interpreter/methods.ts @@ -2,6 +2,7 @@ import { Effect } from "effect" import { type AstNode, CodeModeFunction, + CodeModeGenerator, CoercionFunction, ErrorConstructorReference, GlobalMethodReference, @@ -33,6 +34,7 @@ import { invokeRegExpMethod, invokeRegExpStatic, matchToValue, toHostRegex } fro import { invokeStringStatic } from "../stdlib/string.js" import { invokeURLMethod, invokeURLStatic, uriArgument } from "../stdlib/url.js" import { boundedData, coerceToNumber, coerceToString, errorBrandName } from "../stdlib/value.js" +import { preserveConsumerError, type SyncIteratorRunner } from "./iterator.js" export type CallbackRunner = { readonly invokeFunction: (fn: CodeModeFunction, args: Array) => Effect.Effect @@ -360,19 +362,12 @@ const invokeArrayStatic = (name: string, args: Array, node: AstNode): u return Array.isArray(args[0]) case "of": return [...args] - case "from": - return arrayFromItems(args[0], node) default: throw new InterpreterRuntimeError(`Array.${name} is not available.`, node) } } -const arrayFromItems = (source: unknown, node: AstNode): Array => { - if (source instanceof CodeModeMap) return Array.from(source.map.entries(), ([key, item]) => [key, item]) - if (source instanceof CodeModeSet) return Array.from(source.set.values()) - if (source instanceof CodeModeURLSearchParams) { - return Array.from(source.params.entries(), ([key, value]) => [key, value]) - } +const arrayLikeSource = (source: unknown, node: AstNode): { readonly length: number; readonly source: object } => { if (source instanceof CodeModePromise) { throw new InterpreterRuntimeError( "Array.from received an un-awaited Promise; await it before creating the array.", @@ -380,15 +375,16 @@ const arrayFromItems = (source: unknown, node: AstNode): Array => { "InvalidDataValue", ) } - if (typeof source === "string") return Array.from(source) - if (Array.isArray(source)) return [...source] if ( source !== null && typeof source === "object" && (Object.getPrototypeOf(source) === Object.prototype || Object.getPrototypeOf(source) === null) && typeof (source as { length?: unknown }).length === "number" ) { - return Array.from(source as ArrayLike) + const length = (source as { length: number }).length + const normalized = Number.isNaN(length) || length <= 0 ? 0 : Math.trunc(length) + if (normalized > 4_294_967_295) throw new RangeError("Invalid array length") + return { length: normalized, source } } throw new InterpreterRuntimeError( "Array.from expects an array, string, Map, Set, or array-like value.", @@ -398,24 +394,42 @@ const arrayFromItems = (source: unknown, node: AstNode): Array => { } export const invokeArrayFrom = ( - runner: CallbackRunner, + runner: CallbackRunner & SyncIteratorRunner, args: Array, node: AstNode, ): Effect.Effect => { - const items = arrayFromItems(args[0], node) - if (args.length < 2 || args[1] === undefined) return Effect.succeed(items) - const apply = applyCollectionCallback(runner, args[1], "Array.from", node) + const source = args[0] + const apply = + args.length < 2 || args[1] === undefined ? undefined : applyCollectionCallback(runner, args[1], "Array.from", node) return Effect.gen(function* () { + const cursor = yield* runner.syncIterator(source, node) + if (cursor === undefined) { + if (source instanceof CodeModeGenerator) { + throw new InterpreterRuntimeError("Array.from expects a synchronous iterable or array-like value.", node).as( + "TypeError", + ) + } + const arrayLike = arrayLikeSource(source, node) + const values: Array = [] + for (let index = 0; index < arrayLike.length; index += 1) { + const item = Reflect.get(arrayLike.source, index) + values.push(apply === undefined ? item : yield* apply([item, index])) + } + return values + } const values: Array = [] - for (let index = 0; index < items.length; index += 1) { - values.push(yield* apply([items[index], index])) + let index = 0 + while (true) { + const step = yield* cursor.next + if (step.done) return values + values.push(apply === undefined ? step.value : yield* preserveConsumerError(cursor, apply([step.value, index]))) + index += 1 } - return values }) } export const invokeGroupBy = ( - runner: CallbackRunner, + runner: CallbackRunner & SyncIteratorRunner, namespace: "Map" | "Object", args: Array, node: AstNode, @@ -425,47 +439,50 @@ export const invokeGroupBy = ( throw new InterpreterRuntimeError(`${namespace}.groupBy expects an iterable collection.`, node).as("TypeError") } const apply = applyCollectionCallback(runner, args[1], `${namespace}.groupBy`, node) - const items = supportedIterableItems(source) - if (items === undefined) { - throw new InterpreterRuntimeError(`${namespace}.groupBy expects an iterable collection.`, node).as("TypeError") - } return Effect.gen(function* () { + const cursor = yield* runner.syncIterator(source, node) + if (cursor === undefined) { + throw new InterpreterRuntimeError(`${namespace}.groupBy expects an iterable collection.`, node).as("TypeError") + } if (namespace === "Map") { const result = new CodeModeMap() let index = 0 - for (const item of items) { - const key = yield* apply([item, index]) + while (true) { + const step = yield* cursor.next + if (step.done) return result + const item = step.value + const key = yield* preserveConsumerError(cursor, apply([item, index])) const group = result.map.get(key) if (group === undefined) result.map.set(key, [item]) else (group as Array).push(item) index += 1 } - return result } const result: SafeObject = Object.create(null) as SafeObject let index = 0 - for (const item of items) { - const key = yield* coerceGroupByPropertyKey(runner, yield* apply([item, index]), node) + while (true) { + const step = yield* cursor.next + if (step.done) return result + const item = step.value + const key = yield* preserveConsumerError( + cursor, + Effect.flatMap(apply([item, index]), (value) => coerceGroupByPropertyKey(runner, value, node)), + ) if (isBlockedMember(key)) { - throw new InterpreterRuntimeError(`Property '${key}' is not available.`, node) + return yield* preserveConsumerError( + cursor, + Effect.fail(new InterpreterRuntimeError(`Property '${key}' is not available.`, node)), + ) } const group = result[key] if (group === undefined) result[key] = [item] else (group as Array).push(item) index += 1 } - return result }) } -const supportedIterableItems = (source: unknown): Iterable | undefined => { - if (Array.isArray(source) || typeof source === "string") return source - if (source instanceof CodeModeMap) return source.map.entries() - if (source instanceof CodeModeSet) return source.set.values() - if (source instanceof CodeModeURLSearchParams) return source.params.entries() -} - const coerceGroupByPropertyKey = ( runner: CallbackRunner, value: unknown, diff --git a/packages/codemode/src/interpreter/model.ts b/packages/codemode/src/interpreter/model.ts index ef66d8876cbb..b3a82e68867f 100644 --- a/packages/codemode/src/interpreter/model.ts +++ b/packages/codemode/src/interpreter/model.ts @@ -1,3 +1,4 @@ +import type { Effect } from "effect" import type { SafeObject } from "../tool-runtime.js" import type { CodeModePromise, CodeModeRegExp, CodeModeURL } from "../values.js" @@ -45,6 +46,27 @@ export class CodeModeFunction { readonly body: AstNode, readonly capturedScopes: ReadonlyArray>, readonly async: boolean, + readonly generator: boolean, + ) {} +} + +export type GeneratorRequestKind = "next" | "return" | "throw" + +export class CodeModeGenerator { + constructor( + readonly asynchronous: boolean, + readonly request: ( + kind: GeneratorRequestKind, + value: unknown, + node: AstNode, + ) => Effect.Effect, + ) {} +} + +export class GeneratorMethodReference { + constructor( + readonly generator: CodeModeGenerator, + readonly kind: GeneratorRequestKind | "iterator", ) {} } @@ -128,6 +150,10 @@ export class ProgramThrow { constructor(readonly value: unknown) {} } +export class GeneratorReturn { + constructor(readonly value: unknown) {} +} + export class ErrorConstructorReference { constructor(readonly name: string) {} } diff --git a/packages/codemode/src/interpreter/promises.ts b/packages/codemode/src/interpreter/promises.ts index 6f7c07c5b8b0..8f5fe2bdd42c 100644 --- a/packages/codemode/src/interpreter/promises.ts +++ b/packages/codemode/src/interpreter/promises.ts @@ -13,9 +13,9 @@ import { import { caughtErrorValue, normalizeError } from "./errors.js" import { applyCollectionCallback, isSupportedCallback, type CallbackRunner, type SupportedCallback } from "./methods.js" import { typeofValue } from "./references.js" -import { spreadItems } from "../stdlib/collections.js" import { createAggregateErrorValue } from "../stdlib/value.js" import { CodeModePromise } from "../values.js" +import type { SyncIteratorRunner } from "./iterator.js" // Observation only controls rejection reporting; program completion interrupts all promise work. export class PromiseRuntime { @@ -64,6 +64,10 @@ export class PromiseRuntime { return Fiber.await(promise.fiber) } + fork(effect: Effect.Effect): Effect.Effect { + return Effect.asVoid(Effect.forkIn(effect, this.scope, { startImmediately: true })) + } + diagnostics(): Array { return [...this.failures].sort(([left], [right]) => left - right).map(([, failure]) => failure) } @@ -84,7 +88,7 @@ export const selfResolutionError = (node?: AstNode): InterpreterRuntimeError => new InterpreterRuntimeError("Chaining cycle detected: a promise cannot resolve with itself.", node).as("TypeError") export const invokePromiseMethod = ( - runner: CallbackRunner, + runner: CallbackRunner & SyncIteratorRunner, promises: PromiseRuntime, ref: PromiseMethodReference, args: Array, @@ -98,79 +102,69 @@ export const invokePromiseMethod = ( return promises.create(Effect.fail(new ProgramThrow(args[0]))) } - const spread = spreadItems(args[0]) - if (spread === undefined) { - return promises.create( - Effect.fail( - new InterpreterRuntimeError( - `Promise.${ref.name} expects an array of promises or plain values (e.g. Promise.${ref.name}(items.map((item) => tools.ns.tool(item)))).`, + return promises.create( + Effect.gen(function* () { + const cursor = yield* runner.syncIterator(args[0], node) + if (cursor === undefined) { + throw new InterpreterRuntimeError( + `Promise.${ref.name} expects an array or other synchronous iterable.`, node, - ).as("TypeError"), - ), - ) - } - const items = Array.from(spread) - - for (const item of items) { - if (item instanceof CodeModePromise) promises.markObserved(item) - } + ).as("TypeError") + } + const items: Array = [] + while (true) { + const step = yield* cursor.next + if (step.done) break + items.push(step.value) + if (step.value instanceof CodeModePromise) promises.markObserved(step.value) + } - switch (ref.name) { - case "all": { - const observations = items.map((item) => - item instanceof CodeModePromise ? Effect.flatten(promises.await(item)) : Effect.succeed(item), - ) - return promises.create(settleAfterTurn(Effect.all(observations, { concurrency: "unbounded" }))) - } - case "allSettled": { - const observations = items.map((item) => - item instanceof CodeModePromise ? promises.await(item) : Effect.succeed(Exit.succeed(item)), - ) - return promises.create( - settleAfterTurn( - Effect.gen(function* () { - const outcomes: Array = [] - for (const observation of observations) { - const exit = yield* observation - if (Exit.isSuccess(exit)) { - outcomes.push( - Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }), - ) - continue - } - if (Cause.hasInterruptsOnly(exit.cause)) { - // Teardown interruption is not a program-level rejection. - return yield* Effect.failCause(exit.cause) - } - outcomes.push( - Object.assign(Object.create(null) as SafeObject, { - status: "rejected", - reason: caughtErrorValue(Cause.squash(exit.cause)), - }), - ) - } - return outcomes - }), - ), - ) - } - case "race": { - if (items.length === 0) { - return promises.create( - Effect.fail( - new InterpreterRuntimeError( - "Promise.race([]) would never settle; provide at least one promise or value.", - node, + if (ref.name === "all") { + return yield* settleAfterTurn( + Effect.all( + items.map((item) => + item instanceof CodeModePromise ? Effect.flatten(promises.await(item)) : Effect.succeed(item), + ), + { concurrency: "unbounded" }, + ), + ) + } + if (ref.name === "allSettled") { + const outcomes: Array = [] + for (const item of items) { + const exit = item instanceof CodeModePromise ? yield* promises.await(item) : Exit.succeed(item) + if (Exit.isSuccess(exit)) { + outcomes.push(Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value })) + continue + } + if (Cause.hasInterruptsOnly(exit.cause)) return yield* Effect.failCause(exit.cause) + outcomes.push( + Object.assign(Object.create(null) as SafeObject, { + status: "rejected", + reason: caughtErrorValue(Cause.squash(exit.cause)), + }), + ) + } + yield* Effect.yieldNow + return outcomes + } + if (ref.name === "race") { + if (items.length === 0) { + throw new InterpreterRuntimeError( + "Promise.race([]) would never settle; provide at least one promise or value.", + node, + ) + } + return yield* settleAfterTurn( + Effect.flatten( + Effect.raceAll( + items.map((item) => + item instanceof CodeModePromise ? promises.await(item) : Effect.succeed(Exit.succeed(item)), + ), ), ), ) } - const observations = items.map((item) => - item instanceof CodeModePromise ? promises.await(item) : Effect.succeed(Exit.succeed(item)), - ) - return promises.create(settleAfterTurn(Effect.flatten(Effect.raceAll(observations)))) - } - case "any": { const flipped = items.map((item) => item instanceof CodeModePromise ? Effect.flatMap(promises.await(item), (exit) => { @@ -180,17 +174,18 @@ export const invokePromiseMethod = ( }) : Effect.fail(new PromiseAnyFulfilled(item)), ) - const body = Effect.all(flipped, { concurrency: "unbounded" }).pipe( - Effect.flatMap((reasons) => - Effect.fail(new ProgramThrow(createAggregateErrorValue(reasons, "All promises were rejected"))), - ), - Effect.catch((error) => - error instanceof PromiseAnyFulfilled ? Effect.succeed(error.value) : Effect.fail(error), + return yield* settleAfterTurn( + Effect.all(flipped, { concurrency: "unbounded" }).pipe( + Effect.flatMap((reasons) => + Effect.fail(new ProgramThrow(createAggregateErrorValue(reasons, "All promises were rejected"))), + ), + Effect.catch((error) => + error instanceof PromiseAnyFulfilled ? Effect.succeed(error.value) : Effect.fail(error), + ), ), ) - return promises.create(settleAfterTurn(body)) - } - } + }), + ) } export const invokePromiseInstanceMethod = ( diff --git a/packages/codemode/src/interpreter/references.ts b/packages/codemode/src/interpreter/references.ts index ae3a9afa46e9..bfc7663a7e30 100644 --- a/packages/codemode/src/interpreter/references.ts +++ b/packages/codemode/src/interpreter/references.ts @@ -1,10 +1,12 @@ import { type AstNode, CodeModeFunction, + CodeModeGenerator, CoercionFunction, ErrorConstructorReference, GlobalMethodReference, GlobalNamespace, + GeneratorMethodReference, InterpreterRuntimeError, IntrinsicReference, JsonMethodReference, @@ -21,6 +23,8 @@ import { isCodeModeValue, CodeModePromise } from "../values.js" export const isRuntimeReference = (value: unknown): boolean => value instanceof CodeModeFunction || + value instanceof CodeModeGenerator || + value instanceof GeneratorMethodReference || value instanceof ToolReference || value instanceof IntrinsicReference || value instanceof GlobalNamespace || @@ -107,6 +111,7 @@ export const rejectCircularInsertion = (container: object, value: unknown, label export const typeofValue = (value: unknown): string => { if ( value instanceof CodeModeFunction || + value instanceof GeneratorMethodReference || value instanceof CoercionFunction || value instanceof IntrinsicReference || value instanceof GlobalMethodReference || diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts index 765b97ee5bab..1d4e03066513 100644 --- a/packages/codemode/src/interpreter/runtime.ts +++ b/packages/codemode/src/interpreter/runtime.ts @@ -1,4 +1,4 @@ -import { Cause, Effect, Exit } from "effect" +import { Cause, Deferred, Effect, Exit } from "effect" import { isBlockedMember, ToolReference, ToolRuntimeError, type SafeObject } from "../tool-runtime.js" import { type AstNode, @@ -6,11 +6,15 @@ import { asNode, type Binding, CodeModeFunction, + CodeModeGenerator, CoercionFunction, ComputedValue, ErrorConstructorReference, GlobalMethodReference, GlobalNamespace, + GeneratorMethodReference, + GeneratorReturn, + type GeneratorRequestKind, type GlobalNamespaceName, getArray, getBoolean, @@ -35,11 +39,10 @@ import { SearchFunction, SymbolNamespace, type StatementResult, - supportedSyntaxMessage, unsupportedSyntax, UriFunction, } from "./model.js" -import { caughtErrorValue, constructErrorValue } from "./errors.js" +import { caughtErrorValue, constructAggregateErrorValue, constructErrorValue } from "./errors.js" import { arrayStatics, type CallbackRunner, @@ -48,6 +51,7 @@ import { invokeGroupBy, invokeIntrinsic, } from "./methods.js" +import { preserveConsumerError, type SyncIteratorRunner } from "./iterator.js" import { constructPromise, invokePromiseInstanceMethod, @@ -57,13 +61,13 @@ import { } from "./promises.js" import { containsOpaqueReference, isRuntimeReference, rejectCircularInsertion, typeofValue } from "./references.js" import { ScopeStack } from "./scope.js" -import { arrayMethods, mapMethods, mapStatics, setMethods, spreadItems } from "../stdlib/collections.js" +import { arrayMethods, mapMethods, mapStatics, setMethods } from "../stdlib/collections.js" import { consoleMethods, formatConsoleMessage } from "../stdlib/console.js" import { dateMethods, dateStatics } from "../stdlib/date.js" import { invokeJsonMethod, jsonStatics, type JsonMethodName } from "../stdlib/json.js" -import { mathConstants, mathMethods } from "../stdlib/math.js" +import { invokeMathSumPrecise, mathConstants, mathMethods } from "../stdlib/math.js" import { numberConstants, numberMethods, numberStatics } from "../stdlib/number.js" -import { objectMethodsPreservingIdentity, objectStatics } from "../stdlib/object.js" +import { invokeObjectFromEntries, objectMethodsPreservingIdentity, objectStatics } from "../stdlib/object.js" import { promiseStatics } from "../stdlib/promise.js" import { escapeRegexHint, @@ -216,11 +220,52 @@ const loopDeclaration = (left: AstNode, statement: "for...of" | "for...in") => { } type CustomIterator = { - iterator: SafeObject + iterator: SafeObject | CodeModeGenerator next: unknown asynchronous: boolean } +type OpaqueMemberReference = + | ToolReference + | PromiseMethodReference + | PromiseInstanceMethodReference + | IntrinsicReference + | GlobalMethodReference + | JsonMethodReference + | GeneratorMethodReference + +const isOpaqueMemberReference = (value: unknown): value is OpaqueMemberReference => + value instanceof ToolReference || + value instanceof PromiseMethodReference || + value instanceof PromiseInstanceMethodReference || + value instanceof IntrinsicReference || + value instanceof GlobalMethodReference || + value instanceof JsonMethodReference || + value instanceof GeneratorMethodReference + +const copyIteratorSymbols = (source: object, target: object, consumed?: ReadonlySet): void => { + for (const symbol of IteratorSymbols) { + if (!consumed?.has(symbol) && Object.hasOwn(source, symbol)) + Reflect.set(target, symbol, Reflect.get(source, symbol)) + } +} + +type GeneratorRequest = { + kind: GeneratorRequestKind + value: unknown + response: Deferred.Deferred +} + +type GeneratorState = { + started: boolean + completed: boolean + draining: boolean + active?: GeneratorRequest + pending: Array + pendingIndex: number + available?: Deferred.Deferred +} + export class Interpreter { private scopes: ScopeStack private readonly invokeTool: (path: ReadonlyArray, args: Array) => Effect.Effect @@ -228,10 +273,13 @@ export class Interpreter { private readonly toolKeys: (path: ReadonlyArray) => ReadonlyArray private readonly logs: Array private readonly promises: PromiseRuntime - private readonly runner: CallbackRunner = { + private generatorState?: GeneratorState + private generatorAsync = false + private readonly runner: CallbackRunner & SyncIteratorRunner = { invokeFunction: (fn, args) => this.invokeFunction(fn, args), invokeCallable: (callable, args, node) => this.invokeCallable(callable, args, node), settlePromise: (promise) => this.settlePromise(promise), + syncIterator: (value, node) => this.syncIterator(value, node), } constructor( @@ -403,16 +451,12 @@ export class Interpreter { } private createFunction(node: AstNode): CodeModeFunction { - if (node.generator === true) { - throw new InterpreterRuntimeError("Generator functions are not supported.", node, "UnsupportedSyntax", [ - supportedSyntaxMessage, - ]) - } return new CodeModeFunction( getArray(node, "params").map((parameter, index) => asNode(parameter, `params[${index}]`)), getNode(node, "body"), this.scopes.capture(), node.async === true, + node.generator === true, ) } @@ -646,14 +690,20 @@ export class Interpreter { const right = yield* self.evaluateExpression(getNode(node, "right")) const body = getNode(node, "body") - const iterable = spreadItems(right) - const iterator = iterable === undefined && awaiting ? yield* self.customIterator(right, node) : undefined - if (iterable === undefined && iterator === undefined) { + const iterator = yield* self.customIterator(right, node, awaiting) + const cursor = iterator === undefined ? yield* self.syncIterator(right, node) : undefined + if (iterator === undefined && cursor === undefined) { throw new InterpreterRuntimeError( - `${awaiting ? "for await...of" : "for...of"} requires an array, string, Map, Set, or URLSearchParams${awaiting ? ", or custom iterator" : ""} value.`, + `${awaiting ? "for await...of" : "for...of"} requires an array, string, Map, Set, or URLSearchParams, or custom iterator value.`, node, - ) + ).as("TypeError") } + const close = () => + iterator + ? self.closeIterator(iterator, node, awaiting) + : awaiting + ? Effect.andThen(cursor?.close ?? Effect.void, Effect.yieldNow) + : (cursor?.close ?? Effect.void) let assignment: AstNode | undefined @@ -687,45 +737,35 @@ export class Interpreter { ), ) - if (iterable !== undefined) { - for (const value of iterable) { - const result = yield* evaluateBody(awaiting ? yield* self.awaitValue(value) : value) - - if (result.kind === "return") return result - if (result.kind === "break") { - if (result.label !== undefined && !labels?.has(result.label)) return result - return { kind: "none" } satisfies StatementResult - } - if (result.kind === "continue" && result.label !== undefined && !labels?.has(result.label)) return result - } - return { kind: "none" } satisfies StatementResult - } - if (iterator === undefined) throw new InterpreterRuntimeError("Custom iterator is unavailable.", node) - while (true) { - const step = yield* self.nextIteratorResult(iterator, node) + const current = iterator + ? yield* self.nextIteratorResult(iterator, node, awaiting) + : yield* cursor?.next ?? Effect.fail(new InterpreterRuntimeError("Iterator is unavailable.", node)) + const step = cursor && awaiting ? { done: current.done, value: yield* self.awaitValue(current.value) } : current if (step.done) return { kind: "none" } satisfies StatementResult const bodyExit = yield* Effect.exit(evaluateBody(step.value)) if (!Exit.isSuccess(bodyExit)) { // Process interruption must remain prompt; user cleanup cannot extend a timeout. - if (!Cause.hasInterruptsOnly(bodyExit.cause)) yield* Effect.exit(self.closeIterator(iterator, node)) + if (!Cause.hasInterruptsOnly(bodyExit.cause)) { + yield* Effect.exit(close()) + } return yield* Effect.failCause(bodyExit.cause) } const result = bodyExit.value if (result.kind === "return") { - yield* self.closeIterator(iterator, node) + yield* close() return result } if (result.kind === "break") { - yield* self.closeIterator(iterator, node) + yield* close() if (result.label !== undefined && !labels?.has(result.label)) return result return { kind: "none" } satisfies StatementResult } if (result.kind === "continue" && result.label !== undefined && !labels?.has(result.label)) { - yield* self.closeIterator(iterator, node) + yield* close() return result } } @@ -742,26 +782,86 @@ export class Interpreter { return value instanceof CodeModePromise ? this.settlePromise(value) : Effect.as(Effect.yieldNow, value) } - private customIterator(value: unknown, node: AstNode) { + private awaitAsyncFromSyncValue( + iterator: CustomIterator, + value: unknown, + node: AstNode, + closeOnRejection: boolean, + ): Effect.Effect { + const self = this + return Effect.gen(function* () { + const settled = yield* Effect.exit(self.awaitValue(value)) + if (Exit.isSuccess(settled)) return settled.value + if (closeOnRejection && !Cause.hasInterruptsOnly(settled.cause)) { + yield* Effect.exit(self.closeIterator(iterator, node, false)) + } + return yield* Effect.failCause(settled.cause) + }) + } + + private syncIterator(value: unknown, node: AstNode) { + const iterator = Array.isArray(value) + ? value[Symbol.iterator]() + : typeof value === "string" + ? value[Symbol.iterator]() + : value instanceof CodeModeMap + ? value.map.entries() + : value instanceof CodeModeSet + ? value.set.values() + : value instanceof CodeModeURLSearchParams + ? value.params.entries() + : undefined + if (iterator !== undefined) { + return Effect.succeed({ + next: Effect.sync(() => { + const step = iterator.next() + return { done: Boolean(step.done), value: step.value } + }), + close: Effect.void, + }) + } + const self = this + return Effect.map(this.customIterator(value, node, false), (iterator) => + iterator === undefined + ? undefined + : { + next: self.nextIteratorResult(iterator, node, false), + close: Effect.suspend(() => self.closeIterator(iterator, node, false)), + }, + ) + } + + private customIterator(value: unknown, node: AstNode, allowAsync = true) { + if (value instanceof CodeModeGenerator) { + if (value.asynchronous && !allowAsync) return Effect.succeed(undefined) + return Effect.succeed({ + iterator: value, + next: new GeneratorMethodReference(value, "next"), + asynchronous: value.asynchronous, + }) + } if (!isRecord(value) || isRuntimeReference(value)) return Effect.succeed(undefined) - const asyncMethod = Reflect.get(value, AsyncIteratorSymbol) + const asyncMethod = allowAsync ? Reflect.get(value, AsyncIteratorSymbol) : undefined const method = asyncMethod ?? Reflect.get(value, IteratorSymbol) if (method === undefined || method === null) return Effect.succeed(undefined) const self = this return Effect.map( this.invokeCallable(this.requireIteratorMethod(method, "Iterator method", node), [], node), (iterator) => { - const object = self.requireIteratorObject(iterator, "Iterator method result", node) + const object = self.requireIterator(iterator, node) return { iterator: object, - next: self.requireIteratorMethod(object.next, "Iterator next", node), + next: + object instanceof CodeModeGenerator + ? new GeneratorMethodReference(object, "next") + : self.requireIteratorMethod(object.next, "Iterator next", node), asynchronous: asyncMethod !== undefined && asyncMethod !== null, } }, ) } - private nextIteratorResult(iterator: CustomIterator, node: AstNode) { + private nextIteratorResult(iterator: CustomIterator, node: AstNode, awaiting: boolean) { const self = this return Effect.gen(function* () { if (iterator.asynchronous) { @@ -775,7 +875,7 @@ export class Interpreter { const called = yield* Effect.exit(self.invokeCallable(iterator.next, [], node)) if (!Exit.isSuccess(called)) { - yield* Effect.yieldNow + if (awaiting) yield* Effect.yieldNow return yield* Effect.failCause(called.cause) } const captured = yield* Effect.exit( @@ -785,16 +885,24 @@ export class Interpreter { }), ) if (!Exit.isSuccess(captured)) { - yield* Effect.yieldNow + if (awaiting) yield* Effect.yieldNow return yield* Effect.failCause(captured.cause) } - return { done: captured.value.done, value: yield* self.awaitValue(captured.value.value) } + return { + done: captured.value.done, + value: awaiting + ? yield* self.awaitAsyncFromSyncValue(iterator, captured.value.value, node, !captured.value.done) + : captured.value.value, + } }) } - private closeIterator(iterator: CustomIterator, node: AstNode): Effect.Effect { - const close = iterator.iterator.return - if (close === undefined || close === null) return iterator.asynchronous ? Effect.void : Effect.yieldNow + private closeIterator(iterator: CustomIterator, node: AstNode, awaiting = true): Effect.Effect { + const close = + iterator.iterator instanceof CodeModeGenerator + ? new GeneratorMethodReference(iterator.iterator, "return") + : iterator.iterator.return + if (close === undefined || close === null) return iterator.asynchronous || !awaiting ? Effect.void : Effect.yieldNow const self = this return Effect.gen(function* () { const method = self.requireIteratorMethod(close, "Iterator return", node) @@ -809,17 +917,17 @@ export class Interpreter { const called = yield* Effect.exit(self.invokeCallable(method, [], node)) if (!Exit.isSuccess(called)) { - yield* Effect.yieldNow + if (awaiting) yield* Effect.yieldNow return yield* Effect.failCause(called.cause) } const captured = yield* Effect.exit( Effect.sync(() => self.requireIteratorObject(called.value, "Iterator return() result", node).value), ) if (!Exit.isSuccess(captured)) { - yield* Effect.yieldNow + if (awaiting) yield* Effect.yieldNow return yield* Effect.failCause(captured.cause) } - yield* self.awaitValue(captured.value) + if (awaiting) yield* self.awaitValue(captured.value) }) } @@ -828,6 +936,12 @@ export class Interpreter { throw new InterpreterRuntimeError(`${context} must be an object.`, node).as("TypeError") } + private requireIterator(value: unknown, node: AstNode): SafeObject | CodeModeGenerator { + return value instanceof CodeModeGenerator + ? value + : this.requireIteratorObject(value, "Iterator method result", node) + } + private requireIteratorMethod(value: unknown, context: string, node: AstNode): unknown { if (typeofValue(value) === "function") return value throw new InterpreterRuntimeError(`${context} must be a function.`, node).as("TypeError") @@ -966,7 +1080,7 @@ export class Interpreter { const attempted = Effect.matchCauseEffect(this.evaluateStatement(body), { onFailure: (cause) => { - if (cause.reasons.some(Cause.isInterruptReason) || !handler) { + if (cause.reasons.some(Cause.isInterruptReason) || Cause.squash(cause) instanceof GeneratorReturn || !handler) { return Effect.failCause(cause) } @@ -1059,10 +1173,7 @@ export class Interpreter { for (const [key, item] of Object.entries(value as SafeObject)) { if (!consumed.has(key) && !isBlockedMember(key)) rest[key] = item } - for (const symbol of IteratorSymbols) { - if (!consumed.has(symbol) && Object.hasOwn(value, symbol)) - Reflect.set(rest, symbol, Reflect.get(value, symbol)) - } + copyIteratorSymbols(value, rest, consumed) yield* self.declarePattern(getNode(property, "argument"), rest, mutable, property, initialize) continue } @@ -1084,21 +1195,9 @@ export class Interpreter { } if (pattern.type === "ArrayPattern") { - const items = spreadItems(value) - if (items === undefined) { - throw new InterpreterRuntimeError("Array destructuring requires a supported iterable value.", pattern) - } - - for (const [index, item] of getArray(pattern, "elements").entries()) { - if (item === null) continue - const element = asNode(item, `elements[${index}]`) - if (element.type === "RestElement") { - yield* self.declarePattern(getNode(element, "argument"), items.slice(index), mutable, element, initialize) - break - } - yield* self.declarePattern(element, items[index], mutable, pattern, initialize) - } - return + return yield* self.destructureArrayPattern(pattern, value, (target, item, context) => + self.declarePattern(target, item, mutable, context, initialize), + ) } throw new InterpreterRuntimeError(`Unsupported binding pattern '${pattern.type}'.`, pattern) @@ -1142,10 +1241,7 @@ export class Interpreter { for (const [key, item] of Object.entries(source)) { if (!consumed.has(key) && !isBlockedMember(key)) rest[key] = item } - for (const symbol of IteratorSymbols) { - if (!consumed.has(symbol) && Object.hasOwn(source, symbol)) - Reflect.set(rest, symbol, Reflect.get(source, symbol)) - } + copyIteratorSymbols(source, rest, consumed) yield* self.assignPattern(getNode(property, "argument"), rest, property) continue } @@ -1160,23 +1256,60 @@ export class Interpreter { } if (pattern.type === "ArrayPattern") { - const items = spreadItems(value) - if (items === undefined) { - throw new InterpreterRuntimeError("Array destructuring requires a supported iterable value.", pattern) - } - for (const [index, item] of getArray(pattern, "elements").entries()) { + return yield* self.destructureArrayPattern(pattern, value, (target, item, context) => + self.assignPattern(target, item, context), + ) + } + + throw new InterpreterRuntimeError(`Unsupported assignment pattern '${pattern.type}'.`, node) + }) + } + + private destructureArrayPattern( + pattern: AstNode, + value: unknown, + consume: (target: AstNode, value: unknown, context: AstNode) => Effect.Effect, + ): Effect.Effect { + const self = this + return Effect.gen(function* () { + const cursor = yield* self.syncIterator(value, pattern) + if (cursor === undefined) { + throw new InterpreterRuntimeError("Array destructuring requires a supported iterable value.", pattern).as( + "TypeError", + ) + } + let done = false + for (const [index, item] of getArray(pattern, "elements").entries()) { + if (done) { if (item === null) continue const element = asNode(item, `elements[${index}]`) - if (element.type === "RestElement") { - yield* self.assignPattern(getNode(element, "argument"), items.slice(index), element) - break + yield* consume( + element.type === "RestElement" ? getNode(element, "argument") : element, + element.type === "RestElement" ? [] : undefined, + element, + ) + if (element.type === "RestElement") return + continue + } + const step = yield* cursor.next + done = step.done + if (item === null) continue + const element = asNode(item, `elements[${index}]`) + if (element.type === "RestElement") { + const rest: Array = [] + if (!step.done) rest.push(step.value) + while (!done) { + const next = yield* cursor.next + done = next.done + if (!done) rest.push(next.value) } - yield* self.assignPattern(element, items[index], pattern) + yield* consume(getNode(element, "argument"), rest, element) + return } - return + const consumed = consume(element, step.done ? undefined : step.value, pattern) + yield* step.done ? consumed : preserveConsumerError(cursor, consumed) } - - throw new InterpreterRuntimeError(`Unsupported assignment pattern '${pattern.type}'.`, node) + if (!done) yield* cursor.close }) } @@ -1259,6 +1392,8 @@ export class Interpreter { value instanceof CodeModePromise ? self.settlePromise(value) : Effect.as(Effect.yieldNow, value), ) } + case "YieldExpression": + return this.evaluateYieldExpression(node) case "NewExpression": return this.evaluateNewExpression(node) default: @@ -1280,7 +1415,11 @@ export class Interpreter { ) } if (errorConstructors.has(name)) { - return Effect.map(this.evaluateCallArguments(argNodes), (args) => constructErrorValue(name, args, node)) + return Effect.flatMap(this.evaluateCallArguments(argNodes), (args) => + name === "AggregateError" + ? constructAggregateErrorValue(self.runner, args, node) + : Effect.succeed(constructErrorValue(name, args)), + ) } // Array and Object construct identically with or without new, like JS. if (name === "Array") { @@ -1298,13 +1437,13 @@ export class Interpreter { case "RegExp": return self.constructRegExp(args, node) case "Map": - return self.constructMap(args[0], node) + return yield* self.constructMap(args[0], node) case "Set": - return self.constructSet(args[0], node) + return yield* self.constructSet(args[0], node) case "URL": return self.constructURL(args, node) default: - return self.constructURLSearchParams(args[0], node) + return yield* self.constructURLSearchParams(args[0], node) } }) } @@ -1390,44 +1529,53 @@ export class Interpreter { } } - private constructMap(init: unknown, node: AstNode): CodeModeMap { + private constructMap(init: unknown, node: AstNode): Effect.Effect { const target = new CodeModeMap() - if (init === undefined || init === null) return target - const entries = Array.isArray(init) - ? init - : init instanceof CodeModeMap - ? Array.from(init.map.entries(), ([key, item]): Array => [key, item]) - : undefined - if (entries === undefined) { - throw new InterpreterRuntimeError( - "new Map(...) expects an array of [key, value] pairs, a Map, or no argument.", - node, - ) - } - for (const pair of entries) { - if (!Array.isArray(pair)) { - throw new InterpreterRuntimeError("new Map(...) expects [key, value] pairs.", node) + if (init === undefined || init === null) return Effect.succeed(target) + const self = this + return Effect.gen(function* () { + const cursor = yield* self.syncIterator(init, node) + if (cursor === undefined) { + throw new InterpreterRuntimeError( + "new Map(...) expects an iterable of [key, value] pairs or no argument.", + node, + ).as("TypeError") } - target.map.set(pair[0], pair[1]) - } - return target + while (true) { + const step = yield* cursor.next + if (step.done) return target + yield* preserveConsumerError( + cursor, + Effect.sync(() => { + if (!isRecord(step.value) || isRuntimeReference(step.value)) { + throw new InterpreterRuntimeError("new Map(...) expects [key, value] pairs as entry objects.", node).as( + "TypeError", + ) + } + target.map.set(step.value[0], step.value[1]) + }), + ) + } + }) } - private constructSet(init: unknown, node: AstNode): CodeModeSet { + private constructSet(init: unknown, node: AstNode): Effect.Effect { const target = new CodeModeSet() - if (init === undefined || init === null) return target - const items = Array.isArray(init) - ? init - : init instanceof CodeModeSet - ? Array.from(init.set.values()) - : typeof init === "string" - ? Array.from(init) - : undefined - if (items === undefined) { - throw new InterpreterRuntimeError("new Set(...) expects an array, Set, string, or no argument.", node) - } - for (const item of items) target.set.add(item) - return target + if (init === undefined || init === null) return Effect.succeed(target) + const self = this + return Effect.gen(function* () { + const cursor = yield* self.syncIterator(init, node) + if (cursor === undefined) { + throw new InterpreterRuntimeError("new Set(...) expects a synchronous iterable or no argument.", node).as( + "TypeError", + ) + } + while (true) { + const step = yield* cursor.next + if (step.done) return target + target.set.add(step.value) + } + }) } private constructURL(args: Array, node: AstNode): CodeModeURL { @@ -1448,47 +1596,79 @@ export class Interpreter { } } - private constructURLSearchParams(init: unknown, node: AstNode): CodeModeURLSearchParams { - if (init === undefined) return new CodeModeURLSearchParams(new URLSearchParams()) + private constructURLSearchParams(init: unknown, node: AstNode): Effect.Effect { + if (init === undefined) return Effect.succeed(new CodeModeURLSearchParams(new URLSearchParams())) if (init instanceof CodeModeURLSearchParams) { - return new CodeModeURLSearchParams(new URLSearchParams(init.params)) + return Effect.succeed(new CodeModeURLSearchParams(new URLSearchParams(init.params))) } - if (typeof init === "string") return new CodeModeURLSearchParams(new URLSearchParams(init)) + if (typeof init === "string") return Effect.succeed(new CodeModeURLSearchParams(new URLSearchParams(init))) if (init === null || typeof init === "number" || typeof init === "boolean") { - return new CodeModeURLSearchParams(new URLSearchParams(coerceToString(init))) + return Effect.succeed(new CodeModeURLSearchParams(new URLSearchParams(coerceToString(init)))) } - if (init instanceof CodeModeMap) { - return this.constructURLSearchParams( - Array.from(init.map.entries(), ([key, value]) => [key, value]), - node, + const self = this + return Effect.gen(function* () { + const cursor = yield* self.syncIterator(init, node) + if (cursor !== undefined) { + const entries: Array> = [] + while (true) { + const step = yield* cursor.next + if (step.done) { + if (entries.some((entry) => entry.length !== 2)) { + throw new InterpreterRuntimeError( + "new URLSearchParams(...) expects iterable [name, value] pairs.", + node, + ).as("TypeError") + } + return new CodeModeURLSearchParams( + new URLSearchParams(entries.map((entry): [string, string] => [entry[0] ?? "", entry[1] ?? ""])), + ) + } + entries.push(yield* preserveConsumerError(cursor, self.readURLSearchParamsPair(step.value, node))) + } + } + if (isRuntimeReference(init)) { + throw new InterpreterRuntimeError( + "new URLSearchParams(...) expects a query string, data object, or synchronous iterable pairs.", + node, + ).as("TypeError") + } + if (isCodeModeValue(init)) return new CodeModeURLSearchParams(new URLSearchParams()) + const data = boundedData(init, "new URLSearchParams input") + if (data === null || typeof data !== "object") { + throw new InterpreterRuntimeError( + "new URLSearchParams(...) expects a query string, data object, iterable pairs, or URLSearchParams.", + node, + ).as("TypeError") + } + return new CodeModeURLSearchParams( + new URLSearchParams( + Object.fromEntries(Object.entries(data).map(([key, value]) => [key, coerceToString(value)])), + ), ) - } - if (Array.isArray(init)) { - const entries = init.map((pair) => { - if (!Array.isArray(pair) || pair.length !== 2) { - throw new InterpreterRuntimeError( - "new URLSearchParams(...) expects an array of [name, value] pairs.", - node, - ).as("TypeError") - } - return [uriArgument(pair[0], "URLSearchParams name"), uriArgument(pair[1], "URLSearchParams value")] as [ - string, - string, - ] - }) - return new CodeModeURLSearchParams(new URLSearchParams(entries)) - } - if (isCodeModeValue(init)) return new CodeModeURLSearchParams(new URLSearchParams()) - const data = boundedData(init, "new URLSearchParams input") - if (data === null || typeof data !== "object") { - throw new InterpreterRuntimeError( - "new URLSearchParams(...) expects a query string, data object, array of pairs, or URLSearchParams.", - node, - ).as("TypeError") - } - return new CodeModeURLSearchParams( - new URLSearchParams(Object.fromEntries(Object.entries(data).map(([key, value]) => [key, coerceToString(value)]))), - ) + }) + } + + private readURLSearchParamsPair(value: unknown, node: AstNode): Effect.Effect, unknown, R> { + const self = this + return Effect.gen(function* () { + const cursor = yield* self.syncIterator(value, node) + if (cursor === undefined) { + throw new InterpreterRuntimeError("new URLSearchParams(...) expects iterable [name, value] pairs.", node).as( + "TypeError", + ) + } + const items: Array = [] + while (true) { + const step = yield* cursor.next + if (step.done) return items + items.push( + yield* preserveConsumerError( + cursor, + Effect.sync(() => uriArgument(step.value, "URLSearchParams pair value")), + ), + ) + } + }) } private evaluateBinaryExpression(node: AstNode): Effect.Effect { @@ -1503,6 +1683,8 @@ export class Interpreter { } private applyBinaryOperator(operator: string, lhs: unknown, rhs: unknown, node: AstNode): unknown { + if (operator === "===") return lhs === rhs + if (operator === "!==") return lhs !== rhs if (containsOpaqueReference(lhs) || containsOpaqueReference(rhs)) { throw new InterpreterRuntimeError("Binary operators require data values.", node, "InvalidDataValue") } @@ -1532,12 +1714,8 @@ export class Interpreter { return (l as number) ** (r as number) case "==": return bothObjects ? lhs === rhs : l == r - case "===": - return lhs === rhs case "!=": return bothObjects ? lhs !== rhs : l != r - case "!==": - return lhs !== rhs case "<": return (l as string) < (r as string) case "<=": @@ -1773,6 +1951,11 @@ export class Interpreter { if (callable instanceof CodeModeFunction) { return yield* self.invokeFunction(callable, args) } + if (callable instanceof GeneratorMethodReference) { + if (callable.kind === "iterator") return callable.generator + const requested = callable.generator.request(callable.kind, args[0], node) as Effect.Effect + return callable.generator.asynchronous ? yield* self.createPromise(requested) : yield* requested + } if (callable instanceof IntrinsicReference) { return yield* invokeIntrinsic(self.runner, callable, args, node) } @@ -1782,6 +1965,7 @@ export class Interpreter { return self.invokeObjectMethodOnTools(callable.name, args[0], node) } if (callable.namespace === "Object" && objectMethodsPreservingIdentity.has(callable.name)) { + if (callable.name === "fromEntries") return yield* invokeObjectFromEntries(self.runner, args[0], node) return invokeGlobalMethod(callable, args, node) } if (callable.namespace === "Array" && callable.name === "from") { @@ -1790,6 +1974,9 @@ export class Interpreter { if ((callable.namespace === "Object" || callable.namespace === "Map") && callable.name === "groupBy") { return yield* invokeGroupBy(self.runner, callable.namespace, args, node) } + if (callable.namespace === "Math" && callable.name === "sumPrecise") { + return yield* invokeMathSumPrecise(self.runner, args[0], node) + } if (callable.namespace === "Array" && callable.name === "of") { return invokeGlobalMethod(callable, args, node) } @@ -1808,7 +1995,8 @@ export class Interpreter { return yield* self.invokeSearch(args) } if (callable instanceof ErrorConstructorReference) { - return constructErrorValue(callable.name, args, node) + if (callable.name === "AggregateError") return yield* constructAggregateErrorValue(self.runner, args, node) + return constructErrorValue(callable.name, args) } if (callable instanceof GlobalNamespace) { // Real JS permits calling Array, Object, Date, and RegExp without new. @@ -1868,10 +2056,16 @@ export class Interpreter { const argNode = asNode(arg, `arguments[${index}]`) if (argNode.type === "SpreadElement") { const spread = yield* self.evaluateExpression(getNode(argNode, "argument")) - const items = spreadItems(spread) - if (items === undefined) - throw new InterpreterRuntimeError("Spread arguments require an array, string, Map, or Set.", argNode) - args.push(...items) + const cursor = yield* self.syncIterator(spread, argNode) + if (cursor === undefined) + throw new InterpreterRuntimeError("Spread arguments require a synchronous iterable.", argNode).as( + "TypeError", + ) + while (true) { + const step = yield* cursor.next + if (step.done) break + args.push(step.value) + } } else { args.push(yield* self.evaluateExpression(argNode)) } @@ -1906,6 +2100,7 @@ export class Interpreter { return yield* invocation.evaluateExpression(fn.body) }) + if (fn.generator) return Effect.succeed(this.createGenerator(invocation, run, fn.async)) if (!fn.async) return run // The initial yield assigns `box.own` before the body can self-resolve. const box: { own?: CodeModePromise } = {} @@ -1924,6 +2119,250 @@ export class Interpreter { ) } + private createGenerator( + invocation: Interpreter, + run: Effect.Effect, + asynchronous: boolean, + ): CodeModeGenerator { + const state: GeneratorState = { started: false, completed: false, draining: false, pending: [], pendingIndex: 0 } + invocation.generatorState = state + invocation.generatorAsync = asynchronous + const generator = new CodeModeGenerator(asynchronous, (kind, value, node) => { + const request = { kind, value, response: Deferred.makeUnsafe() } + if (!asynchronous && state.active) { + return Effect.fail(new InterpreterRuntimeError("Generator is already running.", node).as("TypeError")) + } + if (asynchronous && (state.completed || (!state.started && kind !== "next"))) { + state.started = true + state.completed = true + state.pending.push(request) + if (state.draining) return Deferred.await(request.response) + state.draining = true + return Effect.andThen( + this.promises.fork( + invocation + .completeGeneratorRequests(state, true) + .pipe(Effect.ensuring(Effect.sync(() => (state.draining = false)))), + ), + Deferred.await(request.response), + ) + } + if (state.completed) { + if (kind === "throw") return Effect.fail(new ProgramThrow(value)) + return Effect.succeed({ value: kind === "return" ? value : undefined, done: true }) + } + if (!state.started && kind !== "next") { + state.completed = true + if (kind === "throw") return Effect.fail(new ProgramThrow(value)) + return Effect.succeed({ value, done: true }) + } + + state.pending.push(request) + if (state.available) { + const available = state.available + state.available = undefined + Deferred.doneUnsafe(available, Exit.succeed(undefined)) + } + if (!state.started) { + state.started = true + const body = Effect.gen(function* () { + state.active = yield* invocation.takeGeneratorRequest(state) + const exit = yield* Effect.exit( + run.pipe( + Effect.flatMap((result) => (asynchronous ? invocation.awaitValue(result) : Effect.succeed(result))), + Effect.catch((error) => + error instanceof GeneratorReturn + ? asynchronous + ? invocation.awaitValue(error.value) + : Effect.succeed(error.value) + : Effect.fail(error), + ), + ), + ) + const active = state.active + state.active = undefined + if (active) { + Deferred.doneUnsafe( + active.response, + Exit.isSuccess(exit) ? Exit.succeed({ value: exit.value, done: true }) : exit, + ) + } + yield* invocation.completeGeneratorRequests(state, asynchronous) + state.completed = true + }) + return Effect.andThen(this.promises.fork(body), Deferred.await(request.response)) + } + return Deferred.await(request.response) + }) + return generator + } + + private completeGeneratorRequests(state: GeneratorState, asynchronous: boolean): Effect.Effect { + const self = this + return Effect.gen(function* () { + while (true) { + const pending = self.dequeueGeneratorRequest(state) + if (!pending) return + if (pending.kind === "throw") { + Deferred.doneUnsafe(pending.response, Exit.fail(new ProgramThrow(pending.value))) + continue + } + if (asynchronous && pending.kind === "return") { + const resolved = yield* Effect.exit(self.awaitValue(pending.value)) + Deferred.doneUnsafe( + pending.response, + Exit.isSuccess(resolved) ? Exit.succeed({ value: resolved.value, done: true }) : resolved, + ) + continue + } + Deferred.doneUnsafe( + pending.response, + Exit.succeed({ value: pending.kind === "return" ? pending.value : undefined, done: true }), + ) + } + }) + } + + private takeGeneratorRequest(state: GeneratorState): Effect.Effect { + const next = this.dequeueGeneratorRequest(state) + if (next) return Effect.succeed(next) + state.available = Deferred.makeUnsafe() + return Effect.andThen( + Deferred.await(state.available), + Effect.sync(() => this.dequeueGeneratorRequest(state)!), + ) + } + + private dequeueGeneratorRequest(state: GeneratorState): GeneratorRequest | undefined { + const request = state.pending[state.pendingIndex] + if (!request) return undefined + state.pendingIndex += 1 + if (state.pendingIndex === state.pending.length) { + state.pending = [] + state.pendingIndex = 0 + } + return request + } + + private evaluateYieldExpression(node: AstNode): Effect.Effect { + const argument = getOptionalNode(node, "argument") + const self = this + return Effect.gen(function* () { + if (!self.generatorState) throw new InterpreterRuntimeError("yield is only valid inside a generator.", node) + if (node.delegate === true) { + const value = argument ? yield* self.evaluateExpression(argument) : undefined + return yield* self.delegateYield(value, node) + } + const value = argument ? yield* self.evaluateExpression(argument) : undefined + const yielded = self.generatorAsync ? yield* self.awaitValue(value) : value + return yield* self.suspendGenerator(yielded, node) + }) + } + + private suspendGenerator(value: unknown, node: AstNode): Effect.Effect { + const state = this.generatorState + if (!state?.active) throw new InterpreterRuntimeError("Generator has no active request.", node) + Deferred.doneUnsafe(state.active.response, Exit.succeed({ value, done: false })) + state.active = undefined + return Effect.flatMap(this.takeGeneratorRequest(state), (request) => { + state.active = request + if (request.kind === "next") return Effect.succeed(request.value) + if (request.kind === "throw") return Effect.fail(new ProgramThrow(request.value)) + return this.generatorAsync + ? Effect.flatMap(this.awaitValue(request.value), (value) => Effect.fail(new GeneratorReturn(value))) + : Effect.fail(new GeneratorReturn(request.value)) + }) + } + + private delegateYield(value: unknown, node: AstNode): Effect.Effect { + const self = this + return Effect.gen(function* () { + if ( + Array.isArray(value) || + typeof value === "string" || + value instanceof CodeModeMap || + value instanceof CodeModeSet || + value instanceof CodeModeURLSearchParams + ) { + const cursor = yield* self.syncIterator(value, node) + if (!cursor) throw new InterpreterRuntimeError("Built-in iterator is unavailable.", node) + while (true) { + const step = yield* cursor.next + if (step.done) return undefined + const resumed = yield* Effect.exit( + self.suspendGenerator(self.generatorAsync ? yield* self.awaitValue(step.value) : step.value, node), + ) + if (Exit.isSuccess(resumed)) continue + const error = Cause.squash(resumed.cause) + if (error instanceof GeneratorReturn) { + yield* cursor.close + return yield* Effect.fail(error) + } + if (error instanceof ProgramThrow) { + yield* cursor.close + throw new InterpreterRuntimeError("The delegated iterator does not provide a throw() method.", node).as( + "TypeError", + ) + } + return yield* Effect.failCause(resumed.cause) + } + } + + const iterator = yield* self.customIterator(value, node, self.generatorAsync) + if (!iterator) + throw new InterpreterRuntimeError("yield* requires a compatible iterable value.", node).as("TypeError") + let kind: GeneratorRequestKind = "next" + let input: unknown = undefined + while (true) { + const method = + kind === "next" + ? iterator.next + : iterator.iterator instanceof CodeModeGenerator + ? new GeneratorMethodReference(iterator.iterator, kind) + : iterator.iterator[kind] + if (method === undefined || method === null) { + if (kind === "return") return yield* Effect.fail(new GeneratorReturn(input)) + yield* self.closeIterator(iterator, node, self.generatorAsync) + throw new InterpreterRuntimeError("The delegated iterator does not provide a throw() method.", node).as( + "TypeError", + ) + } + const called = yield* self.invokeCallable( + self.requireIteratorMethod(method, `Iterator ${kind}`, node), + [input], + node, + ) + const result = self.requireIteratorObject( + iterator.asynchronous ? yield* self.awaitValue(called) : called, + `Iterator ${kind}() result`, + node, + ) + const done = Boolean(result.done) + const resultValue: unknown = + self.generatorAsync && !iterator.asynchronous + ? yield* self.awaitAsyncFromSyncValue(iterator, result.value, node, kind !== "return" && !done) + : result.value + if (done) { + if (kind === "return") return yield* Effect.fail(new GeneratorReturn(resultValue)) + return resultValue + } + + const resumed: Exit.Exit = yield* Effect.exit(self.suspendGenerator(resultValue, node)) + if (Exit.isSuccess(resumed)) { + kind = "next" + input = resumed.value + continue + } + const error: unknown = Cause.squash(resumed.cause) + if (!(error instanceof GeneratorReturn) && !(error instanceof ProgramThrow)) { + return yield* Effect.failCause(resumed.cause) + } + kind = error instanceof GeneratorReturn ? "return" : "throw" + input = error.value + } + }) + } + private evaluateObjectExpression(node: AstNode): Effect.Effect, unknown, R> { const objectValue: Record = Object.create(null) as Record const properties = getArray(node, "properties") @@ -1942,9 +2381,7 @@ export class Interpreter { if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available.`, property) objectValue[key] = value } - for (const symbol of IteratorSymbols) { - if (Object.hasOwn(spread, symbol)) Reflect.set(objectValue, symbol, Reflect.get(spread, symbol)) - } + copyIteratorSymbols(spread, objectValue) continue } @@ -1997,10 +2434,14 @@ export class Interpreter { const element = asNode(elementValue, "elements") if (element.type === "SpreadElement") { const spread = yield* self.evaluateExpression(getNode(element, "argument")) - const items = spreadItems(spread) - if (items === undefined) - throw new InterpreterRuntimeError("Array spread requires an array, string, Map, or Set.", element) - values.push(...items) + const cursor = yield* self.syncIterator(spread, element) + if (cursor === undefined) + throw new InterpreterRuntimeError("Array spread requires a synchronous iterable.", element).as("TypeError") + while (true) { + const step = yield* cursor.next + if (step.done) break + values.push(step.value) + } } else { values.push(yield* self.evaluateExpression(element)) } @@ -2061,6 +2502,7 @@ export class Interpreter { | IntrinsicReference | GlobalMethodReference | JsonMethodReference + | GeneratorMethodReference | ComputedValue | typeof OptionalShortCircuit | undefined, @@ -2205,6 +2647,19 @@ export class Interpreter { ) } + if (objectValue instanceof CodeModeGenerator) { + if (key === "next" || key === "return" || key === "throw") { + return new GeneratorMethodReference(objectValue, key) + } + if ( + (key === IteratorSymbol && !objectValue.asynchronous) || + (key === AsyncIteratorSymbol && objectValue.asynchronous) + ) { + return new GeneratorMethodReference(objectValue, "iterator") + } + return new ComputedValue(undefined) + } + if (isRuntimeReference(objectValue)) { throw new InterpreterRuntimeError( "Runtime references are opaque and do not expose properties.", @@ -2241,16 +2696,7 @@ export class Interpreter { return Effect.map(this.getMemberReference(node), (reference) => { if (reference === OptionalShortCircuit) return OptionalShortCircuit if (reference instanceof ComputedValue) return reference.value - if ( - reference === undefined || - reference instanceof ToolReference || - reference instanceof PromiseMethodReference || - reference instanceof PromiseInstanceMethodReference || - reference instanceof IntrinsicReference || - reference instanceof GlobalMethodReference || - reference instanceof JsonMethodReference - ) - return reference + if (reference === undefined || isOpaqueMemberReference(reference)) return reference if (Array.isArray(reference.target)) { if (reference.key === "length") return reference.target.length if (typeof reference.key === "string") return new IntrinsicReference(reference.target, reference.key) @@ -2278,12 +2724,7 @@ export class Interpreter { if ( reference instanceof ComputedValue || reference === undefined || - reference instanceof ToolReference || - reference instanceof PromiseMethodReference || - reference instanceof PromiseInstanceMethodReference || - reference instanceof IntrinsicReference || - reference instanceof GlobalMethodReference || - reference instanceof JsonMethodReference || + isOpaqueMemberReference(reference) || reference.target instanceof CodeModeURL ) { throw new InterpreterRuntimeError("Only data fields may be deleted.", target, "InvalidDataValue") @@ -2307,12 +2748,7 @@ export class Interpreter { reference === OptionalShortCircuit || reference instanceof ComputedValue || reference === undefined || - reference instanceof ToolReference || - reference instanceof PromiseMethodReference || - reference instanceof PromiseInstanceMethodReference || - reference instanceof IntrinsicReference || - reference instanceof GlobalMethodReference || - reference instanceof JsonMethodReference + isOpaqueMemberReference(reference) ) { throw new InterpreterRuntimeError("Only data fields may be assigned.", node) } diff --git a/packages/codemode/src/stdlib/math.ts b/packages/codemode/src/stdlib/math.ts index 040153b97ecf..211d307cac25 100644 --- a/packages/codemode/src/stdlib/math.ts +++ b/packages/codemode/src/stdlib/math.ts @@ -1,5 +1,6 @@ +import { Effect } from "effect" +import { preserveConsumerError, type SyncIteratorRunner } from "../interpreter/iterator.js" import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" -import { spreadItems } from "./collections.js" // Bun exposes ES2026 Math.sumPrecise before TypeScript's standard library types. declare global { @@ -53,17 +54,6 @@ export const mathMethods = new Set([ export const invokeMathMethod = (name: string, args: Array, node: AstNode): number => { if (!mathMethods.has(name)) throw new InterpreterRuntimeError(`Math.${name} is not available.`, node) if (name === "random") return Math.random() - if (name === "sumPrecise") { - const items = spreadItems(args[0]) - if (items === undefined) { - throw new InterpreterRuntimeError("Math.sumPrecise expects an iterable collection.", node).as("TypeError") - } - const numbers = Array.from(items) - if (!numbers.every((item): item is number => typeof item === "number")) { - throw new InterpreterRuntimeError("Math.sumPrecise expects an iterable of numbers.", node).as("TypeError") - } - return Math.sumPrecise(numbers) - } // Validate only the arguments the method consumes; like JS, extras are ignored // (so built-ins work as callbacks receiving (element, index, array)). const num = (index: number): number => { @@ -153,3 +143,29 @@ export const invokeMathMethod = (name: string, args: Array, node: AstNo } throw new InterpreterRuntimeError(`Math.${name} is not available.`, node) } + +export const invokeMathSumPrecise = ( + runner: SyncIteratorRunner, + source: unknown, + node: AstNode, +): Effect.Effect => + Effect.gen(function* () { + const cursor = yield* runner.syncIterator(source, node) + if (cursor === undefined) { + throw new InterpreterRuntimeError("Math.sumPrecise expects a synchronous iterable.", node).as("TypeError") + } + const numbers: Array = [] + while (true) { + const step = yield* cursor.next + if (step.done) return Math.sumPrecise(numbers) + yield* preserveConsumerError( + cursor, + Effect.sync(() => { + if (typeof step.value !== "number") { + throw new InterpreterRuntimeError("Math.sumPrecise expects an iterable of numbers.", node).as("TypeError") + } + numbers.push(step.value) + }), + ) + } + }) diff --git a/packages/codemode/src/stdlib/object.ts b/packages/codemode/src/stdlib/object.ts index b4182904d614..583a4b4a42b5 100644 --- a/packages/codemode/src/stdlib/object.ts +++ b/packages/codemode/src/stdlib/object.ts @@ -1,3 +1,4 @@ +import { Effect } from "effect" import { type AstNode, AsyncIteratorSymbol, @@ -7,8 +8,9 @@ import { } from "../interpreter/model.js" import { containsOpaqueReference } from "../interpreter/references.js" import { isBlockedMember } from "../tool-runtime.js" -import { isCodeModeValue, CodeModeMap, CodeModePromise, CodeModeSet, CodeModeURLSearchParams } from "../values.js" +import { isCodeModeValue, CodeModePromise } from "../values.js" import { boundedData, coerceToString } from "./value.js" +import { preserveConsumerError, type SyncIteratorRunner } from "../interpreter/iterator.js" export const objectMethodsPreservingIdentity = new Set(["assign", "values", "entries", "fromEntries"]) @@ -39,11 +41,6 @@ export const invokeObjectMethod = (name: string, args: Array, node: Ast if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available.`, node) out[key] = item } - const addEntry = (out: Record, key: unknown, item: unknown): void => { - boundedData(key, "Object.fromEntries key") - boundedData(item, "Object.fromEntries value") - guardedSet(out, coerceToString(key), item) - } switch (name) { case "keys": return Object.keys(requireObject()) @@ -79,32 +76,47 @@ export const invokeObjectMethod = (name: string, args: Array, node: Ast } return out } - case "fromEntries": { - if (args[0] instanceof CodeModeMap) { - const out: Record = Object.create(null) - for (const [key, item] of args[0].map.entries()) addEntry(out, key, item) - return out - } - if (args[0] instanceof CodeModeURLSearchParams) { - const out: Record = Object.create(null) - for (const [key, value] of args[0].params.entries()) guardedSet(out, key, value) - return out - } - const pairs = args[0] instanceof CodeModeSet ? Array.from(args[0].set.values()) : args[0] - if (!Array.isArray(pairs)) { - boundedData(args[0], "Object.fromEntries input") - throw new InterpreterRuntimeError("Object.fromEntries expects an array of [key, value] pairs.", node) - } - const out: Record = Object.create(null) - for (const pair of pairs) { - const validated = boundedData(pair, "Object.fromEntries entry") - if (validated === null || typeof validated !== "object" || isCodeModeValue(validated)) - throw new InterpreterRuntimeError("Object.fromEntries expects [key, value] entry objects.", node) - const entry = pair as Record - addEntry(out, entry[0], entry[1]) - } - return out - } } throw new InterpreterRuntimeError(`Object.${name} is not available.`, node) } + +export const invokeObjectFromEntries = ( + runner: SyncIteratorRunner, + source: unknown, + node: AstNode, +): Effect.Effect, unknown, R> => { + const out: Record = Object.create(null) + return Effect.gen(function* () { + const cursor = yield* runner.syncIterator(source, node) + if (cursor === undefined) { + throw new InterpreterRuntimeError("Object.fromEntries expects a synchronous iterable of entries.", node).as( + "TypeError", + ) + } + while (true) { + const step = yield* cursor.next + if (step.done) return out + yield* preserveConsumerError( + cursor, + Effect.sync(() => { + if ( + step.value === null || + typeof step.value !== "object" || + isCodeModeValue(step.value) || + containsOpaqueReference(step.value) + ) { + throw new InterpreterRuntimeError("Object.fromEntries expects [key, value] entry objects.", node).as( + "TypeError", + ) + } + const entry = step.value as Record + boundedData(entry[0], "Object.fromEntries key") + boundedData(entry[1], "Object.fromEntries value") + const key = coerceToString(entry[0]) + if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available.`, node) + out[key] = entry[1] + }), + ) + } + }) +} diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index f64858aa355a..febc2ba58606 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -553,7 +553,7 @@ export const prepare = (tools: Tools, catalogBudget = defaultCatalogBudget "## Language", "", "Use common JavaScript data operations, functions, control flow, selected standard-library methods, and awaited tool calls. Built-ins include Date, RegExp, Map, Set, URL, URLSearchParams, and URI encoding helpers.", - "Modules/imports, classes, generators, timers, fetch, eval, prototype access, and unlisted methods are unavailable. Use tools for external operations. Use await with try/catch.", + "Modules/imports, classes, timers, fetch, eval, prototype access, and unlisted methods are unavailable. Use tools for external operations. Use await with try/catch.", "Prefer explicit `return`; otherwise only the final top-level expression becomes the result.", "Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.", ] diff --git a/packages/codemode/test/codemode.test.ts b/packages/codemode/test/codemode.test.ts index c017a0b9719b..8699daf07da0 100644 --- a/packages/codemode/test/codemode.test.ts +++ b/packages/codemode/test/codemode.test.ts @@ -732,9 +732,10 @@ describe("CodeMode public contract", () => { expect(instructions).toContain("not a general-purpose runtime") expect(instructions).not.toContain("Standard modern JavaScript works") expect(instructions).not.toContain("TypeScript type annotations") - for (const missing of ["Modules/imports", "classes", "generators", "fetch"]) { + for (const missing of ["Modules/imports", "classes", "fetch"]) { expect(instructions).toContain(missing) } + expect(instructions).not.toContain("generators") expect(instructions).not.toContain("new Promise(...) are unavailable") expect(instructions).not.toContain("promise chaining") expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers") diff --git a/packages/codemode/test/generator-test262.test.ts b/packages/codemode/test/generator-test262.test.ts new file mode 100644 index 000000000000..70528f51fcd3 --- /dev/null +++ b/packages/codemode/test/generator-test262.test.ts @@ -0,0 +1,1271 @@ +/* + * Portable portions adapted from Test262 at revision + * 250f204f23a9249ff204be2baec29600faae7b75. Exact source paths are cited + * beside the corresponding tests below. + * + * Copyright (C) 2013-2017 the V8 project authors. All rights reserved. + * Copyright (C) 2018 Valerie Young. All rights reserved. + * Copyright (C) 2020 Alexey Shvayka. All rights reserved. + * Copyright (C) 2022 Kevin Gibbons. All rights reserved. + * Copyright Ecma International. All rights reserved. + * Test262 portions are governed by the BSD license in LICENSE.test262. + */ +import { describe, expect, test } from "bun:test" +import { Effect } from "effect" +import { CodeMode } from "../src/index.js" + +const execute = (code: string) => Effect.runPromise(CodeMode.execute({ code, tools: {} })) + +const value = async (code: string) => { + const result = await execute(code) + if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) + return result.value +} + +describe("confined generators", () => { + // test/built-ins/GeneratorPrototype/next/return-yield-expr.js + test("is lazy and preserves next(value), nested suspension, return, and exhaustion", async () => { + expect( + await value(` + const events = [] + function* generate() { + events.push("start") + const received = yield 1 + (yield 2) + return received + } + const iterator = generate() + const before = events.slice() + const first = iterator.next(99) + const second = iterator.next(3) + const third = iterator.next(7) + const fourth = iterator.next(8) + return [before, events, first, second, third, fourth] + `), + ).toEqual([ + [], + ["start"], + { value: 2, done: false }, + { value: 4, done: false }, + { value: 7, done: true }, + { value: null, done: true }, + ]) + }) + + test("routes throw and return through catch and finally", async () => { + expect( + await value(` + function* generate() { + try { + try { yield "try" } catch (error) { yield "caught " + error } + } finally { + yield "finally" + } + } + const iterator = generate() + return [iterator.next(), iterator.throw("boom"), iterator.return("done"), iterator.next()] + `), + ).toEqual([ + { value: "try", done: false }, + { value: "caught boom", done: false }, + { value: "finally", done: false }, + { value: "done", done: true }, + ]) + }) + + test("throws into a suspended generator and after exhaustion", async () => { + expect( + await value(` + function* generate() { yield 1 } + const iterator = generate() + iterator.next() + let suspended + let exhausted + try { iterator.throw("first") } catch (error) { suspended = error } + try { iterator.throw("second") } catch (error) { exhausted = error } + return [suspended, exhausted, iterator.next()] + `), + ).toEqual(["first", "second", { value: null, done: true }]) + }) + + test("rejects synchronous generator reentry", async () => { + expect( + await value(` + let iterator + function* generate() { + try { iterator.next() } catch (error) { return error.name } + } + iterator = generate() + return iterator.next() + `), + ).toEqual({ value: "TypeError", done: true }) + }) + + test("delegates yield*, forwards next values, and receives the delegate return value", async () => { + expect( + await value(` + function* inner() { + const input = yield 1 + return input * 2 + } + function* outer() { + const result = yield* inner() + return result + 1 + } + const iterator = outer() + return [iterator.next(), iterator.next(4)] + `), + ).toEqual([ + { value: 1, done: false }, + { value: 9, done: true }, + ]) + }) + + test("delegates throw and return to custom iterators", async () => { + expect( + await value(` + const calls = [] + let step = 0 + const delegate = { + [Symbol.iterator]: () => delegate, + next(...args) { + calls.push(["next", args.length, args[0]]) + step += 1 + return step === 1 ? { value: "one", done: false } : { value: "end", done: true } + }, + throw(value) { + calls.push(["throw", value]) + return { value: "recovered", done: false } + }, + return(value) { + calls.push(["return", value]) + return { value: value + "!", done: true } + }, + } + function* generate() { return yield* delegate } + const iterator = generate() + const first = iterator.next() + const second = iterator.throw("x") + const third = iterator.return("stop") + return [first, second, third, calls] + `), + ).toEqual([ + { value: "one", done: false }, + { value: "recovered", done: false }, + { value: "stop!", done: true }, + [ + ["next", 1, null], + ["throw", "x"], + ["return", "stop"], + ], + ]) + }) + + test("uses the missing-throw delegation path for built-in iterables", async () => { + expect( + await value(` + function* generate() { yield* [1, 2] } + const iterator = generate() + iterator.next() + try { iterator.throw("boom") } catch (error) { return error.name } + `), + ).toBe("TypeError") + }) + + test("exposes only the appropriate iterator symbol and works in for...of", async () => { + expect( + await value(` + function* generate() { yield 1; yield 2 } + const iterator = generate() + const symbols = [iterator[Symbol.iterator]() === iterator, iterator[Symbol.asyncIterator]] + const values = [] + for (const item of iterator) values.push(item) + return [symbols, values] + `), + ).toEqual([ + [true, null], + [1, 2], + ]) + }) + + test("accepts generator methods as iterator acquisition results", async () => { + expect( + await value(` + const sync = { + *[Symbol.iterator]() { yield 1; yield 2 }, + } + const asynchronous = { + async *[Symbol.asyncIterator]() { yield 3; yield 4 }, + } + const values = [] + for (const item of sync) values.push(item) + for await (const item of asynchronous) values.push(item) + return values + `), + ).toEqual([1, 2, 3, 4]) + }) + + test("closes a generator when for...of exits abruptly", async () => { + expect( + await value(` + const events = [] + function* generate() { + try { yield 1; yield 2 } finally { events.push("closed") } + } + for (const item of generate()) break + return events + `), + ).toEqual(["closed"]) + }) + + test("async generator requests are promises and execute in request order", async () => { + expect( + await value(` + const events = [] + async function* generate() { + events.push("start") + const input = yield Promise.resolve(1) + events.push("received " + input) + return Promise.resolve(3) + } + const iterator = generate() + const first = iterator.next() + const second = iterator.next(2) + const third = iterator.next(4) + const promiseFlags = [first instanceof Promise, second instanceof Promise, third instanceof Promise] + return [promiseFlags, await Promise.all([first, second, third]), events] + `), + ).toEqual([ + [true, true, true], + [ + { value: 1, done: false }, + { value: 3, done: true }, + { value: null, done: true }, + ], + ["start", "received 2"], + ]) + }) + + test("keeps requests queued while a completed generator adopts return values", async () => { + expect( + await value(` + const events = [] + let resolve + const pending = new Promise((done) => { resolve = done }) + async function* generate() { return 1 } + const iterator = generate() + const first = iterator.next() + const returned = iterator.return(pending) + const later = first.then(() => iterator.next()).then(() => events.push("later")) + returned.then(() => events.push("returned")) + await first + await Promise.resolve() + const before = events.slice() + resolve(9) + await Promise.all([returned, later]) + return [before, events] + `), + ).toEqual([[], ["returned", "later"]]) + }) + + test("serializes requests made after async generator exhaustion", async () => { + expect( + await value(` + const events = [] + let resolve + const pending = new Promise((done) => { resolve = done }) + async function* generate() { return 1 } + const iterator = generate() + await iterator.next() + const returned = iterator.return(pending).then(() => events.push("returned")) + const later = iterator.next().then(() => events.push("later")) + await Promise.resolve() + const before = events.slice() + resolve(9) + await Promise.all([returned, later]) + return [before, events] + `), + ).toEqual([[], ["returned", "later"]]) + }) + + test("async generators adopt yielded, returned, and return-request promises", async () => { + expect( + await value(` + async function* yielded() { yield Promise.resolve(1) } + async function* returned() { return Promise.resolve(2) } + async function* pending() { yield 0 } + const first = yielded() + const second = returned() + const third = pending() + const exhausted = returned() + await third.next() + await exhausted.next() + return await Promise.all([ + first.next(), + second.next(), + third.return(Promise.resolve(3)), + exhausted.return(Promise.resolve(4)), + ]) + `), + ).toEqual([ + { value: 1, done: false }, + { value: 2, done: true }, + { value: 3, done: true }, + { value: 4, done: true }, + ]) + }) + + test("awaits return-request promises before injecting completion", async () => { + expect( + await value(` + const events = [] + async function* generate() { + try { + yield 1 + } catch (error) { + events.push("caught " + error) + yield "recovered" + } finally { + events.push("finally") + } + } + const iterator = generate() + const first = await iterator.next() + const returned = await iterator.return(Promise.reject("bad")) + const beforeNext = events.slice() + const last = await iterator.next() + return [first, returned, beforeNext, last, events] + `), + ).toEqual([ + { value: 1, done: false }, + { value: "recovered", done: false }, + ["caught bad"], + { value: null, done: true }, + ["caught bad", "finally"], + ]) + }) + + test("loop consumers call iterator next with no arguments", async () => { + expect( + await value(` + const calls = [] + let syncStep = 0 + const sync = { + [Symbol.iterator]: () => sync, + next(...args) { + calls.push(["sync", args.length]) + syncStep += 1 + return { value: syncStep, done: syncStep > 1 } + }, + } + let asyncStep = 0 + const asynchronous = { + [Symbol.asyncIterator]: () => asynchronous, + async next(...args) { + calls.push(["async", args.length]) + asyncStep += 1 + return { value: asyncStep, done: asyncStep > 1 } + }, + } + for (const item of sync) {} + for await (const item of asynchronous) {} + return calls + `), + ).toEqual([ + ["sync", 0], + ["sync", 0], + ["async", 0], + ["async", 0], + ]) + }) + + test("supports async generators in for await...of and keeps them out of for...of", async () => { + expect( + await value(` + async function* generate() { yield 1; yield await Promise.resolve(2) } + const values = [] + for await (const item of generate()) values.push(item) + let name + try { for (const item of generate()) {} } catch (error) { name = error.name } + return [values, name] + `), + ).toEqual([[1, 2], "TypeError"]) + }) + + test("keeps generator references opaque at the data boundary", async () => { + const result = await execute(`function* generate() { yield 1 } return generate()`) + expect(result.ok).toBe(false) + if (result.ok) return + expect(result.error.kind).toBe("InvalidDataValue") + }) + + // test/built-ins/GeneratorPrototype/return/from-state-suspended-start.js + // test/built-ins/GeneratorPrototype/throw/from-state-suspended-start.js + // test/built-ins/GeneratorPrototype/return/from-state-completed.js + // test/built-ins/GeneratorPrototype/throw/from-state-completed.js + test("honors sync return and throw in suspended-start and completed states", async () => { + expect( + await value(` + const events = [] + function* generate() { events.push("body"); yield 1 } + + const returned = generate() + const startReturn = returned.return(7) + const afterReturn = returned.next() + + const thrown = generate() + let startThrow + try { thrown.throw("start") } catch (error) { startThrow = error } + const afterThrow = thrown.next() + let completedThrow + try { thrown.throw("completed") } catch (error) { completedThrow = error } + + return [startReturn, afterReturn, startThrow, afterThrow, completedThrow, events] + `), + ).toEqual([ + { value: 7, done: true }, + { value: null, done: true }, + "start", + { value: null, done: true }, + "completed", + [], + ]) + }) + + // test/built-ins/AsyncGeneratorPrototype/return/return-suspendedStart-promise.js + // test/built-ins/AsyncGeneratorPrototype/throw/throw-suspendedStart.js + // test/built-ins/AsyncGeneratorPrototype/return/return-state-completed.js + // test/built-ins/AsyncGeneratorPrototype/throw/throw-state-completed.js + test("honors async return and throw in suspended-start and completed states", async () => { + expect( + await value(` + const events = [] + async function* generate() { events.push("body"); yield 1 } + + const returned = generate() + const startReturn = await returned.return(Promise.resolve(7)) + const afterReturn = await returned.next() + + const thrown = generate() + let startThrow + try { await thrown.throw("start") } catch (error) { startThrow = error } + const afterThrow = await thrown.next() + let completedThrow + try { await thrown.throw("completed") } catch (error) { completedThrow = error } + + return [startReturn, afterReturn, startThrow, afterThrow, completedThrow, events] + `), + ).toEqual([ + { value: 7, done: true }, + { value: null, done: true }, + "start", + { value: null, done: true }, + "completed", + [], + ]) + }) + + // test/built-ins/AsyncGeneratorPrototype/return/return-suspendedYield-try-finally.js + // test/built-ins/AsyncGeneratorPrototype/return/return-suspendedYield-try-finally-return.js + // test/built-ins/AsyncGeneratorPrototype/throw/throw-suspendedYield-try-finally-throw.js + test("runs finally yields and lets finally completions override requests", async () => { + expect( + await value(` + async function* yielding() { + try { yield 1 } finally { yield 2 } + } + async function* returning() { + try { yield 1 } finally { return "override" } + } + async function* throwing() { + try { yield 1 } finally { throw "override" } + } + + const first = yielding() + await first.next() + const finallyYield = await first.return("sent") + const preservedReturn = await first.next() + + const second = returning() + await second.next() + const overriddenReturn = await second.return("sent") + + const third = throwing() + await third.next() + let overriddenThrow + try { await third.throw("sent") } catch (error) { overriddenThrow = error } + return [finallyYield, preservedReturn, overriddenReturn, overriddenThrow] + `), + ).toEqual([{ value: 2, done: false }, { value: "sent", done: true }, { value: "override", done: true }, "override"]) + }) + + // test/language/statements/async-generator/yield-promise-reject-next-catch.js + // test/language/statements/async-generator/yield-promise-reject-next-yield-star-sync-iterator.js + test("rejects yielded promises and closes direct and sync-delegating async generators", async () => { + expect( + await value(` + async function* direct() { yield Promise.reject("direct") } + async function* delegated() { yield* [Promise.reject("delegated"), "unreachable"] } + const results = [] + for (const iterator of [direct(), delegated()]) { + try { await iterator.next() } catch (error) { results.push(error) } + results.push(await iterator.next()) + } + return results + `), + ).toEqual(["direct", { value: null, done: true }, "delegated", { value: null, done: true }]) + }) + + // test/built-ins/AsyncFromSyncIteratorPrototype/next/for-await-iterator-next-rejected-promise-close.js + // test/built-ins/AsyncFromSyncIteratorPrototype/next/yield-iterator-next-rejected-promise-close.js + // test/built-ins/AsyncFromSyncIteratorPrototype/throw/iterator-result-rejected-promise-close.js + test("closes sync iterators when async-from-sync values reject", async () => { + expect( + await value(` + const events = [] + function* loopSource() { + try { yield Promise.reject("loop") } finally { events.push("loop close") } + } + let loopError + try { for await (const item of loopSource()) {} } catch (error) { loopError = error } + + function* yieldSource() { + try { yield Promise.reject("yield") } finally { events.push("yield close") } + } + async function* delegate() { yield* yieldSource() } + let yieldError + try { await delegate().next() } catch (error) { yieldError = error } + + const throwing = { + [Symbol.iterator]: () => throwing, + next: () => ({ value: 1, done: false }), + throw: () => ({ value: Promise.reject("throw"), done: false }), + return: () => { events.push("throw close"); return {} }, + } + async function* throwDelegate() { yield* throwing } + const iterator = throwDelegate() + await iterator.next() + let throwError + try { await iterator.throw("sent") } catch (error) { throwError = error } + return [loopError, yieldError, throwError, events] + `), + ).toEqual(["loop", "yield", "throw", ["loop close", "yield close", "throw close"]]) + }) + + test("does not close rejected terminal or delegated return values", async () => { + expect( + await value(` + const events = [] + const terminal = { + [Symbol.iterator]: () => terminal, + next: () => ({ value: Promise.reject("terminal"), done: true }), + return: () => { events.push("terminal close"); return {} }, + } + let terminalError + try { for await (const item of terminal) {} } catch (error) { terminalError = error } + + let returnCount = 0 + const returned = { + [Symbol.iterator]: () => returned, + next: () => ({ value: 1, done: false }), + return: () => { + returnCount += 1 + return { value: Promise.reject("return"), done: false } + }, + } + async function* delegate() { yield* returned } + const iterator = delegate() + await iterator.next() + let returnError + try { await iterator.return("sent") } catch (error) { returnError = error } + return [terminalError, returnError, returnCount, events] + `), + ).toEqual(["terminal", "return", 1, []]) + }) + + test("serializes a mixed async next, throw, return, and next request queue", async () => { + expect( + await value(` + async function* generate() { + try { + try { yield 1; yield 2 } catch (error) { yield "caught " + error } + } finally { + yield "finally" + } + } + const iterator = generate() + const first = iterator.next() + const thrown = iterator.throw("x") + const returned = iterator.return("done") + const last = iterator.next() + return await Promise.all([first, thrown, returned, last]) + `), + ).toEqual([ + { value: 1, done: false }, + { value: "caught x", done: false }, + { value: "finally", done: false }, + { value: "done", done: true }, + ]) + }) + + // test/built-ins/AsyncGeneratorPrototype/next/request-queue-order.js + // test/built-ins/AsyncGeneratorPrototype/throw/request-queue-order-state-executing.js + // test/built-ins/AsyncGeneratorPrototype/return/request-queue-order-state-executing.js + test("orders async requests enqueued while generators are executing", async () => { + expect( + await value(` + const events = [] + let returned, returnRequest + async function* returnWhileExecuting() { + returnRequest = returned.return(42).then((result) => events.push(["return", result])) + yield 1 + } + returned = returnWhileExecuting() + const firstReturn = returned.next().then((result) => events.push(["first return", result])) + await Promise.all([firstReturn, returnRequest]) + + let thrown, throwRequest + async function* throwWhileExecuting() { + throwRequest = thrown.throw("boom").catch((error) => events.push(["throw", error])) + yield 2 + } + thrown = throwWhileExecuting() + const firstThrow = thrown.next().then((result) => events.push(["first throw", result])) + await Promise.all([firstThrow, throwRequest]) + + async function* queued() { yield "first"; yield "second" } + const iterator = queued() + const first = iterator.next() + const second = iterator.next() + const third = iterator.next() + await Promise.all([ + third.then(() => events.push("third")), + second.then(() => events.push("second")), + first.then(() => events.push("first")), + ]) + return events + `), + ).toEqual([ + ["first return", { value: 1, done: false }], + ["return", { value: 42, done: true }], + ["first throw", { value: 2, done: false }], + ["throw", "boom"], + "first", + "second", + "third", + ]) + }) + + // test/language/statements/async-generator/yield-star-promise-not-unwrapped.js + // test/language/statements/async-generator/yield-star-sync-next.js + test("preserves async iterator promise values but unwraps async-from-sync values", async () => { + expect( + await value(` + const asyncValue = Promise.resolve("async") + const asynchronous = { + [Symbol.asyncIterator]: () => asynchronous, + next: () => ({ value: asyncValue, done: false }), + } + const syncValue = Promise.resolve("sync") + const synchronous = { + [Symbol.iterator]: () => synchronous, + next: () => ({ value: syncValue, done: false }), + } + async function* delegate(value) { yield* value } + const asyncResult = await delegate(asynchronous).next() + const syncResult = await delegate(synchronous).next() + return [asyncResult.value === asyncValue, await asyncResult.value, syncResult] + `), + ).toEqual([true, "async", { value: "sync", done: false }]) + }) + + test("captures delegated result done before awaiting async-from-sync values", async () => { + expect( + await value(` + const result = { value: Promise.resolve(1), done: false } + result.value.then(() => { result.done = true }) + const source = { + [Symbol.iterator]: () => source, + next: () => result, + } + async function* delegate() { yield* source } + return await delegate().next() + `), + ).toEqual({ value: 1, done: false }) + }) + + // test/language/statements/async-generator/yield-star-async-next.js + // test/language/statements/async-generator/yield-star-sync-return.js + // test/language/statements/async-generator/yield-star-async-throw.js + test("forwards next, return, and throw through sync and async yield delegates", async () => { + expect( + await value(` + const calls = [] + const make = (symbol, label) => { + let step = 0 + const iterator = { + [symbol]: () => iterator, + next(...args) { + calls.push([label, "next", args.length, args[0]]) + step += 1 + return { value: step, done: false } + }, + return(value) { + calls.push([label, "return", value]) + return { value: value + "!", done: true } + }, + throw(value) { + calls.push([label, "throw", value]) + return { value: "caught " + value, done: false } + }, + } + return iterator + } + async function* delegate(iterator) { return yield* iterator } + const sync = delegate(make(Symbol.iterator, "sync")) + const asynchronous = delegate(make(Symbol.asyncIterator, "async")) + const results = [await sync.next(9), await sync.next(2), await sync.throw("x"), await sync.return("stop")] + results.push(await asynchronous.next(9), await asynchronous.next(3), await asynchronous.throw("y")) + return [results, calls] + `), + ).toEqual([ + [ + { value: 1, done: false }, + { value: 2, done: false }, + { value: "caught x", done: false }, + { value: "stop!", done: true }, + { value: 1, done: false }, + { value: 2, done: false }, + { value: "caught y", done: false }, + ], + [ + ["sync", "next", 1, null], + ["sync", "next", 1, 2], + ["sync", "throw", "x"], + ["sync", "return", "stop"], + ["async", "next", 1, null], + ["async", "next", 1, 3], + ["async", "throw", "y"], + ], + ]) + }) + + // test/language/statements/async-generator/yield-star-sync-return.js + // test/language/statements/async-generator/yield-star-async-throw.js + test("continues delegation when return and throw report done false", async () => { + expect( + await value(` + let returns = 0 + const synchronous = { + [Symbol.iterator]: () => synchronous, + next: () => ({ value: "next", done: false }), + return(value) { + returns += 1 + return { value: returns === 1 ? "return pending" : value, done: returns > 1 } + }, + } + let throws = 0 + const asynchronous = { + [Symbol.asyncIterator]: () => asynchronous, + next: () => ({ value: "next", done: false }), + throw(value) { + throws += 1 + return { value: throws === 1 ? "throw pending" : value, done: throws > 1 } + }, + } + async function* delegate(iterator) { return yield* iterator } + const returned = delegate(synchronous) + const thrown = delegate(asynchronous) + return [ + await returned.next(), + await returned.return("first"), + await returned.return("second"), + await thrown.next(), + await thrown.throw("first"), + await thrown.throw("second"), + ] + `), + ).toEqual([ + { value: "next", done: false }, + { value: "return pending", done: false }, + { value: "second", done: true }, + { value: "next", done: false }, + { value: "throw pending", done: false }, + { value: "second", done: true }, + ]) + }) + + // test/language/expressions/yield/star-rhs-iter-nrml-next-call-non-obj.js + // test/language/expressions/yield/star-rhs-iter-thrw-thrw-call-non-obj.js + // test/language/expressions/yield/star-rhs-iter-rtrn-rtrn-call-non-obj.js + // test/language/statements/async-generator/yield-star-next-not-callable-number-throw.js + test("rejects malformed yield delegate methods and iterator results", async () => { + expect( + await value(` + const run = (iterator, operation) => { + function* generate() { + try { yield* iterator } catch (error) { return error.name } + } + const value = generate() + const first = value.next() + return operation === "next" ? first : value[operation]() + } + const iterable = (fields) => ({ [Symbol.iterator]: () => fields }) + const nextResult = run(iterable({ next: () => 1 }), "next") + const throwResult = run(iterable({ next: () => ({ done: false }), throw: () => 1 }), "throw") + const returnResult = run(iterable({ next: () => ({ done: false }), return: () => 1 }), "return") + + const badAsync = { + [Symbol.asyncIterator]: () => ({ next: 1 }), + } + async function* asynchronous() { + try { yield* badAsync } catch (error) { return error.name } + } + return [nextResult, throwResult, returnResult, await asynchronous().next()] + `), + ).toEqual([ + { value: "TypeError", done: true }, + { value: "TypeError", done: true }, + { value: "TypeError", done: true }, + { value: "TypeError", done: true }, + ]) + }) + + // test/language/expressions/yield/captured-free-vars.js + // test/language/statements/generators/dflt-params-ref-prior.js + // test/language/expressions/generators/dflt-params-ref-prior.js + // test/language/expressions/object/method-definition/generator-no-yield.js + test("supports declaration, expression, and method forms with closures and parameters", async () => { + expect( + await value(` + const captured = 4 + function* declaration(x, y = x, ...rest) { yield captured + y + rest[0] } + const expression = function* (x, y = x) { yield captured + y } + const object = { *method({ value }, extra = 1) { return captured + value + extra } } + return [declaration(2, undefined, 3).next(), expression(5).next(), object.method({ value: 6 }).next()] + `), + ).toEqual([ + { value: 9, done: false }, + { value: 9, done: false }, + { value: 11, done: true }, + ]) + }) + + // test/language/expressions/assignment/dstr/array-elem-iter-nrml-close.js + test("steps holes and rest and closes array binding and assignment patterns early", async () => { + expect( + await value(` + const events = [] + function* binding() { + try { events.push("b1"); yield 1; events.push("b2"); yield 2; events.push("b3"); yield 3; yield 4 } + finally { events.push("binding close") } + } + const [first, , third] = binding() + function* assignment() { + try { yield 5; yield 6; yield 7 } + finally { events.push("assignment close") } + } + let head, rest + ;[head, ...rest] = assignment() + return [first, third, head, rest, events] + `), + ).toEqual([1, 3, 5, [6, 7], ["b1", "b2", "b3", "binding close", "assignment close"]]) + }) + + // test/language/statements/variable/dstr/ary-ptrn-elem-id-init-throws.js + // test/language/expressions/assignment/dstr/array-elem-iter-thrw-close-err.js + test("closes on destructuring defaults and preserves the binding error over return failure", async () => { + expect( + await value(` + const events = [] + const iterator = { + [Symbol.iterator]: () => iterator, + next: () => ({ value: undefined, done: false }), + return: () => { events.push("close"); throw "close error" }, + } + let caught + try { + const [value = (() => { throw "binding error" })()] = iterator + } catch (error) { caught = error } + return [caught, events] + `), + ).toEqual(["binding error", ["close"]]) + }) + + test("does not close an exhausted iterator when a destructuring default fails", async () => { + expect( + await value(` + const events = [] + const iterator = { + [Symbol.iterator]: () => iterator, + next: () => ({ done: true }), + return: () => { events.push("close"); return {} }, + } + try { const [item = (() => { throw "default" })()] = iterator } catch {} + return events + `), + ).toEqual([]) + }) + + test("consumes generators in array and argument spread without awaiting yielded promises", async () => { + expect( + await value(` + function* values() { yield 1; yield Promise.resolve(2); yield 3 } + const array = [...values()] + const args = ((...items) => items)(...values()) + return [array[0], array[1] instanceof Promise, await array[1], args[2]] + `), + ).toEqual([1, true, 2, 3]) + }) + + test("constructs Map, Set, and URLSearchParams from generators lazily", async () => { + expect( + await value(` + const events = [] + function* pairs() { events.push(1); yield ["a", 1]; events.push(2); yield ["b", 2] } + function* values() { yield 1; yield 1; yield 2 } + const map = new Map(pairs()) + const set = new Set(values()) + const params = new URLSearchParams(pairs()) + return [map.get("b"), [...set], params.toString(), events] + `), + ).toEqual([2, [1, 2], "a=1&b=2", [1, 2, 1, 2]]) + }) + + test("keeps built-in collection iteration live during callbacks", async () => { + expect( + await value(` + const map = new Map([[1, 1], [2, 2]]) + const mapped = Array.from(map, (entry, index) => { + if (index === 0) map.set(3, 3) + return entry[0] + }) + const set = new Set([1, 2]) + const grouped = Map.groupBy(set, (item, index) => { + if (index === 0) set.add(3) + return "items" + }) + return [mapped, grouped.get("items")] + `), + ).toEqual([ + [1, 2, 3], + [1, 2, 3], + ]) + }) + + test("keeps built-in collection iteration live in loops and yield delegation", async () => { + expect( + await value(` + const map = new Map([[1, 1], [2, 2]]) + const mapValues = [] + for (const [key] of map) { + mapValues.push(key) + if (key === 1) map.set(3, 3) + } + const set = new Set([1, 2]) + const setValues = [] + for await (const item of set) { + setValues.push(item) + if (item === 1) set.add(3) + } + const params = new URLSearchParams("a=1&b=2") + function* delegate() { yield* params } + const iterator = delegate() + const first = iterator.next() + params.append("c", "3") + return [mapValues, setValues, first, iterator.next(), iterator.next()] + `), + ).toEqual([ + [1, 2, 3], + [1, 2, 3], + { value: ["a", "1"], done: false }, + { value: ["b", "2"], done: false }, + { value: ["c", "3"], done: false }, + ]) + }) + + test("preserves async-from-sync turns for built-in loop completion and close", async () => { + expect( + await value(` + const completed = [] + Promise.resolve().then(() => completed.push("reaction")) + for await (const item of []) {} + completed.push("after") + + const closed = [] + for await (const item of [1]) { + Promise.resolve().then(() => closed.push("reaction")) + break + } + closed.push("after") + return [completed, closed] + `), + ).toEqual([ + ["reaction", "after"], + ["reaction", "after"], + ]) + }) + + test("reads iterator return only when closing", async () => { + expect( + await value(` + const events = [] + const iterator = { + [Symbol.iterator]: () => iterator, + next() { + iterator.return = () => { events.push("new"); return {} } + return { value: 1, done: false } + }, + return: () => { events.push("old"); return {} }, + } + const [item] = iterator + return [item, events] + `), + ).toEqual([1, ["new"]]) + }) + + test("accepts iterable URLSearchParams entry pairs", async () => { + expect( + await value(` + function* pair() { yield "a"; yield 1 } + function* entries() { yield pair() } + return new URLSearchParams(entries()).toString() + `), + ).toBe("a=1") + }) + + test("converts URLSearchParams pair elements before requesting the next", async () => { + expect( + await value(` + const events = [] + function* pair() { + try { + events.push("first") + yield (function* () {})() + events.push("second") + yield 2 + } finally { events.push("pair close") } + } + function* entries() { + try { yield pair() } finally { events.push("outer close") } + } + let name + try { new URLSearchParams(entries()) } catch (error) { name = error.name } + return [events, name] + `), + ).toEqual([["first", "pair close", "outer close"], "Error"]) + }) + + test("validates URLSearchParams pair lengths after converting the outer sequence", async () => { + expect( + await value(` + const events = [] + function* entries() { + try { + events.push("one") + yield ["a"] + events.push("two") + yield ["b", 2] + } finally { events.push("outer close") } + } + let name + try { new URLSearchParams(entries()) } catch (error) { name = error.name } + return [events, name] + `), + ).toEqual([["one", "two", "outer close"], "TypeError"]) + }) + + test("closes entry constructors when a generator yields a malformed entry", async () => { + expect( + await value(` + const events = [] + function* mapEntries() { try { yield ["a", 1]; yield null } finally { events.push("map close") } } + function* parameterEntries() { try { yield ["a"]; yield ["b", 2] } finally { events.push("params close") } } + const names = [] + try { new Map(mapEntries()) } catch (error) { names.push(error.name) } + try { new URLSearchParams(parameterEntries()) } catch (error) { names.push(error.name) } + return [names, events] + `), + ).toEqual([ + ["TypeError", "TypeError"], + ["map close", "params close"], + ]) + }) + + // test/built-ins/Array/from/iter-map-fn-args.js + // test/built-ins/Array/from/iter-map-fn-err.js + test("interleaves Array.from mapping and closes when its mapper fails", async () => { + expect( + await value(` + const events = [] + function* source() { + try { events.push("next 1"); yield 1; events.push("next 2"); yield 2 } + finally { events.push("close") } + } + const mapped = Array.from(source(), (item) => { events.push("map " + item); return item * 2 }) + let caught + try { Array.from(source(), (item) => { throw "mapper " + item }) } catch (error) { caught = error } + return [mapped, caught, events] + `), + ).toEqual([[2, 4], "mapper 1", ["next 1", "map 1", "next 2", "map 2", "close", "next 1", "close"]]) + }) + + test("reads array-like Array.from values immediately before mapping", async () => { + expect( + await value(` + const source = { 0: 1, 1: 2, length: 2 } + return Array.from(source, (item, index) => { + if (index === 0) source[1] = 9 + return item + }) + `), + ).toEqual([1, 9]) + }) + + test("interleaves Object.groupBy and Map.groupBy callbacks and closes on callback failure", async () => { + expect( + await value(` + const events = [] + function* source() { try { events.push("next"); yield 1; events.push("next"); yield 2 } finally { events.push("close") } } + const object = Object.groupBy(source(), (item) => { events.push("object " + item); return item % 2 }) + const map = Map.groupBy(source(), (item) => { events.push("map " + item); return item % 2 }) + let caught + try { Object.groupBy(source(), () => { throw "callback" }) } catch (error) { caught = error } + return [object, Object.fromEntries(map), caught, events] + `), + ).toEqual([ + { 0: [2], 1: [1] }, + { 0: [2], 1: [1] }, + "callback", + ["next", "object 1", "next", "object 2", "close", "next", "map 1", "next", "map 2", "close", "next", "close"], + ]) + }) + + test("consumes Promise combinator generators in order and observes rejections", async () => { + expect( + await value(` + function* items() { yield Promise.resolve(1); yield Promise.reject("bad"); yield 3 } + const settled = await Promise.allSettled(items()) + let all, any + try { await Promise.all(items()) } catch (error) { all = error } + try { await Promise.any((function* () { yield Promise.reject("a"); yield Promise.reject("b") })()) } + catch (error) { any = error.errors } + const race = await Promise.race((function* () { yield 4; yield Promise.resolve(5) })()) + return [settled, all, any, race] + `), + ).toEqual([ + [ + { status: "fulfilled", value: 1 }, + { status: "rejected", reason: "bad" }, + { status: "fulfilled", value: 3 }, + ], + "bad", + ["a", "b"], + 4, + ]) + }) + + test("finishes Promise combinator iterator consumption before returning the promise", async () => { + expect( + await value(` + const events = [] + function* items() { events.push("first"); yield 1; events.push("second"); yield 2 } + const promise = Promise.all(items()) + events.push("after call") + await promise + return events + `), + ).toEqual(["first", "second", "after call"]) + }) + + // test/built-ins/Object/fromEntries/iterator-closed-for-null-entry.js + test("closes Object.fromEntries on malformed entries and consumes valid generators", async () => { + expect( + await value(` + const events = [] + function* valid() { yield ["a", 1]; yield ["b", 2] } + function* invalid() { try { yield ["a", 1]; yield null; yield ["c", 3] } finally { events.push("close") } } + let name + try { Object.fromEntries(invalid()) } catch (error) { name = error.name } + return [Object.fromEntries(valid()), name, events] + `), + ).toEqual([{ a: 1, b: 2 }, "TypeError", ["close"]]) + }) + + test("consumes AggregateError and Math.sumPrecise generators and closes on invalid numbers", async () => { + expect( + await value(` + const events = [] + function* errors() { yield "a"; yield "b" } + function* numbers() { yield 1e30; yield 0.1; yield -1e30 } + function* invalid() { try { yield 1; yield "bad"; yield 2 } finally { events.push("close") } } + const aggregate = new AggregateError(errors(), "message") + let name + try { Math.sumPrecise(invalid()) } catch (error) { name = error.name } + return [aggregate.errors, aggregate.message, Math.sumPrecise(numbers()), name, events] + `), + ).toEqual([["a", "b"], "message", 0.1, "TypeError", ["close"]]) + }) + + test("rejects async generators in every synchronous iterable consumer", async () => { + expect( + await value(` + async function* source() { yield ["a", 1] } + const checks = [ + () => [...source()], + () => ((...items) => items)(...source()), + () => { const [item] = source(); return item }, + () => Array.from(source()), + () => new Map(source()), + () => new Set(source()), + () => new URLSearchParams(source()), + () => Object.fromEntries(source()), + () => Object.groupBy(source(), (item) => item), + () => Math.sumPrecise(source()), + () => new AggregateError(source()), + ] + const names = [] + for (const check of checks) { + try { check() } catch (error) { names.push(error.name) } + } + try { await Promise.all(source()) } catch (error) { names.push(error.name) } + return names + `), + ).toEqual(Array(12).fill("TypeError")) + }) + + // test/built-ins/Array/from/iter-get-iter-err.js + // test/built-ins/Array/from/iter-adv-err.js + test("does not close when iterator acquisition or next-result validation fails", async () => { + expect( + await value(` + const events = [] + const acquisition = { [Symbol.iterator]: () => { events.push("acquire"); throw "acquisition" } } + const malformed = { + [Symbol.iterator]: () => malformed, + next: () => { events.push("next"); return 1 }, + return: () => { events.push("close"); return {} }, + } + for (const source of [acquisition, malformed]) { + try { Array.from(source) } catch {} + } + return events + `), + ).toEqual(["acquire", "next"]) + }) + + test("reports synchronous iterator failures before queued promise reactions", async () => { + expect( + await value(` + const events = [] + Promise.resolve().then(() => events.push("reaction")) + const iterator = { + [Symbol.iterator]: () => iterator, + next: () => { throw "next" }, + } + try { Array.from(iterator) } catch { events.push("catch") } + await Promise.resolve() + return events + `), + ).toEqual(["catch", "reaction"]) + }) +}) From 80dc21d8f7ef9b2c4d4387d2a09f1d8c5ebe6b3e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 02:34:48 +0000 Subject: [PATCH 005/150] fix(cli): use hosted updater API (#38223) Co-authored-by: Dax Raad --- packages/cli/src/services/updater.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/services/updater.ts b/packages/cli/src/services/updater.ts index 7f8c9fa94618..08b5cd3b7d2c 100644 --- a/packages/cli/src/services/updater.ts +++ b/packages/cli/src/services/updater.ts @@ -95,7 +95,7 @@ export const layer = Layer.effect( const response = yield* Effect.tryPromise({ try: () => fetch( - `https://registry.npmjs.org/${encodeURIComponent(packageName)}/${encodeURIComponent(OPENCODE_CHANNEL)}`, + `https://update.opencode.ai/api/${encodeURIComponent(channel)}/cli/npm`, { headers: { "User-Agent": `opencode/${OPENCODE_VERSION}` }, signal: AbortSignal.timeout(10_000) }, ), catch: (cause) => new Error("Failed to check for updates", { cause }), From 6b9136e797ce35620dc49588a633e18be4353d7f Mon Sep 17 00:00:00 2001 From: James Long Date: Tue, 21 Jul 2026 22:45:21 -0400 Subject: [PATCH 006/150] fix(tui): inherit elevated tool theme (#38224) --- packages/tui/src/context/theme.tsx | 16 ++++++++++++++-- packages/tui/src/routes/session/index.tsx | 21 ++++++++++++++++----- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/packages/tui/src/context/theme.tsx b/packages/tui/src/context/theme.tsx index 13caa748c9e2..a29c25501cf0 100644 --- a/packages/tui/src/context/theme.tsx +++ b/packages/tui/src/context/theme.tsx @@ -22,7 +22,7 @@ import { createComponentTheme, type ComponentTheme } from "../theme/v2/component import { resolveThemeFile } from "../theme/v2/resolve" import { migrateV1 } from "../theme/v2/v1-migrate" import { themeModes } from "../theme/v2/select" -import { createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js" +import { createEffect, createMemo, onCleanup, onMount, type Accessor, type ParentProps } from "solid-js" import { createStore, produce } from "solid-js/store" import { createSimpleContext } from "./helper" import { useConfig } from "../config" @@ -99,7 +99,7 @@ const [store, setStore] = createStore({ subscribeThemes((themes) => setStore("themes", themes)) -export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ +const themeContext = createSimpleContext({ name: "Theme", init: (props: { mode: "dark" | "light"; source?: ThemeSource }) => { const renderer = useRenderer() @@ -363,6 +363,18 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ }, }) +export const useTheme = themeContext.use +export const ThemeProvider = themeContext.provider + +export function ThemeContextProvider(props: ParentProps<{ context: ContextName }>) { + const theme = useTheme() + return ( + + {props.children} + + ) +} + function duration(milliseconds: number) { return `${milliseconds.toFixed(2)} ms` } diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index d2f3543c0ed8..acf38c353c3a 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -22,7 +22,7 @@ import { useData } from "../../context/data" import { SplitBorder } from "../../ui/border" import { useTuiPaths, useTuiTerminalEnvironment } from "../../context/runtime" import { Spinner, SPINNER_FRAMES } from "../../component/spinner" -import { useTheme } from "../../context/theme" +import { ThemeContextProvider, useTheme } from "../../context/theme" import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core" import { Prompt, type PromptRef } from "../../component/prompt" import type { @@ -2352,15 +2352,26 @@ function StatusBadge(props: { children: string }) { ) } -function BlockTool(props: { +type BlockToolProps = { title?: string path?: { label: string; value: string } children?: JSX.Element onClick?: () => void part?: SessionMessageAssistantTool spinner?: boolean -}) { - const { themeV2 } = useTheme().contextual("elevated") +} + +function BlockTool(props: BlockToolProps) { + const parentTheme = useTheme() + return ( + + + + ) +} + +function BlockToolContent(props: BlockToolProps & { borderColor: RGBA }) { + const { themeV2 } = useTheme() const ctx = use() const data = useData() const renderer = useRenderer() @@ -2380,7 +2391,7 @@ function BlockTool(props: { gap={1} backgroundColor={hover() ? themeV2.raise(themeV2.background.default) : themeV2.background.default} customBorderChars={SplitBorder.customBorderChars} - borderColor={themeV2.background.default} + borderColor={props.borderColor} onMouseOver={() => props.onClick && setHover(true)} onMouseOut={() => setHover(false)} onMouseUp={() => { From 59ad593d9c7502bffdbd4579cc11b5aaa6e59e72 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:08:24 -0500 Subject: [PATCH 007/150] fix(core): reduce snapshot repository setup (#38162) Co-authored-by: Aiden Cline Co-authored-by: Aiden Cline --- packages/core/src/git.ts | 50 +++++++++++++++++++++++++--------- packages/core/test/git.test.ts | 15 +++++++++- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/packages/core/src/git.ts b/packages/core/src/git.ts index 989e15ae3b36..8802ca7317a9 100644 --- a/packages/core/src/git.ts +++ b/packages/core/src/git.ts @@ -17,6 +17,25 @@ export class Repository extends Schema.Class("Git.Repository")({ commonDirectory: AbsolutePath, }) {} +// Included from $GIT_DIR/config via include.path (git >= 1.7.10); OpenCode owns +// this file entirely, so updates are plain rewrites with no config parsing. +const snapshotConfigFile = "opencode.gitconfig" +const snapshotConfigInclude = `[include] + path = ${snapshotConfigFile} +` +const snapshotConfig = `[core] + autocrlf = false + longpaths = true + symlinks = true + fsmonitor = false + untrackedCache = true +[feature] + manyFiles = true +[index] + version = 4 + threads = true +` + export const ChangeSet = Schema.String.pipe(Schema.brand("Git.ChangeSet")) export type ChangeSet = typeof ChangeSet.Type @@ -376,19 +395,24 @@ const layer = Layer.effect( commonDirectory: input.gitDirectory, }) yield* repositoryOperation("create", repository, ["init"]) - yield* Effect.forEach( - [ - ["core.autocrlf", "false"], - ["core.longpaths", "true"], - ["core.symlinks", "true"], - ["core.fsmonitor", "false"], - ["feature.manyFiles", "true"], - ["index.version", "4"], - ["index.threads", "true"], - ["core.untrackedCache", "true"], - ], - ([key, value]) => repositoryOperation("create", repository, ["config", key, value]), - { discard: true }, + yield* Effect.gen(function* () { + yield* fs.writeFileString(path.join(input.gitDirectory, snapshotConfigFile), snapshotConfig) + const config = path.join(input.gitDirectory, "config") + const current = yield* fs.readFileString(config) + if (current.includes(snapshotConfigInclude)) return + yield* fs.writeFileString(config, `${current.endsWith("\n") ? "\n" : "\n\n"}${snapshotConfigInclude}`, { + flag: "a", + }) + }).pipe( + Effect.mapError( + (cause) => + new OperationError({ + operation: "create", + directory: input.gitDirectory, + message: "Failed to configure Git storage", + cause, + }), + ), ) if (!input.seed) return repository yield* fs.ensureDir(path.join(input.gitDirectory, "objects", "info")).pipe( diff --git a/packages/core/test/git.test.ts b/packages/core/test/git.test.ts index abcc60181f43..055a6b764076 100644 --- a/packages/core/test/git.test.ts +++ b/packages/core/test/git.test.ts @@ -148,8 +148,21 @@ describe("Git trees", () => { const git = yield* Git.Service const source = yield* git.repo.discover(AbsolutePath.make(root.path)) if (!source) throw new Error("Repository not found") - const storage = AbsolutePath.make(path.join(root.path, ".snapshot")) + const storage = AbsolutePath.make(path.join(root.path, ".snapshot storage")) const repository = yield* git.repo.create({ worktree: source.worktree, gitDirectory: storage, seed: source }) + yield* Effect.promise(() => $`git --git-dir ${storage} config --add include.path first.gitconfig`.quiet()) + yield* Effect.promise(() => $`git --git-dir ${storage} config --add include.path second.gitconfig`.quiet()) + yield* Effect.promise(() => $`git --git-dir ${storage} config core.autocrlf true`.quiet()) + yield* git.repo.create({ worktree: source.worktree, gitDirectory: storage, seed: source }) + expect( + yield* Effect.promise(() => $`git --git-dir ${storage} config --local --includes core.autocrlf`.text()), + ).toBe("false\n") + expect( + (yield* Effect.promise(() => fs.readFile(path.join(storage, "config"), "utf8"))).match(/opencode\.gitconfig/g), + ).toHaveLength(1) + expect( + yield* Effect.promise(() => $`git --git-dir ${storage} config --local --get-all include.path`.text()), + ).toBe("opencode.gitconfig\nfirst.gitconfig\nsecond.gitconfig\n") yield* git.index.refresh({ repository, scope: RelativePath.make("scope") }) const before = yield* git.tree.write(repository) From 69c05ae3fc4ae761e5a1b2983e6cbdf906007da9 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:32:02 -0500 Subject: [PATCH 008/150] feat(codemode): assimilate promise thenables (#38237) --- packages/codemode/interpreter-support.md | 20 ++-- packages/codemode/src/interpreter/promises.ts | 109 ++++++++++++------ packages/codemode/src/interpreter/runtime.ts | 30 +++-- .../codemode/test/promise-test262.test.ts | 96 ++++++++++++++- 4 files changed, 189 insertions(+), 66 deletions(-) diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index 7c2a9596652f..0d989595ec82 100644 --- a/packages/codemode/interpreter-support.md +++ b/packages/codemode/interpreter-support.md @@ -136,8 +136,8 @@ ultimate source of truth. - [x] Optional property access and optional calls. - [x] Function/tool calls and spread arguments. - [x] Sequence expressions (the comma operator). -- [x] `await` for CodeMode promises; a plain value passes through unchanged, though every `await` still defers its - continuation one reaction turn. +- [x] `await` for CodeMode promises and callable thenables; a plain value passes through unchanged, though every + `await` still defers its continuation one reaction turn. - [x] `new` for Array, Object, Error types, Date, RegExp, Map, Set, URL, URLSearchParams, and Promise. - [x] Arithmetic operators: `+`, `-`, `*`, `/`, `%`, and `**`. - [x] Equality and ordering: `==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, and `>=`. @@ -152,7 +152,8 @@ ultimate source of truth. ## Promises and tools - [x] Tool calls start eagerly and return supervised, run-once CodeMode promises. -- [x] Direct `await`, repeated awaits, and implicit resolution when a promise is returned from a function/program. +- [x] Direct `await`, repeated awaits, and recursive thenable assimilation when a promise or thenable is returned from + a function/program. - [x] `Promise.resolve` and `Promise.reject`. - [x] `Promise.all`, `Promise.allSettled`, `Promise.race`, and `Promise.any` over finite collections, custom synchronous iterators, and synchronous generators containing promises and plain values. @@ -178,11 +179,14 @@ ultimate source of truth. the catch-normalized reasons in input order, and empty input rejects with an empty `AggregateError`. - [x] `new Promise((resolve, reject) => ...)`: the executor runs synchronously and receives first-class resolve/reject callables that settle the promise exactly once (they may escape the executor and settle later); an executor - throw rejects unless the promise already settled, resolving with a promise adopts it, and resolving with the - promise itself rejects with a `TypeError`. Resolver callables work anywhere callbacks are accepted, including - `.then`/`.catch` handlers and collection callbacks, but remain opaque references that cannot cross the data - boundary. -- [ ] Thenable assimilation; objects with a callable `then` field remain plain data. + throw rejects unless the promise already settled, resolving with a promise or callable thenable adopts it, and + resolving with the promise itself rejects with a `TypeError`. Resolver callables work anywhere callbacks are + accepted, including `.then`/`.catch` handlers and collection callbacks, but remain opaque references that cannot + cross the data boundary. +- [x] Recursive assimilation of objects with an own callable `then` field across `Promise.resolve`, combinators, + constructors, reactions, `finally`, `await`, and async returns. Thenable methods run deferred, receive + first-call-wins resolve/reject functions, and ignore throws after settlement. Inherited/accessor `then` fields + and a JavaScript `this` receiver remain outside the supported object/function model. - [x] Dotted tool names are canonicalized into namespace paths; a path can be both callable and a namespace, and the last definition supplied for a canonical path wins. - [x] Tool path segments may be named `constructor`, `prototype`, or `__proto__` because paths use inert Map keys. diff --git a/packages/codemode/src/interpreter/promises.ts b/packages/codemode/src/interpreter/promises.ts index 8f5fe2bdd42c..26b6824e993a 100644 --- a/packages/codemode/src/interpreter/promises.ts +++ b/packages/codemode/src/interpreter/promises.ts @@ -87,6 +87,51 @@ export class PromiseRuntime { export const selfResolutionError = (node?: AstNode): InterpreterRuntimeError => new InterpreterRuntimeError("Chaining cycle detected: a promise cannot resolve with itself.", node).as("TypeError") +export const resolvePromiseValue = ( + runner: CallbackRunner, + value: unknown, + node: AstNode, + own?: { promise?: CodeModePromise }, +): Effect.Effect => { + if (own?.promise !== undefined && value === own.promise) return Effect.fail(selfResolutionError(node)) + if (value instanceof CodeModePromise) return runner.settlePromise(value) + if (value === null || typeof value !== "object" || !Object.hasOwn(value, "then")) return Effect.succeed(value) + const then = (value as SafeObject).then + if (typeofValue(then) !== "function") return Effect.succeed(value) + + return Effect.gen(function* () { + // Promise resolution invokes a thenable's method in a later job. + yield* Effect.yieldNow + const deferred = Deferred.makeUnsafe() + const resolve = new PromiseCapabilityFunction((result) => { + Deferred.doneUnsafe(deferred, Exit.succeed(result)) + }) + const reject = new PromiseCapabilityFunction((reason) => { + Deferred.doneUnsafe(deferred, Exit.fail(new ProgramThrow(reason))) + }) + const executed = yield* Effect.exit(runner.invokeCallable(then, [resolve, reject], node)) + if (!Exit.isSuccess(executed)) { + if (Cause.hasInterruptsOnly(executed.cause)) return yield* Effect.failCause(executed.cause) + Deferred.doneUnsafe(deferred, Exit.fail(Cause.squash(executed.cause))) + } + return yield* resolvePromiseValue(runner, yield* Deferred.await(deferred), node, own) + }) +} + +export const resolvePromise = ( + runner: CallbackRunner, + promises: PromiseRuntime, + value: unknown, + node: AstNode, +): Effect.Effect => { + if (value instanceof CodeModePromise) return Effect.succeed(value) + const box: { promise?: CodeModePromise } = {} + return Effect.map(promises.create(resolvePromiseValue(runner, value, node, box)), (promise) => { + box.promise = promise + return promise + }) +} + export const invokePromiseMethod = ( runner: CallbackRunner & SyncIteratorRunner, promises: PromiseRuntime, @@ -95,8 +140,7 @@ export const invokePromiseMethod = ( node: AstNode, ): Effect.Effect => { if (ref.name === "resolve") { - const value = args[0] - return value instanceof CodeModePromise ? Effect.succeed(value) : promises.create(Effect.succeed(value)) + return resolvePromise(runner, promises, args[0], node) } if (ref.name === "reject") { return promises.create(Effect.fail(new ProgramThrow(args[0]))) @@ -111,20 +155,19 @@ export const invokePromiseMethod = ( node, ).as("TypeError") } - const items: Array = [] + const items: Array = [] while (true) { const step = yield* cursor.next if (step.done) break - items.push(step.value) - if (step.value instanceof CodeModePromise) promises.markObserved(step.value) + const item = yield* resolvePromise(runner, promises, step.value, node) + promises.markObserved(item) + items.push(item) } if (ref.name === "all") { return yield* settleAfterTurn( Effect.all( - items.map((item) => - item instanceof CodeModePromise ? Effect.flatten(promises.await(item)) : Effect.succeed(item), - ), + items.map((item) => Effect.flatten(promises.await(item))), { concurrency: "unbounded" }, ), ) @@ -132,7 +175,7 @@ export const invokePromiseMethod = ( if (ref.name === "allSettled") { const outcomes: Array = [] for (const item of items) { - const exit = item instanceof CodeModePromise ? yield* promises.await(item) : Exit.succeed(item) + const exit = yield* promises.await(item) if (Exit.isSuccess(exit)) { outcomes.push(Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value })) continue @@ -155,24 +198,14 @@ export const invokePromiseMethod = ( node, ) } - return yield* settleAfterTurn( - Effect.flatten( - Effect.raceAll( - items.map((item) => - item instanceof CodeModePromise ? promises.await(item) : Effect.succeed(Exit.succeed(item)), - ), - ), - ), - ) + return yield* settleAfterTurn(Effect.flatten(Effect.raceAll(items.map((item) => promises.await(item))))) } const flipped = items.map((item) => - item instanceof CodeModePromise - ? Effect.flatMap(promises.await(item), (exit) => { - if (Exit.isSuccess(exit)) return Effect.fail(new PromiseAnyFulfilled(exit.value)) - if (Cause.hasInterruptsOnly(exit.cause)) return Effect.failCause(exit.cause) - return Effect.succeed(caughtErrorValue(Cause.squash(exit.cause))) - }) - : Effect.fail(new PromiseAnyFulfilled(item)), + Effect.flatMap(promises.await(item), (exit) => { + if (Exit.isSuccess(exit)) return Effect.fail(new PromiseAnyFulfilled(exit.value)) + if (Cause.hasInterruptsOnly(exit.cause)) return Effect.failCause(exit.cause) + return Effect.succeed(caughtErrorValue(Cause.squash(exit.cause))) + }), ) return yield* settleAfterTurn( Effect.all(flipped, { concurrency: "unbounded" }).pipe( @@ -219,15 +252,11 @@ export const constructPromise = ( } return Effect.gen(function* () { const deferred = Deferred.makeUnsafe() - const box: { own?: CodeModePromise } = {} + const box: { promise?: CodeModePromise } = {} const promise = yield* promises.create( - Effect.flatMap(Deferred.await(deferred), (value) => { - if (!(value instanceof CodeModePromise)) return Effect.succeed(value) - if (value === box.own) return Effect.fail(selfResolutionError(node)) - return runner.settlePromise(value) - }), + Effect.flatMap(Deferred.await(deferred), (value) => resolvePromiseValue(runner, value, node, box)), ) - box.own = promise + box.promise = promise const resolve = new PromiseCapabilityFunction((value) => { Deferred.doneUnsafe(deferred, Exit.succeed(value)) }) @@ -283,19 +312,17 @@ const chainReaction = ( method: string, node: AstNode, ): Effect.Effect => { - const box: { derived?: CodeModePromise } = {} + const box: { promise?: CodeModePromise } = {} const body = Effect.gen(function* () { const exit = yield* reactionExit(promises, source) const handler = Exit.isSuccess(exit) ? onFulfilled : onRejected if (handler === undefined) return yield* exit const input = Exit.isSuccess(exit) ? exit.value : caughtErrorValue(Cause.squash(exit.cause)) const result = yield* applyCollectionCallback(runner, handler, method, node)([input]) - if (result === box.derived) return yield* Effect.fail(selfResolutionError(node)) - if (result instanceof CodeModePromise) return yield* runner.settlePromise(result) - return result + return yield* resolvePromiseValue(runner, result, node, box) }) return Effect.map(promises.create(body), (derived) => { - box.derived = derived + box.promise = derived return derived }) } @@ -313,7 +340,13 @@ const chainFinally = ( const exit = yield* reactionExit(promises, source) if (cleanup !== undefined) { const result = yield* applyCollectionCallback(runner, cleanup, method, node)([]) - if (result instanceof CodeModePromise) yield* runner.settlePromise(result) + const intermediate = yield* promises.create( + Effect.gen(function* () { + yield* runner.settlePromise(yield* resolvePromise(runner, promises, result, node)) + return yield* exit + }), + ) + return yield* runner.settlePromise(intermediate) } return yield* exit }), diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts index 1d4e03066513..3af385d5130c 100644 --- a/packages/codemode/src/interpreter/runtime.ts +++ b/packages/codemode/src/interpreter/runtime.ts @@ -57,7 +57,8 @@ import { invokePromiseInstanceMethod, invokePromiseMethod, PromiseRuntime, - selfResolutionError, + resolvePromise, + resolvePromiseValue, } from "./promises.js" import { containsOpaqueReference, isRuntimeReference, rejectCircularInsertion, typeofValue } from "./references.js" import { ScopeStack } from "./scope.js" @@ -266,6 +267,8 @@ type GeneratorState = { available?: Deferred.Deferred } +const promiseResolutionNode: AstNode = { type: "PromiseResolution" } + export class Interpreter { private scopes: ScopeStack private readonly invokeTool: (path: ReadonlyArray, args: Array) => Effect.Effect @@ -356,7 +359,7 @@ export class Interpreter { } // The implicit async body adopts returned promises before copy-out. - if (value instanceof CodeModePromise) value = yield* self.settlePromise(value) + value = yield* resolvePromiseValue(self.runner, value, program) return value }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop()))) } @@ -778,8 +781,10 @@ export class Interpreter { ) } - private awaitValue(value: unknown): Effect.Effect { - return value instanceof CodeModePromise ? this.settlePromise(value) : Effect.as(Effect.yieldNow, value) + private awaitValue(value: unknown, node: AstNode = promiseResolutionNode): Effect.Effect { + return Effect.flatMap(resolvePromise(this.runner, this.promises, value, node), (promise) => + this.settlePromise(promise), + ) } private awaitAsyncFromSyncValue( @@ -1387,9 +1392,8 @@ export class Interpreter { return this.evaluateUpdateExpression(node) case "AwaitExpression": { // Await always suspends, including for plain values. - const self = this return Effect.flatMap(this.evaluateExpression(getNode(node, "argument")), (value) => - value instanceof CodeModePromise ? self.settlePromise(value) : Effect.as(Effect.yieldNow, value), + this.awaitValue(value, node), ) } case "YieldExpression": @@ -2102,18 +2106,12 @@ export class Interpreter { }) if (fn.generator) return Effect.succeed(this.createGenerator(invocation, run, fn.async)) if (!fn.async) return run - // The initial yield assigns `box.own` before the body can self-resolve. - const box: { own?: CodeModePromise } = {} + // The initial yield assigns the promise before the body can self-resolve. + const box: { promise?: CodeModePromise } = {} return Effect.map( - this.createPromise( - Effect.flatMap(run, (value) => { - if (!(value instanceof CodeModePromise)) return Effect.succeed(value) - if (value === box.own) return Effect.fail(selfResolutionError()) - return invocation.settlePromise(value) - }), - ), + this.createPromise(Effect.flatMap(run, (value) => resolvePromiseValue(invocation.runner, value, fn.body, box))), (promise) => { - box.own = promise + box.promise = promise return promise }, ) diff --git a/packages/codemode/test/promise-test262.test.ts b/packages/codemode/test/promise-test262.test.ts index 0dc57e7b75aa..d2b60659b2de 100644 --- a/packages/codemode/test/promise-test262.test.ts +++ b/packages/codemode/test/promise-test262.test.ts @@ -946,7 +946,7 @@ describe("Test262 expected Promise conformance", () => { ).toBe("TypeError") }) - test.failing("Promise.resolve recursively assimilates callable thenables", async () => { + test("Promise.resolve recursively assimilates callable thenables", async () => { // Source: test/built-ins/Promise/resolve/resolve-thenable.js expect( await value(` @@ -958,7 +958,7 @@ describe("Test262 expected Promise conformance", () => { ).toBe(true) }) - test.failing("Promise combinators assimilate callable thenable inputs", async () => { + test("Promise combinators assimilate callable thenable inputs", async () => { // Sources: // test/built-ins/Promise/all/reject-immed.js // test/built-ins/Promise/all/reject-ignored-immed.js @@ -988,7 +988,7 @@ describe("Test262 expected Promise conformance", () => { ]) }) - test.failing("await assimilates callable thenables", async () => { + test("await assimilates callable thenables", async () => { // Source: test/language/expressions/await/await-awaits-thenables.js expect( await value(` @@ -998,7 +998,7 @@ describe("Test262 expected Promise conformance", () => { ).toBe(42) }) - test.failing("await rejects when a callable thenable throws", async () => { + test("await rejects when a callable thenable throws", async () => { // Source: test/language/expressions/await/await-awaits-thenables-that-throw.js expect( await value(` @@ -1013,6 +1013,94 @@ describe("Test262 expected Promise conformance", () => { `), ).toBe(true) }) + + test("thenable resolution is deferred and settles only once", async () => { + // Sources: + // test/built-ins/Promise/resolve/S25.Promise_resolve_foreign_thenable_2.js + // test/built-ins/Promise/exception-after-resolve-in-thenable-job.js + expect( + await value(` + const sequence = [] + const thenable = { + then: (resolve, reject) => { + sequence.push("then") + resolve(1) + reject(2) + throw 3 + } + } + const promise = Promise.resolve(thenable) + sequence.push("after resolve") + const result = await promise + sequence.push("after await") + return [result, sequence] + `), + ).toEqual([1, ["after resolve", "then", "after await"]]) + }) + + test("the first thenable rejection wins over later resolution and throws", async () => { + // Source: test/built-ins/Promise/exception-after-resolve-in-thenable-job.js + expect( + await value(` + const thenable = { + then: (resolve, reject) => { + reject("first") + resolve("second") + throw "third" + } + } + try { + await thenable + return "fulfilled" + } catch (reason) { + return reason + } + `), + ).toBe("first") + }) + + test("constructors, reactions, finally, and async returns assimilate thenables", async () => { + // Sources: + // test/built-ins/Promise/resolve-thenable-immed.js + // test/built-ins/Promise/prototype/then/resolve-settled-fulfilled-thenable.js + // test/built-ins/Promise/prototype/finally/resolved-observable-then-calls.js + const thenable = (value: string) => `({ then: (resolve) => resolve(${JSON.stringify(value)}) })` + expect( + await value(` + const fromAsync = async () => ${thenable("async")} + const cleanup = [] + return await Promise.all([ + new Promise((resolve) => resolve(${thenable("constructor")})), + Promise.resolve().then(() => ${thenable("reaction")}), + Promise.resolve("kept").finally(() => { + cleanup.push("ran") + return ${thenable("ignored")} + }), + fromAsync(), + ]).then((values) => [values, cleanup]) + `), + ).toEqual([["constructor", "reaction", "kept", "async"], ["ran"]]) + }) + + test("finally settles after its cleanup thenable reactions", async () => { + // Source: test/built-ins/Promise/prototype/finally/resolved-observable-then-calls-PromiseResolve.js + expect( + await value(` + const sequence = [] + const cleanup = { then: (resolve) => { sequence.push("then"); resolve() } } + const result = Promise.resolve("kept").finally(() => cleanup) + result.then(() => sequence.push("finally")) + Promise.resolve() + .then(() => sequence.push("tick1")) + .then(() => sequence.push("tick2")) + .then(() => sequence.push("tick3")) + .then(() => sequence.push("tick4")) + await result + sequence.push("await") + return sequence + `), + ).toEqual(["tick1", "then", "tick2", "tick3", "tick4", "finally", "await"]) + }) }) describe("Test262 Promise.any", () => { From 23483ea013af7e806e29b443780cc62f4da0d992 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:42:59 -0500 Subject: [PATCH 009/150] feat(ai): support custom reasoning fields (#38227) --- packages/ai/src/protocols/openai-chat.ts | 97 ++++++++++++------- packages/ai/src/schema/options.ts | 1 + .../openai-chat-reasoning.recorded.test.ts | 28 +++--- packages/ai/test/provider/openai-chat.test.ts | 68 +++++++++++++ .../client/src/promise/generated/types.ts | 7 +- packages/core/src/aisdk.ts | 7 +- packages/core/src/config/plugin/provider.ts | 3 +- packages/core/src/config/provider.ts | 1 + packages/core/src/model.ts | 12 +++ packages/core/src/models-dev.ts | 3 +- packages/core/src/plugin/provider/opencode.ts | 1 + packages/core/src/session/runner/model.ts | 19 ++-- packages/core/src/v1/config/migrate.ts | 2 + packages/core/src/v1/config/provider.ts | 5 +- packages/core/test/config/config.test.ts | 22 +++++ packages/core/test/config/provider.test.ts | 2 + packages/core/test/models.test.ts | 11 ++- .../core/test/session-runner-model.test.ts | 4 + packages/docs/models.mdx | 22 +++++ packages/httpapi-codegen/src/index.ts | 14 ++- .../httpapi-codegen/test/generate.test.ts | 13 +++ packages/plugin/src/v2/effect/catalog.ts | 9 +- packages/schema/src/model.ts | 12 +++ packages/schema/test/model.test.ts | 10 ++ 24 files changed, 306 insertions(+), 67 deletions(-) diff --git a/packages/ai/src/protocols/openai-chat.ts b/packages/ai/src/protocols/openai-chat.ts index 3b35bb88d825..e423137fca03 100644 --- a/packages/ai/src/protocols/openai-chat.ts +++ b/packages/ai/src/protocols/openai-chat.ts @@ -25,6 +25,7 @@ import { ToolStream } from "./utils/tool-stream" const ADAPTER = "openai-chat" const IMAGE_MIMES = new Set(ProviderShared.IMAGE_MIMES) +const RESERVED_REASONING_FIELDS = new Set(["role", "content", "tool_calls"]) export const DEFAULT_BASE_URL = "https://api.openai.com/v1" export const PATH = "/chat/completions" @@ -70,15 +71,18 @@ const OpenAIChatMessage = Schema.Union([ role: Schema.Literal("user"), content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]), }), - Schema.Struct({ - role: Schema.Literal("assistant"), - content: Schema.NullOr(Schema.String), - tool_calls: optionalArray(OpenAIChatAssistantToolCall), - reasoning_content: Schema.optional(Schema.String), - reasoning: Schema.optional(Schema.String), - reasoning_text: Schema.optional(Schema.String), - reasoning_details: optionalArray(Schema.Unknown), - }), + Schema.StructWithRest( + Schema.Struct({ + role: Schema.Literal("assistant"), + content: Schema.NullOr(Schema.String), + tool_calls: optionalArray(OpenAIChatAssistantToolCall), + reasoning_content: Schema.optional(Schema.String), + reasoning: Schema.optional(Schema.String), + reasoning_text: Schema.optional(Schema.String), + reasoning_details: Schema.optional(Schema.Unknown), + }), + [Schema.Record(Schema.String, Schema.Unknown)], + ), Schema.Struct({ role: Schema.Literal("tool"), tool_call_id: Schema.String, content: Schema.String }), ]).pipe(Schema.toTaggedUnion("role")) type OpenAIChatMessage = Schema.Schema.Type @@ -145,14 +149,17 @@ const OpenAIChatToolCallDelta = Schema.Struct({ }) type OpenAIChatToolCallDelta = Schema.Schema.Type -const OpenAIChatDelta = Schema.Struct({ - content: optionalNull(Schema.String), - reasoning_content: optionalNull(Schema.String), - reasoning: optionalNull(Schema.String), - reasoning_text: optionalNull(Schema.String), - reasoning_details: optionalNull(Schema.Array(Schema.Unknown)), - tool_calls: optionalNull(Schema.Array(OpenAIChatToolCallDelta)), -}) +const OpenAIChatDelta = Schema.StructWithRest( + Schema.Struct({ + content: optionalNull(Schema.String), + reasoning_content: optionalNull(Schema.String), + reasoning: optionalNull(Schema.String), + reasoning_text: optionalNull(Schema.String), + reasoning_details: optionalNull(Schema.Unknown), + tool_calls: optionalNull(Schema.Array(OpenAIChatToolCallDelta)), + }), + [Schema.Record(Schema.String, Schema.Unknown)], +) const OpenAIChatChoice = Schema.Struct({ delta: optionalNull(OpenAIChatDelta), @@ -179,7 +186,7 @@ export interface ParserState { readonly usage?: Usage readonly finishReason?: FinishReason readonly lifecycle: Lifecycle.State - readonly reasoningField?: "reasoning" | "reasoning_content" | "reasoning_text" + readonly reasoningField?: string readonly reasoningDetails: Array readonly reasoningDetailsObserved: boolean readonly reasoningEmitted: boolean @@ -227,7 +234,7 @@ const openAICompatibleReasoningContent = (native: unknown) => const reasoningField = (part: ReasoningPart) => { const field = part.providerMetadata?.openai?.reasoningField - if (field === "reasoning" || field === "reasoning_content" || field === "reasoning_text") return field + return typeof field === "string" ? field : undefined } const reasoningDetails = (parts: ReadonlyArray, native: unknown) => { @@ -259,6 +266,7 @@ const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (mes const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(function* ( message: OpenAIChatRequestMessage, + configuredField?: string, ) { const content: TextPart[] = [] const reasoning: ReasoningPart[] = [] @@ -285,24 +293,25 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func const nativeReasoning = openAICompatibleReasoningContent(message.native?.openaiCompatible) const fullyStructured = reasoning.every((part) => Array.isArray(part.providerMetadata?.openai?.reasoningDetails)) const field = (() => { - if (reasoning.length === 0) return + if (configuredField !== undefined) return configuredField + if (reasoning.length === 0) return undefined if (observedField !== undefined) return observedField if (nativeReasoning !== undefined) return "reasoning_content" if (!fullyStructured) return "reasoning_content" })() - const reasoningContent = (() => { + const reasoningText = (() => { + if (configuredField !== undefined) return reasoning.length === 0 ? (nativeReasoning ?? "") : text if (reasoning.length === 0) return nativeReasoning - if (field === "reasoning_content") return text + return text })() - return { + const result = { role: "assistant" as const, content: content.length === 0 ? null : ProviderShared.joinText(content), tool_calls: toolCalls.length === 0 ? undefined : toolCalls, - reasoning_content: reasoningContent, - reasoning: reasoning.length > 0 && field === "reasoning" ? text : undefined, - reasoning_text: reasoning.length > 0 && field === "reasoning_text" ? text : undefined, reasoning_details: details, } + if (field === undefined || reasoningText === undefined) return result + return { ...result, [field]: reasoningText } }) const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (message: OpenAIChatRequestMessage) { @@ -328,9 +337,12 @@ const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (m return { messages, images } }) -const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* (message: OpenAIChatRequestMessage) { +const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* ( + message: OpenAIChatRequestMessage, + reasoningField?: string, +) { if (message.role === "user") return [yield* lowerUserMessage(message)] - if (message.role === "assistant") return [yield* lowerAssistantMessage(message)] + if (message.role === "assistant") return [yield* lowerAssistantMessage(message, reasoningField)] return (yield* lowerToolMessages(message)).messages }) @@ -368,7 +380,7 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request: continue } flushImages() - messages.push(...(yield* lowerMessage(message))) + messages.push(...(yield* lowerMessage(message, request.model.compatibility?.reasoningField))) } flushImages() return messages @@ -386,6 +398,11 @@ const lowerOptions = Effect.fn("OpenAIChat.lowerOptions")(function* (request: LL const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (request: LLMRequest) { // `fromRequest` returns the provider body only. Endpoint, auth, framing, // validation, and HTTP execution are composed by `Route.make`. + const reasoningField = request.model.compatibility?.reasoningField + if (reasoningField && RESERVED_REASONING_FIELDS.has(reasoningField)) + return yield* ProviderShared.invalidRequest( + `OpenAI Chat reasoning field conflicts with reserved field ${reasoningField}`, + ) const generation = request.generation const toolSchemaCompatibility = request.model.compatibility?.toolSchema return { @@ -446,10 +463,18 @@ const mapUsage = (usage: OpenAIChatEvent["usage"]): Usage | undefined => { }) } -const reasoningDelta = (delta: Schema.Schema.Type | null | undefined) => { - if (delta?.reasoning_content) return { field: "reasoning_content", text: delta.reasoning_content } as const - if (delta?.reasoning) return { field: "reasoning", text: delta.reasoning } as const - if (delta?.reasoning_text) return { field: "reasoning_text", text: delta.reasoning_text } as const +const reasoningDelta = ( + delta: Schema.Schema.Type | null | undefined, + configuredField?: string, +) => { + if (!delta) return undefined + const fields = new Set([configuredField, "reasoning_content", "reasoning", "reasoning_text"]) + for (const field of fields) { + if (field === undefined) continue + const text = delta[field] + if (typeof text === "string" && text.length > 0) return { field, text } + } + return undefined } const detailText = (details: ReadonlyArray) => { @@ -518,7 +543,7 @@ const step = (state: ParserState, event: OpenAIChatEvent) => let lifecycle = state.lifecycle - const reasoning = reasoningDelta(delta) + const reasoning = reasoningDelta(delta, state.reasoningField) const reasoningField = state.reasoningField ?? (!state.lifecycle.text.has("text-0") ? reasoning?.field : undefined) const detailDelta = Array.isArray(delta?.reasoning_details) ? delta.reasoning_details : undefined if (detailDelta !== undefined) appendReasoningDetails(state.reasoningDetails, detailDelta) @@ -635,12 +660,12 @@ export const protocol = Protocol.make({ }, stream: { event: Protocol.jsonEvent(OpenAIChatEvent), - initial: () => ({ + initial: (request) => ({ tools: ToolStream.empty(), pendingTools: {}, toolCallEvents: [], lifecycle: Lifecycle.initial(), - reasoningField: undefined, + reasoningField: request.model.compatibility?.reasoningField, reasoningDetails: [], reasoningDetailsObserved: false, reasoningEmitted: false, diff --git a/packages/ai/src/schema/options.ts b/packages/ai/src/schema/options.ts index 6d11333b536d..b7ef93252789 100644 --- a/packages/ai/src/schema/options.ts +++ b/packages/ai/src/schema/options.ts @@ -168,6 +168,7 @@ export type ModelToolSchemaCompatibility = Schema.Schema.Type("LLM.ModelCompatibility")({ toolSchema: Schema.optional(ModelToolSchemaCompatibility), + reasoningField: Schema.optional(Schema.String), }) {} export namespace ModelCompatibility { diff --git a/packages/ai/test/provider/openai-chat-reasoning.recorded.test.ts b/packages/ai/test/provider/openai-chat-reasoning.recorded.test.ts index 5ac810db616b..a8b40346cf20 100644 --- a/packages/ai/test/provider/openai-chat-reasoning.recorded.test.ts +++ b/packages/ai/test/provider/openai-chat-reasoning.recorded.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" -import { LLM, LLMEvent, LLMResponse } from "../../src" +import { LLM, LLMEvent, LLMResponse, Model } from "../../src" import { OpenAIChat } from "../../src/protocols/openai-chat" import * as OpenAICompatible from "../../src/providers/openai-compatible" import * as OpenRouter from "../../src/providers/openrouter" @@ -11,22 +11,28 @@ import { expectWeatherToolLoop, goldenWeatherToolLoopRequest, runWeatherToolLoop const cases = [ { name: "OpenRouter", - model: OpenRouter.configure({ - apiKey: process.env.OPENROUTER_API_KEY ?? "fixture", - providerOptions: { openrouter: { reasoning: { max_tokens: 1024 } } }, - }).model("anthropic/claude-sonnet-4.6"), + model: Model.update( + OpenRouter.configure({ + apiKey: process.env.OPENROUTER_API_KEY ?? "fixture", + providerOptions: { openrouter: { reasoning: { max_tokens: 1024 } } }, + }).model("anthropic/claude-sonnet-4.6"), + { compatibility: { reasoningField: "reasoning" } }, + ), requires: ["OPENROUTER_API_KEY"], cassette: "openrouter-reasoning", structured: true, }, { name: "Vercel AI Gateway", - model: OpenAICompatible.configure({ - provider: "vercel-ai-gateway", - baseURL: "https://ai-gateway.vercel.sh/v1", - apiKey: process.env.AI_GATEWAY_API_KEY ?? "fixture", - http: { body: { reasoning: { enabled: true, max_tokens: 1024 } } }, - }).model("anthropic/claude-sonnet-4.6"), + model: Model.update( + OpenAICompatible.configure({ + provider: "vercel-ai-gateway", + baseURL: "https://ai-gateway.vercel.sh/v1", + apiKey: process.env.AI_GATEWAY_API_KEY ?? "fixture", + http: { body: { reasoning: { enabled: true, max_tokens: 1024 } } }, + }).model("anthropic/claude-sonnet-4.6"), + { compatibility: { reasoningField: "reasoning" } }, + ), requires: ["AI_GATEWAY_API_KEY"], cassette: "vercel-ai-gateway-reasoning", structured: true, diff --git a/packages/ai/test/provider/openai-chat.test.ts b/packages/ai/test/provider/openai-chat.test.ts index 591e5f0be0de..50bc34df15f8 100644 --- a/packages/ai/test/provider/openai-chat.test.ts +++ b/packages/ai/test/provider/openai-chat.test.ts @@ -92,6 +92,45 @@ describe("OpenAI Chat route", () => { }), ) + it.effect("writes reasoning to a configured custom field on every assistant message", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model: Model.update(model, { compatibility: { reasoningField: "vendor_reasoning" } }), + messages: [ + Message.assistant([ + { + type: "reasoning", + text: "thinking", + providerMetadata: { openai: { reasoningField: "reasoning" } }, + }, + { type: "text", text: "Hello" }, + ]), + Message.assistant("Done"), + ], + }), + ) + + expect(prepared.body.messages).toEqual([ + { role: "assistant", content: "Hello", vendor_reasoning: "thinking" }, + { role: "assistant", content: "Done", vendor_reasoning: "" }, + ]) + }), + ) + + it.effect("rejects reasoning fields that conflict with assistant message fields", () => + Effect.gen(function* () { + const error = yield* LLMClient.prepare( + LLM.request({ + model: Model.update(model, { compatibility: { reasoningField: "content" } }), + messages: [Message.assistant([{ type: "reasoning", text: "thinking" }])], + }), + ).pipe(Effect.flip) + + expect(error.message).toContain("reserved field content") + }), + ) + it.effect("maps OpenAI provider options to Chat options", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( @@ -570,6 +609,35 @@ describe("OpenAI Chat route", () => { }), ) + it.effect("parses and replays a configured custom reasoning field", () => + Effect.gen(function* () { + const custom = Model.update(model, { compatibility: { reasoningField: "vendor_reasoning" } }) + const response = yield* LLMClient.generate(LLM.updateRequest(request, { model: custom })).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { choices: [{ delta: { vendor_reasoning: "thinking" } }] }, + { choices: [{ delta: { content: "Hello" } }] }, + { choices: [{ delta: {}, finish_reason: "stop" }] }, + ), + ), + ), + ) + + expect(response.reasoning).toBe("thinking") + expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({ + openai: { reasoningField: "vendor_reasoning" }, + }) + + const replay = yield* LLMClient.prepare( + LLM.request({ model: custom, messages: [response.message] }), + ) + expect(replay.body.messages).toEqual([ + { role: "assistant", content: "Hello", vendor_reasoning: "thinking" }, + ]) + }), + ) + it.effect("preserves and replays reasoning details alongside scalar reasoning", () => Effect.gen(function* () { const details = [ diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 6664c1424c2c..d09aa48238da 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -163,6 +163,8 @@ export type SessionMessageProviderState7 = { [x: string]: any } export type EventLogSynced = { type: "log.synced"; aggregateID: string; seq?: number } +export type ModelReasoningField = "reasoning" | "reasoning_content" | "reasoning_text" | (string & {}) + export type ModelCapabilities = { tools: boolean; input: Array; output: Array } export type ModelVariant = { @@ -1094,7 +1096,7 @@ export type TuiCommandExecute = { | "prompt.clear" | "prompt.submit" | "agent.cycle" - | string + | (string & {}) } } @@ -1403,6 +1405,8 @@ export type SessionToolFailed = { } } +export type ModelCompatibility = { reasoningField?: ModelReasoningField } + export type ModelCost = { tier?: { type: "context"; size: number } input: MoneyUSDPerMillionTokens @@ -1893,6 +1897,7 @@ export type ModelInfo = { providerID: string family?: string name: string + compatibility?: ModelCompatibility package?: string settings?: { [x: string]: JsonValue } headers?: { [x: string]: string } diff --git a/packages/core/src/aisdk.ts b/packages/core/src/aisdk.ts index d81d62505757..6d749b8526f4 100644 --- a/packages/core/src/aisdk.ts +++ b/packages/core/src/aisdk.ts @@ -344,7 +344,12 @@ function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) { prepareTransport: (body) => Effect.succeed(body), streamPrepared: (prepared) => streamLanguage(language, prepared as LanguageModelV3CallOptions), } - return Model.make({ id: info.modelID ?? info.id, provider: info.providerID, route }) + return Model.make({ + id: info.modelID ?? info.id, + provider: info.providerID, + route, + compatibility: info.compatibility, + }) } function gatewayProviderOptions(modelID: ModelV2.ID, settings: Readonly>) { diff --git a/packages/core/src/config/plugin/provider.ts b/packages/core/src/config/plugin/provider.ts index 1bb05e0fa198..b0dceca26e37 100644 --- a/packages/core/src/config/plugin/provider.ts +++ b/packages/core/src/config/plugin/provider.ts @@ -4,7 +4,6 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { Money } from "@opencode-ai/schema/money" import { Effect, Stream } from "effect" import { Config } from "../../config" -import { ModelV2 } from "../../model" import { ProviderV2 } from "../../provider" export const Plugin = define({ @@ -59,6 +58,8 @@ export const Plugin = define({ if (config.family !== undefined) model.family = config.family if (config.name !== undefined) model.name = config.name if (config.modelID !== undefined) model.modelID = config.modelID + if (config.compatibility !== undefined) + model.compatibility = { ...model.compatibility, ...config.compatibility } if (config.package !== undefined) model.package = config.package if (config.settings !== undefined) model.settings = ProviderV2.mergeOverlay(model.settings, config.settings) diff --git a/packages/core/src/config/provider.ts b/packages/core/src/config/provider.ts index e2fa972806e5..8441ef0828c0 100644 --- a/packages/core/src/config/provider.ts +++ b/packages/core/src/config/provider.ts @@ -42,6 +42,7 @@ class Model extends Schema.Class("ConfigV2.Model")({ modelID: ModelV2.ID.pipe(Schema.optional), family: ModelV2.Family.pipe(Schema.optional), name: Schema.String.pipe(Schema.optional), + compatibility: ModelV2.Compatibility.pipe(Schema.optional), package: Schema.String.pipe(Schema.optional), ...Overlays, capabilities: ModelV2.Capabilities.pipe(Schema.optional), diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index 3f264ecd9f47..e8045f11043c 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -12,6 +12,12 @@ export type VariantID = typeof VariantID.Type export const Family = Model.Family export type Family = Model.Family +export const ReasoningField = Model.ReasoningField +export type ReasoningField = Model.ReasoningField + +export const Compatibility = Model.Compatibility +export type Compatibility = Model.Compatibility + export const Capabilities = Model.Capabilities export type Capabilities = Model.Capabilities @@ -25,6 +31,12 @@ export type Info = Model.Info export type MutableInfo = DeepMutable +export function compatibility(input: unknown): Compatibility | undefined { + if (typeof input === "string") return { reasoningField: input } + if (typeof input !== "object" || input === null || Array.isArray(input) || !("field" in input)) return undefined + return typeof input.field === "string" ? { reasoningField: input.field } : undefined +} + export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } { const [providerID, ...modelID] = input.split("/") return { diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts index 6fb301136310..2db39ab124e8 100644 --- a/packages/core/src/models-dev.ts +++ b/packages/core/src/models-dev.ts @@ -43,7 +43,7 @@ type SourceModel = { readonly reasoning_options?: readonly ReasoningOption[] readonly temperature?: boolean readonly tool_call: boolean - readonly interleaved?: true | { readonly field: "reasoning" | "reasoning_content" | "reasoning_details" } + readonly interleaved?: boolean | string | { readonly field: string } readonly cost?: Cost readonly limit: { readonly context: number; readonly input?: number; readonly output: number } readonly modalities?: { readonly input: readonly Modality[]; readonly output: readonly Modality[] } @@ -495,6 +495,7 @@ function modelInfo( modelID: ModelV2.ID.make(model.id), providerID, name: input.name ?? model.name, + compatibility: ModelV2.compatibility(model.interleaved), family: model.family ? ModelV2.Family.make(model.family) : undefined, package: model.provider?.npm ? ProviderV2.aisdk(model.provider.npm) : undefined, settings: model.provider?.api ? { baseURL: model.provider.api } : undefined, diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts index 1e8c52e6fc05..e70be4281429 100644 --- a/packages/core/src/plugin/provider/opencode.ts +++ b/packages/core/src/plugin/provider/opencode.ts @@ -134,6 +134,7 @@ export const OpencodePlugin = define - Model.update(module.model(resolved.modelID ?? resolved.id, settings), { provider: resolved.providerID }), + try: () => { + const runtime = module.model(resolved.modelID ?? resolved.id, settings) + return Model.update(runtime, { + provider: resolved.providerID, + compatibility: resolved.compatibility + ? { ...runtime.compatibility, ...resolved.compatibility } + : runtime.compatibility, + }) + }, catch: () => unsupported(resolved), }) }) @@ -302,7 +309,7 @@ const codexModel = ( account === undefined ? Auth.none : Auth.headers({ "chatgpt-account-id": account }), ), }) - .model({ id: model.modelID ?? model.id }) + .model({ id: model.modelID ?? model.id, compatibility: model.compatibility }) } const unsupported = (model: ModelV2.Info) => diff --git a/packages/core/src/v1/config/migrate.ts b/packages/core/src/v1/config/migrate.ts index 63348e548068..6c0a342abe7b 100644 --- a/packages/core/src/v1/config/migrate.ts +++ b/packages/core/src/v1/config/migrate.ts @@ -8,6 +8,7 @@ import { ConfigPermissionV1 } from "./permission" import { ConfigProviderV1 } from "./provider" import { ConfigProviderOptionsV1 } from "./provider-options" import { ProviderV2 } from "../../provider" +import { ModelV2 } from "../../model" const keys = new Set([ "logLevel", @@ -278,6 +279,7 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type) { modelID: info.id, family: info.family, name: info.name, + compatibility: ModelV2.compatibility(info.interleaved), package: info.provider?.npm ? ProviderV2.aisdk(info.provider.npm) : undefined, settings: info.provider?.api ? { ...settings, baseURL: info.provider.api } : settings, capabilities, diff --git a/packages/core/src/v1/config/provider.ts b/packages/core/src/v1/config/provider.ts index d54a3f08f926..37e8c58c27fa 100644 --- a/packages/core/src/v1/config/provider.ts +++ b/packages/core/src/v1/config/provider.ts @@ -16,9 +16,10 @@ export const Model = Schema.Struct({ tool_call: Schema.optional(Schema.Boolean), interleaved: Schema.optional( Schema.Union([ - Schema.Literal(true), + Schema.Boolean, + Schema.String, Schema.Struct({ - field: Schema.Literals(["reasoning", "reasoning_content", "reasoning_details"]), + field: Schema.String, }), ]), ), diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index 2daecccc6a0c..64e08b5e7963 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -419,6 +419,28 @@ describe("Config", () => { }), ) + it.effect("migrates v1 interleaved fields to compatibility", () => + Effect.sync(() => { + const migrated = ConfigMigrateV1.migrate({ + provider: { + custom: { + models: { + object: { interleaved: { field: "vendor_reasoning" } }, + string: { interleaved: "reasoning_text" }, + boolean: { interleaved: true }, + }, + }, + }, + }) + + expect(migrated.providers?.custom?.models?.object?.compatibility).toEqual({ + reasoningField: "vendor_reasoning", + }) + expect(migrated.providers?.custom?.models?.string?.compatibility).toEqual({ reasoningField: "reasoning_text" }) + expect(migrated.providers?.custom?.models?.boolean?.compatibility).toBeUndefined() + }), + ) + it.effect("migrates v1 command configuration", () => Effect.sync(() => { expect( diff --git a/packages/core/test/config/provider.test.ts b/packages/core/test/config/provider.test.ts index 0c7c85a1ca68..c3c5b9c42057 100644 --- a/packages/core/test/config/provider.test.ts +++ b/packages/core/test/config/provider.test.ts @@ -170,6 +170,7 @@ describe("ConfigProviderPlugin.Plugin", () => { models: { chat: { name: "First", + compatibility: { reasoningField: "vendor_reasoning" }, capabilities: { tools: true, input: ["text"], output: ["text"] }, disabled: true, limit: { context: 100, output: 50 }, @@ -251,6 +252,7 @@ describe("ConfigProviderPlugin.Plugin", () => { expect(model.id).toBe(modelID) expect(model.modelID).toBe(ModelV2.ID.make("api-chat")) expect(model.name).toBe("Last") + expect(model.compatibility).toEqual({ reasoningField: "vendor_reasoning" }) expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] }) expect(model.enabled).toBe(false) expect(model.limit).toEqual({ context: 100, output: 75 }) diff --git a/packages/core/test/models.test.ts b/packages/core/test/models.test.ts index 0a85eac11ac8..1d8bd4ae184c 100644 --- a/packages/core/test/models.test.ts +++ b/packages/core/test/models.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, beforeEach, afterAll } from "bun:test" +import { describe, expect, beforeEach, afterAll, test } from "bun:test" import { Money } from "@opencode-ai/schema/money" import { Effect, Layer, Ref } from "effect" import { HttpClient, HttpClientResponse } from "effect/unstable/http" @@ -15,6 +15,13 @@ import path from "path" const cacheFile = path.join(Global.Path.cache, "models.json") +test("normalizes permissive interleaved values to compatibility", () => { + expect(ModelV2.compatibility("reasoning_text")).toEqual({ reasoningField: "reasoning_text" }) + expect(ModelV2.compatibility({ field: "vendor_reasoning" })).toEqual({ reasoningField: "vendor_reasoning" }) + expect(ModelV2.compatibility(true)).toBeUndefined() + expect(ModelV2.compatibility(false)).toBeUndefined() +}) + const fixture = { acme: { id: "acme", @@ -30,6 +37,7 @@ const fixture = { reasoning: false, temperature: true, tool_call: true, + interleaved: { field: "vendor_reasoning" }, limit: { context: 128000, output: 8192 }, }, }, @@ -49,6 +57,7 @@ const fixtureSnapshot = [ modelID: ModelV2.ID.make("acme-1"), providerID: ProviderV2.ID.make("acme"), name: "Acme One", + compatibility: { reasoningField: "vendor_reasoning" }, family: undefined, package: undefined, settings: undefined, diff --git a/packages/core/test/session-runner-model.test.ts b/packages/core/test/session-runner-model.test.ts index 291cd66cc330..38fc707918c1 100644 --- a/packages/core/test/session-runner-model.test.ts +++ b/packages/core/test/session-runner-model.test.ts @@ -16,6 +16,7 @@ import { it } from "./lib/effect" interface ModelOptions { readonly modelID?: string + readonly compatibility?: ModelV2.Compatibility readonly settings?: ModelV2.Info["settings"] readonly headers?: ModelV2.Info["headers"] readonly body?: ModelV2.Info["body"] @@ -28,6 +29,7 @@ const model = (packageName: string | undefined, options: ModelOptions = {}) => modelID: ModelV2.ID.make(options.modelID ?? "api-test-model"), providerID: ProviderV2.ID.make("test-provider"), name: "Test model", + compatibility: options.compatibility, package: packageName, settings: options.settings ?? {}, headers: options.headers ?? { "x-test": "header" }, @@ -101,6 +103,7 @@ describe("SessionRunnerModel", () => { Effect.gen(function* () { const resolved = yield* SessionRunnerModel.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), { + compatibility: { reasoningField: "vendor_reasoning" }, settings: { apiKey: "settings-secret", baseURL: "https://compatible.example/v1", @@ -121,6 +124,7 @@ describe("SessionRunnerModel", () => { expect(headers.authorization).toBe("Bearer settings-secret") expect(resolved.route.id).toBe("openai-compatible-chat") + expect(resolved.compatibility?.reasoningField).toBe("vendor_reasoning") expect(resolved.route.endpoint.baseURL).toBe("https://compatible.example/v1") expect(resolved.route.defaults.http?.body).toEqual({}) }), diff --git a/packages/docs/models.mdx b/packages/docs/models.mdx index d8ba1a8be3be..60fd63812adb 100644 --- a/packages/docs/models.mdx +++ b/packages/docs/models.mdx @@ -98,6 +98,28 @@ Here `openai/coding-default` is the selectable catalog reference, while `gpt-5.2 model that is not already in the catalog, set accurate `capabilities` and `limit` values so OpenCode can expose tools and enforce the correct context limits. Set `disabled: true` on a model entry to hide it from the available catalog. +OpenAI-compatible models that stream reasoning through a custom assistant-message field can set +`compatibility.reasoningField`: + +```jsonc title="opencode.jsonc" +{ + "providers": { + "local": { + "models": { + "reasoner": { + "compatibility": { + "reasoningField": "reasoning_content" + } + } + } + } + } +} +``` + +OpenCode recognizes `reasoning`, `reasoning_content`, and `reasoning_text`, and accepts any provider-specific string. It +reads streamed reasoning from this field and includes the field when replaying assistant messages to the model. + ### Custom variants Add a variant, or override a catalog variant with the same ID, under the model's `variants` array: diff --git a/packages/httpapi-codegen/src/index.ts b/packages/httpapi-codegen/src/index.ts index 337593117c72..4de8068d56c9 100644 --- a/packages/httpapi-codegen/src/index.ts +++ b/packages/httpapi-codegen/src/index.ts @@ -853,7 +853,7 @@ function structuralTypes(schemas: ReadonlyArray, mutable: boolean, r .replaceAll(/ & Brand\.Brand<"[^"]+">/g, "") .replaceAll("Schema.Json", "JsonValue") .replaceAll(/(? render(code.Type)), @@ -893,9 +893,15 @@ function structuralType(schema: Schema.Top) { } return type } - return expand(document.codes[0].Type) - .replaceAll(/ & Brand\.Brand<"[^"]+">/g, "") - .replaceAll("Schema.Json", "JsonValue") + return preserveStringSuggestions( + expand(document.codes[0].Type) + .replaceAll(/ & Brand\.Brand<"[^"]+">/g, "") + .replaceAll("Schema.Json", "JsonValue"), + ) +} + +function preserveStringSuggestions(type: string) { + return type.replaceAll(/((?:"(?:\\.|[^"\\])*"\s*\|\s*)+)string\b/g, "$1(string & {})") } function normalizePromiseClientContent(content: string, groups: ReadonlyArray) { diff --git a/packages/httpapi-codegen/test/generate.test.ts b/packages/httpapi-codegen/test/generate.test.ts index 0322157cbd07..7374bd549034 100644 --- a/packages/httpapi-codegen/test/generate.test.ts +++ b/packages/httpapi-codegen/test/generate.test.ts @@ -503,6 +503,19 @@ describe("HttpApiCodegen.generate", () => { expect(types).not.toContain("Brand") }) + test("preserves suggestions for open string unions in Promise wire types", () => { + const Field = Schema.Union([Schema.Literals(["reasoning", "reasoning_content"]), Schema.String]).annotate({ + identifier: "Field", + }) + const output = emitPromise( + compileContract(api(HttpApiEndpoint.get("get", "/model", { success: Schema.Struct({ field: Field }) }))), + ) + + expect(output.files.find((file) => file.path === "types.ts")?.content).toContain( + 'export type Field = "reasoning" | "reasoning_content" | (string & {})', + ) + }) + test("retains non-recursive references in Promise wire types", () => { const Referenced = Schema.Struct({ value: Schema.String }).annotate({ identifier: "Referenced" }) const output = emitPromise( diff --git a/packages/plugin/src/v2/effect/catalog.ts b/packages/plugin/src/v2/effect/catalog.ts index 834578dd604e..2d2f6777c6b0 100644 --- a/packages/plugin/src/v2/effect/catalog.ts +++ b/packages/plugin/src/v2/effect/catalog.ts @@ -1,11 +1,14 @@ import type { ModelInfo, ProviderV2Info } from "@opencode-ai/sdk/v2/types" import type { CatalogApi } from "@opencode-ai/client/effect/api" +import type { Model } from "@opencode-ai/schema/model" import type { Effect } from "effect" import type { Transform } from "./registration.js" +type CatalogModel = ModelInfo & { compatibility?: Model.Compatibility } + export interface CatalogProviderRecord { readonly provider: ProviderV2Info - readonly models: ReadonlyMap + readonly models: ReadonlyMap } export interface CatalogDraft { @@ -16,8 +19,8 @@ export interface CatalogDraft { remove(providerID: string): void } readonly model: { - get(providerID: string, modelID: string): ModelInfo | undefined - update(providerID: string, modelID: string, update: (model: ModelInfo) => void): void + get(providerID: string, modelID: string): CatalogModel | undefined + update(providerID: string, modelID: string, update: (model: CatalogModel) => void): void remove(providerID: string, modelID: string): void readonly default: { get(): { providerID: string; modelID: string } | undefined diff --git a/packages/schema/src/model.ts b/packages/schema/src/model.ts index 623b80a60a10..81caea654fc8 100644 --- a/packages/schema/src/model.ts +++ b/packages/schema/src/model.ts @@ -41,6 +41,17 @@ export interface Ref extends Schema.Schema.Type {} export const Family = Schema.String.pipe(Schema.brand("Model.Family")) export type Family = typeof Family.Type +export type ReasoningField = "reasoning" | "reasoning_content" | "reasoning_text" | (string & {}) +export const ReasoningField: Schema.Codec = Schema.Union([ + Schema.Literals(["reasoning", "reasoning_content", "reasoning_text"]), + Schema.String, +]).annotate({ identifier: "Model.ReasoningField" }) + +export interface Compatibility extends Schema.Schema.Type {} +export const Compatibility = Schema.Struct({ + reasoningField: ReasoningField.pipe(optional), +}).annotate({ identifier: "Model.Compatibility" }) + export interface Capabilities extends Schema.Schema.Type {} export const Capabilities = Schema.Struct({ tools: Schema.Boolean, @@ -75,6 +86,7 @@ export const Info = Schema.Struct({ providerID: Provider.ID, family: Family.pipe(optional), name: Schema.String, + compatibility: Compatibility.pipe(optional), package: Provider.Package.pipe(optional), ...Provider.Overlays, capabilities: Capabilities, diff --git a/packages/schema/test/model.test.ts b/packages/schema/test/model.test.ts index 1dedf0869816..7a11eb842419 100644 --- a/packages/schema/test/model.test.ts +++ b/packages/schema/test/model.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test" +import { Schema } from "effect" import { Model } from "../src/model.js" describe("Model.Ref", () => { @@ -20,3 +21,12 @@ describe("Model.Ref", () => { expect(() => Model.Ref.parse("openai/gpt-5#high#extra")).toThrow() }) }) + +describe("Model.ReasoningField", () => { + test("accepts suggested and custom fields", () => { + const decode = Schema.decodeUnknownSync(Model.ReasoningField) + + for (const field of ["reasoning", "reasoning_content", "reasoning_text", "vendor_reasoning"]) + expect(decode(field)).toBe(field) + }) +}) From ca6da05d07b69c16bbd517ece2150a33d74b7be2 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Wed, 22 Jul 2026 07:03:02 +0200 Subject: [PATCH 010/150] fix(tui): show skill name in mini tool output (#38250) --- packages/tui/src/mini/tool.ts | 7 ++++-- packages/tui/test/mini/tool.test.ts | 34 ++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/tui/src/mini/tool.ts b/packages/tui/src/mini/tool.ts index dd41193526ba..a51d0d79ae14 100644 --- a/packages/tui/src/mini/tool.ts +++ b/packages/tui/src/mini/tool.ts @@ -49,6 +49,7 @@ type PatchFile = { } type ToolInput = ToolDict & { + id?: string path?: string pattern?: string url?: string @@ -67,6 +68,7 @@ type ToolInput = ToolDict & { } type ToolMetadata = ToolDict & { + name?: string count?: number matches?: number diff?: string @@ -416,9 +418,10 @@ function runTask(p: ToolProps): ToolInline { } function runSkill(p: ToolProps): ToolInline { + const name = p.metadata.name ?? p.input.id ?? "" return { icon: "→", - title: `Skill "${p.input.name ?? ""}"`, + title: `Skill "${name}"`, } } @@ -819,7 +822,7 @@ function scrollLspStart(p: ToolProps): string { } function scrollSkillStart(p: ToolProps): string { - return `→ Skill "${p.input.name ?? ""}"` + return `→ Skill "${p.metadata.name ?? p.input.id ?? ""}"` } function scrollGlobStart(p: ToolProps): string { diff --git a/packages/tui/test/mini/tool.test.ts b/packages/tui/test/mini/tool.test.ts index 5f9d1860f68a..4ebd16c8168a 100644 --- a/packages/tui/test/mini/tool.test.ts +++ b/packages/tui/test/mini/tool.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { normalizeTool, toolOutputText, toolPath } from "../../src/mini/tool" +import { normalizeTool, toolInlineInfo, toolOutputText, toolPath, toolScroll } from "../../src/mini/tool" describe("Mini tool presentation", () => { test("uses V2 shell output without the model-facing status", () => { @@ -73,6 +73,38 @@ describe("Mini tool presentation", () => { ).toMatchObject({ name: "subagent", state: { input: { agent: "explore" } } }) }) + test("renders the skill name from structured metadata with the input id as fallback", () => { + const skill = (structured: { name?: string }) => ({ + type: "tool" as const, + id: "call-skill", + name: "skill", + state: { + status: "completed" as const, + input: { id: "tigerstyle" }, + structured, + content: [], + }, + time: { created: 1, ran: 1, completed: 2 }, + }) + + expect(toolInlineInfo(skill({ name: "effect" })).title).toBe('Skill "effect"') + expect(toolInlineInfo(skill({})).title).toBe('Skill "tigerstyle"') + expect( + toolScroll("start", { + directory: "/work/project", + raw: "", + name: "skill", + input: { id: "tigerstyle" }, + meta: { name: "effect" }, + state: {}, + status: "completed", + error: "", + output: "", + time: {}, + }), + ).toBe('→ Skill "effect"') + }) + test("keeps segment-safe contained tool paths relative", () => { expect(toolPath("..cache/result.txt", { directory: "/work/project" })).toBe("..cache/result.txt") expect(toolPath("../shared/result.txt", { directory: "/work/project" })).toBe("/work/shared/result.txt") From 691a7d93c8a0657a8d2339f80b685fef72045bd1 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Wed, 22 Jul 2026 07:03:46 +0200 Subject: [PATCH 011/150] feat(tui): add compact command to mini (#38251) --- packages/tui/src/mini/footer.command.tsx | 10 +++- packages/tui/src/mini/footer.prompt.tsx | 6 +++ packages/tui/src/mini/prompt.shared.ts | 5 ++ packages/tui/src/mini/runtime.queue.ts | 31 +++++++++-- packages/tui/src/mini/runtime.ts | 3 ++ packages/tui/test/mini/footer.view.test.tsx | 3 +- packages/tui/test/mini/runtime.queue.test.ts | 57 ++++++++++++++++++++ 7 files changed, 109 insertions(+), 6 deletions(-) diff --git a/packages/tui/src/mini/footer.command.tsx b/packages/tui/src/mini/footer.command.tsx index 5b3998413de2..4c7e5aef13cc 100644 --- a/packages/tui/src/mini/footer.command.tsx +++ b/packages/tui/src/mini/footer.command.tsx @@ -374,7 +374,7 @@ export function RunCommandMenuBody(props: { const skills = createMemo(() => (props.commands() ?? []).filter((item) => item.source === "skill")) const activeSubagentCount = createMemo(() => props.subagents().filter((item) => item.status === "running").length) const entries = createMemo(() => { - const builtins = ["editor", "new", "settings"] + const builtins = ["compact", "editor", "new", "settings"] const session: CommandEntry[] = [ { action: "editor", @@ -404,6 +404,14 @@ export function RunCommandMenuBody(props: { }, ] : []), + { + action: "slash", + category: "Session", + name: "compact", + display: "Compact session", + footer: "/compact", + keywords: "compact summarize session context", + }, { action: "slash", category: "Session", diff --git a/packages/tui/src/mini/footer.prompt.tsx b/packages/tui/src/mini/footer.prompt.tsx index 6d0ece4e14f7..544d76007d20 100644 --- a/packages/tui/src/mini/footer.prompt.tsx +++ b/packages/tui/src/mini/footer.prompt.tsx @@ -395,6 +395,12 @@ export function createPromptState(input: PromptInput): PromptState { description: "configure Mini transcript output", } satisfies SlashOption, { kind: "slash", name: "new", display: "/new", description: "start a new session" } satisfies SlashOption, + { + kind: "slash", + name: "compact", + display: "/compact", + description: "summarize the session to reduce context usage", + } satisfies SlashOption, { kind: "slash", name: "exit", display: "/exit", description: "close OpenCode" } satisfies SlashOption, ] const hidden = new Set(builtins.map((item) => item.name)) diff --git a/packages/tui/src/mini/prompt.shared.ts b/packages/tui/src/mini/prompt.shared.ts index 64924e0daed3..be1e24db8671 100644 --- a/packages/tui/src/mini/prompt.shared.ts +++ b/packages/tui/src/mini/prompt.shared.ts @@ -53,6 +53,11 @@ export function isNewCommand(input: string): boolean { return input.trim().toLowerCase() === "/new" } +export function isCompactCommand(input: string): boolean { + const text = input.trim().toLowerCase() + return text === "/compact" || text === "/summarize" +} + export function createPromptHistory(items?: RunPrompt[]): PromptHistoryState { const list = (items ?? []).filter((item) => item.text.trim().length > 0).map(promptCopy) const next: RunPrompt[] = [] diff --git a/packages/tui/src/mini/runtime.queue.ts b/packages/tui/src/mini/runtime.queue.ts index 8141f135ea34..74a399416e2b 100644 --- a/packages/tui/src/mini/runtime.queue.ts +++ b/packages/tui/src/mini/runtime.queue.ts @@ -4,13 +4,13 @@ // operations drain one at a time. Ordinary prompts submitted during an active // ordinary turn are admitted immediately to the server's durable queue. // -// The queue also handles /exit, /quit, and /new commands, empty-prompt rejection, +// The queue also handles local session commands, empty-prompt rejection, // and tracks per-turn wall-clock duration for the footer status line. // // Resolves when the footer closes and all in-flight work finishes. import { SessionMessage } from "@opencode-ai/schema/session-message" import { Locale } from "../util/locale" -import { isExitCommand, isNewCommand } from "./prompt.shared" +import { isCompactCommand, isExitCommand, isNewCommand } from "./prompt.shared" import type { FooterApi, FooterEvent, RunPrompt } from "./types" type Trace = { @@ -24,6 +24,7 @@ export type QueueInput = { onSend?: (prompt: RunPrompt, delivery: "steer" | "queue") => void onAdmissionError?: (prompt: RunPrompt, error: unknown) => void | Promise onNewSession?: () => void | Promise + onCompact?: () => void | Promise admit: (prompt: RunPrompt, signal: AbortSignal) => Promise settle: () => Promise run: (prompt: RunPrompt, signal: AbortSignal, admitted: () => void) => Promise @@ -126,6 +127,24 @@ export async function runPromptQueue(input: QueueInput): Promise { continue } + if (prompt.mode !== "shell" && isCompactCommand(prompt.text)) { + emit( + { + type: "stream.patch", + patch: { + phase: "running", + status: "compacting session", + }, + }, + { + phase: "running", + status: "compacting session", + }, + ) + await input.onCompact?.() + continue + } + const sent = prompt.mode === "shell" ? prompt @@ -251,7 +270,11 @@ export async function runPromptQueue(input: QueueInput): Promise { active.mode !== "shell" && prompt.mode !== "shell" && prompt.command?.source !== "skill" && - !isNewCommand(prompt.text) + !isNewCommand(prompt.text) && + !isCompactCommand(prompt.text) && + !state.queue.some( + (item) => item.mode !== "shell" && (isNewCommand(item.text) || isCompactCommand(item.text)), + ) ) { const sent = { ...prompt, messageID: SessionMessage.ID.create() } const admission = state.admission @@ -265,7 +288,7 @@ export async function runPromptQueue(input: QueueInput): Promise { } state.queue.push(prompt) - if (prompt.mode !== "shell" && isNewCommand(prompt.text)) { + if (prompt.mode !== "shell" && (isNewCommand(prompt.text) || isCompactCommand(prompt.text))) { drain() return } diff --git a/packages/tui/src/mini/runtime.ts b/packages/tui/src/mini/runtime.ts index 34bd286259fd..fac1efda117a 100644 --- a/packages/tui/src/mini/runtime.ts +++ b/packages/tui/src/mini/runtime.ts @@ -876,6 +876,9 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep }) }, onAdmissionError: renderPromptError, + onCompact: async () => { + await state.sdk.session.compact({ sessionID: state.sessionID }, formRequestOptions(state.location)) + }, settle: async () => { const next = await ensureStream() await next.handle.waitForIdle() diff --git a/packages/tui/test/mini/footer.view.test.tsx b/packages/tui/test/mini/footer.view.test.tsx index adbb4050ad46..a4a5936dba46 100644 --- a/packages/tui/test/mini/footer.view.test.tsx +++ b/packages/tui/test/mini/footer.view.test.tsx @@ -410,7 +410,8 @@ test("direct command panel renders grouped command palette", async () => { expect(frame).toContain("Open editor") expect(frame).toContain("/editor") expect(frame).toContain("Show status") - expect(frame).toContain("Switch model") + expect(frame).toContain("Compact session") + expect(frame).toContain("/compact") expect(frame).toContain("Skills") expect(frame).toContain("/skills") expect(frame.match(/\bAgent\b/g)?.length).toBe(1) diff --git a/packages/tui/test/mini/runtime.queue.test.ts b/packages/tui/test/mini/runtime.queue.test.ts index 78ce2913c5a1..c7fe59fba807 100644 --- a/packages/tui/test/mini/runtime.queue.test.ts +++ b/packages/tui/test/mini/runtime.queue.test.ts @@ -80,6 +80,63 @@ describe("run runtime queue", () => { ]) }) + test.each(["/compact", "/summarize"])("treats %s as a local compaction command", async (command) => { + const ui = createFooterApiFixture() + const seen: string[] = [] + let compacted = 0 + + const task = runPromptQueue({ + footer: ui.api, + onCompact: async () => { + compacted += 1 + }, + run: async (input) => { + seen.push(input.text) + ui.api.close() + }, + }) + + ui.submit(command) + ui.submit("hello") + await task + + expect(compacted).toBe(1) + expect(seen).toEqual(["hello"]) + expect(ui.commits.map((item) => item.text)).toEqual(["hello"]) + }) + + test("keeps prompts submitted after an in-flight /compact behind the compaction barrier", async () => { + const ui = createFooterApiFixture() + const active = Promise.withResolvers() + const order: string[] = [] + + const task = runPromptQueue({ + footer: ui.api, + onCompact: async () => { + order.push("compact") + }, + admit: async (prompt) => { + order.push(`admit:${prompt.text}`) + }, + run: async (prompt) => { + order.push(`run:${prompt.text}`) + if (prompt.text === "first") await active.promise + if (prompt.text === "later") ui.api.close() + }, + }) + + ui.submit("first") + await Promise.resolve() + ui.submit("/compact") + ui.submit("later") + await Promise.resolve() + expect(order).toEqual(["run:first"]) + + active.resolve() + await task + expect(order).toEqual(["run:first", "compact", "run:later"]) + }) + test("shell mode submits /exit as a shell command", async () => { const ui = createFooterApiFixture() const seen: RunPrompt[] = [] From 8bb1cfaa3b28e0c7ff1244419926f8a12d51539f Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Wed, 22 Jul 2026 10:32:52 +0200 Subject: [PATCH 012/150] fix: defer catalog validation to session execution (#38258) --- packages/cli/src/mini.ts | 59 +------- packages/cli/src/run/noninteractive.ts | 29 +++- packages/cli/src/run/run.ts | 51 +------ packages/cli/src/services/catalog.ts | 43 ------ .../cli/test/drive/mini-interactive.drive.mjs | 143 +++++++++++++++--- packages/cli/test/mini.test.ts | 37 +++-- packages/cli/test/run/noninteractive.test.ts | 30 +++- packages/protocol/src/groups/model.ts | 3 +- packages/tui/src/mini/catalog.shared.ts | 61 -------- packages/tui/src/mini/footer.view.tsx | 2 +- packages/tui/src/mini/runtime.lifecycle.ts | 2 +- packages/tui/src/mini/runtime.ts | 41 ++--- packages/tui/test/mini/catalog.shared.test.ts | 22 +-- packages/tui/test/mini/footer.view.test.tsx | 10 ++ 14 files changed, 229 insertions(+), 304 deletions(-) delete mode 100644 packages/cli/src/services/catalog.ts diff --git a/packages/cli/src/mini.ts b/packages/cli/src/mini.ts index ff578099d754..049f08a24ea7 100644 --- a/packages/cli/src/mini.ts +++ b/packages/cli/src/mini.ts @@ -2,7 +2,6 @@ import { Service, type Endpoint } from "@opencode-ai/client/effect/service" import { ClientError, OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise" import type { MiniFrontendInput } from "@opencode-ai/tui/mini" import { setTimeout } from "node:timers/promises" -import { waitForCatalogReady } from "./services/catalog" import { readStdin } from "./util/io" import { createMiniHost, INTERACTIVE_INPUT_ERROR, usingInteractiveStdin } from "./mini-host" import { parseSessionTargetModel, resolveSessionTarget, type SessionTargetPreparation } from "./session-target" @@ -214,63 +213,7 @@ function parseModel(value?: string) { } function prepareTarget(requestedAgent?: string): SessionTargetPreparation { - return async (input) => { - if (input.model) - await waitForCatalogReady({ - sdk: input.client, - directory: input.location.directory, - workspace: input.location.workspaceID, - model: { providerID: input.model.providerID, modelID: input.model.id }, - signal: input.signal, - }) - return { - model: input.model, - agent: requestedAgent - ? await validateAgent( - input.client, - input.location.directory, - input.location.workspaceID, - requestedAgent, - input.signal, - ) - : input.agent, - } - } -} - -async function validateAgent( - sdk: OpenCodeClient, - directory: string, - workspace: string | undefined, - name?: string, - signal?: AbortSignal, -) { - if (!name) return - const deadline = Date.now() + 5_000 - let agents: Awaited> | undefined - while (Date.now() < deadline && !signal?.aborted) { - agents = await sdk.agent.list({ location: { directory, workspace } }, { signal }).catch((error) => { - if (signal && error instanceof ClientError && error.reason === "Transport") throw error - return undefined - }) - const agent = agents?.data.find((item) => item.id === name) - if (agent?.mode === "subagent") { - warning(`agent "${name}" is a subagent, not a primary agent. Falling back to default agent`) - return - } - if (agent) return name - await setTimeout(25, undefined, { signal }).catch(() => {}) - } - if (signal?.aborted) return - if (!agents) { - warning("failed to list agents. Falling back to default agent") - return - } - warning(`agent "${name}" not found. Falling back to default agent`) -} - -function warning(message: string) { - process.stderr.write(`\x1b[93m\x1b[1m!\x1b[0m ${message}\n`) + return async (input) => ({ model: input.model, agent: requestedAgent ?? input.agent }) } function fail(message: string): never { diff --git a/packages/cli/src/run/noninteractive.ts b/packages/cli/src/run/noninteractive.ts index 102294f64b0b..5ea239ca1b6c 100644 --- a/packages/cli/src/run/noninteractive.ts +++ b/packages/cli/src/run/noninteractive.ts @@ -211,10 +211,17 @@ export async function runNonInteractivePrompt(input: Input) { } if (!promoted && event.type === "session.execution.failed") { prePromotionError = event.data.error + if (finalizing) return continue } + if ( + !promoted && + finalizing && + (event.type === "session.execution.succeeded" || event.type === "session.execution.interrupted") + ) + return if (!promoted) continue - if (finalizing) continue + if (finalizing && !event.type.startsWith("session.execution.")) continue if (event.type === "session.step.started") { const part = { @@ -618,7 +625,10 @@ export async function runNonInteractivePrompt(input: Input) { if (!emit("error", timestamp, { error: message.error })) UI.error(message.error.message) } } - return projected.found + return { + found: projected.found, + responded: projected.messages.some((message) => message.type === "assistant"), + } } const interrupt = () => { @@ -708,9 +718,18 @@ export async function runNonInteractivePrompt(input: Input) { const waiting = input.client.session.wait({ sessionID: input.sessionID }) await Promise.race([waiting, completed.then(() => waiting)]) finalizing = true - controller.abort() - const found = await reconcile() - if (!found && !interrupted && !permissionRejected && !formCancelled && !emittedError) { + const projected = await reconcile() + if ( + !projected.responded && + !interrupted && + !permissionRejected && + !formCancelled && + !emittedError && + !prePromotionError + ) { + await completed + } + if (!projected.found && !interrupted && !permissionRejected && !formCancelled && !emittedError) { const error = prePromotionError ?? { type: "unknown", message: "Prompt was not promoted" } emittedError = true process.exitCode = 1 diff --git a/packages/cli/src/run/run.ts b/packages/cli/src/run/run.ts index 496bdfa86d0d..4342bb329f09 100644 --- a/packages/cli/src/run/run.ts +++ b/packages/cli/src/run/run.ts @@ -5,7 +5,6 @@ import { open } from "node:fs/promises" import path from "node:path" import { readStdin } from "../util/io" import { ServerConnection } from "../services/server-connection" -import { waitForCatalogReady } from "../services/catalog" import { parseSessionTargetModel, resolveSessionTarget } from "../session-target" import { toolInlineInfo } from "@opencode-ai/tui/mini/tool" import { runNonInteractivePrompt } from "./noninteractive" @@ -95,9 +94,11 @@ async function execute(input: RunCommandInput, prepared: Prepared, endpoint: End prepare: async (next) => { const selected = next.model ?? - (await client.model - .default({ location: { directory: next.location.directory, workspace: next.location.workspaceID } }) - .then((result) => result.data)) + (options.variant + ? await client.model + .default({ location: { directory: next.location.directory, workspace: next.location.workspaceID } }) + .then((result) => result.data) + : undefined) const model = selected ? { providerID: selected.providerID, @@ -107,25 +108,7 @@ async function execute(input: RunCommandInput, prepared: Prepared, endpoint: End : undefined if ((options.variant ?? explicit?.variant) && !model) throw new RunTargetError("Cannot select a variant before selecting a model", next.session?.id) - if (model) { - await waitForCatalogReady({ - sdk: client, - directory: next.location.directory, - workspace: next.location.workspaceID, - model: { providerID: model.providerID, modelID: model.id }, - }) - const available = await client.model.list({ - location: { directory: next.location.directory, workspace: next.location.workspaceID }, - }) - if (!available.data.some((item) => item.providerID === model.providerID && item.id === model.id)) - throw new RunTargetError(`Model unavailable: ${model.providerID}/${model.id}`, next.session?.id) - } - return { - model, - agent: input.agent - ? await validateAgent(client, next.location.directory, next.location.workspaceID, input.agent) - : next.agent, - } + return { model, agent: next.agent } }, }).catch((error) => { if (!(error instanceof RunTargetError)) throw error @@ -190,28 +173,6 @@ export function parseRunModel(value?: string) { } } -async function validateAgent(client: OpenCodeClient, directory: string, workspace: string | undefined, name?: string) { - if (!name) return - const agents = await client.agent - .list({ location: { directory, workspace } }) - .then((result) => result.data) - .catch(() => undefined) - if (!agents) { - warning("failed to list agents. Falling back to default agent") - return - } - const agent = agents.find((item) => item.id === name) - if (!agent) { - warning(`agent "${name}" not found. Falling back to default agent`) - return - } - if (agent.mode === "subagent") { - warning(`agent "${name}" is a subagent, not a primary agent. Falling back to default agent`) - return - } - return name -} - async function prepareFile(input: string, directory: string, options: ExecutionOptions): Promise { const file = path.resolve(directory, input) const handle = await open(file, "r").catch(() => fail(`File not found: ${input}`)) diff --git a/packages/cli/src/services/catalog.ts b/packages/cli/src/services/catalog.ts deleted file mode 100644 index ddf8252c31df..000000000000 --- a/packages/cli/src/services/catalog.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { ClientError, type OpenCodeClient } from "@opencode-ai/client/promise" - -// Location plugins initialize asynchronously, so explicit model selection must -// wait for that exact model before prompt admission. The execution path owns -// the authoritative error if readiness times out. -export async function waitForCatalogReady(input: { - sdk: OpenCodeClient - directory: string - workspace?: string - model: { providerID: string; modelID: string } - timeoutMs?: number - signal?: AbortSignal -}) { - const deadline = Date.now() + (input.timeoutMs ?? 5_000) - while (Date.now() < deadline && !input.signal?.aborted) { - const models = await input.sdk.model - .list( - { location: { directory: input.directory, workspace: input.workspace } }, - { signal: input.signal }, - ) - .then((result) => result.data) - .catch((error) => { - if (input.signal && error instanceof ClientError && error.reason === "Transport") throw error - return undefined - }) - if (models?.some((model) => model.providerID === input.model.providerID && model.id === input.model.modelID)) return - await wait(25, input.signal) - } -} - -function wait(delay: number, signal?: AbortSignal) { - if (!signal) return new Promise((resolve) => setTimeout(resolve, delay)) - if (signal.aborted) return Promise.resolve() - return new Promise((resolve) => { - const timer = setTimeout(done, delay) - signal.addEventListener("abort", done, { once: true }) - function done() { - clearTimeout(timer) - signal?.removeEventListener("abort", done) - resolve() - } - }) -} diff --git a/packages/cli/test/drive/mini-interactive.drive.mjs b/packages/cli/test/drive/mini-interactive.drive.mjs index e745c481b417..02d0772ef664 100644 --- a/packages/cli/test/drive/mini-interactive.drive.mjs +++ b/packages/cli/test/drive/mini-interactive.drive.mjs @@ -16,7 +16,30 @@ export default defineScript({ const preload = Bun.resolveSync("@opentui/solid/preload", path.join(root, "packages/cli")) const session = `mini-stage2-${process.pid}` const snapshots = path.join(artifacts, "mini-stage2") - yield* Effect.promise(() => mkdir(snapshots, { recursive: true })) + const explicitDirectory = path.join(artifacts, "explicit-model") + yield* Effect.promise(() => Promise.all([snapshots, explicitDirectory].map((dir) => mkdir(dir, { recursive: true })))) + /** @param {string} directory @param {string | undefined} model */ + const mini = (directory, model) => [ + "env", + `PWD=${directory}`, + `OPENCODE_PASSWORD=${registration.password}`, + `OPENCODE_CONFIG_DIR=${path.join(artifacts, "files/.opencode")}`, + `OPENCODE_TEST_HOME=${artifacts}`, + `XDG_CACHE_HOME=${path.join(artifacts, "home/.cache")}`, + `XDG_CONFIG_HOME=${path.join(artifacts, "home/.config")}`, + `XDG_DATA_HOME=${path.join(artifacts, "logs")}`, + `XDG_STATE_HOME=${path.join(artifacts, "home/.local/state")}`, + "OPENCODE_DISABLE_AUTOUPDATE=1", + "OPENCODE_DIRECT_TRACE=1", + process.execPath, + "--conditions=browser", + `--preload=${preload}`, + path.join(root, "packages/cli/src/index.ts"), + "mini", + "--server", + registration.url, + ...(model ? ["--model", model] : []), + ] yield* llm.queue( Llm.toolCall({ @@ -42,26 +65,7 @@ export default defineScript({ "-y", "30", "--", - "env", - `PWD=${path.join(artifacts, "files")}`, - `OPENCODE_PASSWORD=${registration.password}`, - `OPENCODE_CONFIG_DIR=${path.join(artifacts, "files/.opencode")}`, - `OPENCODE_TEST_HOME=${artifacts}`, - `XDG_CACHE_HOME=${path.join(artifacts, "home/.cache")}`, - `XDG_CONFIG_HOME=${path.join(artifacts, "home/.config")}`, - `XDG_DATA_HOME=${path.join(artifacts, "logs")}`, - `XDG_STATE_HOME=${path.join(artifacts, "home/.local/state")}`, - "OPENCODE_DISABLE_AUTOUPDATE=1", - "OPENCODE_DIRECT_TRACE=1", - process.execPath, - "--conditions=browser", - `--preload=${preload}`, - path.join(root, "packages/cli/src/index.ts"), - "mini", - "--server", - registration.url, - "--model", - "simulation/gpt-sim-model", + ...mini(path.join(artifacts, "files"), undefined), ]), ), ) @@ -72,7 +76,16 @@ export default defineScript({ if (first.includes("drive mini response complete")) throw new Error("response rendered before prompt submission") - yield* Effect.promise(() => waitForPane(session, "Simulated Model", 15_000)) + yield* Effect.promise(() => waitForPane(session, "Default model", 15_000)) + yield* Effect.promise(() => tmux(["send-keys", "-t", session, "C-p"])) + yield* Effect.promise(() => waitForVisiblePane(session, "Commands")) + yield* Effect.promise(() => tmux(["send-keys", "-t", session, "-l", "model"])) + yield* Effect.promise(() => waitForVisiblePane(session, "Switch model")) + yield* Effect.promise(() => tmux(["send-keys", "-H", "-t", session, "0d"])) + yield* Effect.promise(() => waitForVisiblePane(session, "Select model")) + yield* Effect.promise(() => waitForVisiblePane(session, "Simulated Model", 15_000)) + yield* Effect.promise(() => tmux(["send-keys", "-t", session, "Escape"])) + yield* Effect.promise(() => waitForVisiblePane(session, "Ask anything...")) yield* Effect.promise(() => tmux(["send-keys", "-t", session, "-l", "exercise the mini frontend"])) yield* Effect.sleep(100) yield* Effect.promise(() => tmux(["send-keys", "-H", "-t", session, "0d"])) @@ -134,7 +147,7 @@ export default defineScript({ yield* Effect.promise(() => tmux(["send-keys", "-H", "-t", session, "0d"])) yield* Effect.promise(() => waitForPane(session, "$ sleep 10")) yield* Effect.promise(() => tmux(["send-keys", "-t", session, "Escape"])) - const armed = yield* Effect.promise(() => waitForPane(session, "again to interrupt")) + const armed = yield* Effect.promise(() => waitForPane(session, "esc again")) yield* Effect.promise(() => Bun.write(path.join(snapshots, "04-interrupt-armed.txt"), armed)) yield* Effect.promise(() => tmux(["send-keys", "-t", session, "Escape"])) const interrupted = yield* Effect.promise(() => waitForPane(session, "Step interrupted", 10_000)) @@ -144,7 +157,7 @@ export default defineScript({ if (!(await paneAlive(session))) throw new Error("Mini exited while interrupting an active turn") }) yield* Effect.promise(() => tmux(["send-keys", "-t", session, "C-c"])) - yield* Effect.promise(() => waitForPane(session, "Press ctrl+c again to exit")) + yield* Effect.promise(() => waitForPane(session, "EXIT Press ctrl+")) yield* Effect.promise(() => tmux(["send-keys", "-t", session, "C-c"])) yield* Effect.promise(() => waitForDeadPane(session)) const status = yield* Effect.promise(() => paneDeadStatus(session)) @@ -153,6 +166,75 @@ export default defineScript({ if (!exited.includes("Continue") || !exited.includes("opencode mini -s")) throw new Error("Mini exit splash was not rendered before teardown") yield* Effect.promise(() => Bun.write(path.join(snapshots, "06-exit-teardown.txt"), exited)) + + yield* Effect.promise(() => tmux(["clear-history", "-t", session])) + yield* Effect.promise(() => + tmux([ + "respawn-pane", + "-k", + "-t", + session, + "--", + ...mini(explicitDirectory, "simulation/gpt-sim-model"), + ]), + ) + const explicitModel = yield* Effect.promise(() => waitForPane(session, "Simulated Model", 15_000)) + yield* Effect.promise(() => Bun.write(path.join(snapshots, "07-explicit-model.txt"), explicitModel)) + yield* Effect.promise(() => tmux(["send-keys", "-t", session, "C-c"])) + yield* Effect.promise(() => waitForPane(session, "EXIT Press ctrl+")) + yield* Effect.promise(() => tmux(["send-keys", "-t", session, "C-c"])) + yield* Effect.promise(() => waitForDeadPane(session)) + if ((yield* Effect.promise(() => paneDeadStatus(session))) !== 0) + throw new Error("Explicit-model Mini did not exit cleanly") + + yield* Effect.promise(async () => { + for (const failure of [ + { + args: ["--model", "simulation/definitely-missing"], + capture: "08-unavailable-model.txt", + expected: "Model unavailable: simulation/definitely-missing", + }, + { + args: ["--agent", "definitely-missing"], + capture: "09-unavailable-agent.txt", + expected: 'Agent not found: "definitely-missing"', + }, + ]) { + const child = Bun.spawn( + [ + process.execPath, + path.join(root, "packages/cli/src/index.ts"), + "run", + "--server", + registration.url, + ...failure.args, + "optimistic selection check", + ], + { + cwd: path.join(root, "packages/cli"), + env: { + ...process.env, + PWD: path.join(artifacts, "files"), + OPENCODE_PASSWORD: registration.password, + OPENCODE_CONFIG_DIR: path.join(artifacts, "files/.opencode"), + OPENCODE_DISABLE_AUTOUPDATE: "1", + }, + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + }, + ) + const [exitCode, stdout, stderr] = await Promise.all([ + child.exited, + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]) + await Bun.write(path.join(snapshots, failure.capture), stdout + stderr) + if (exitCode !== 1) throw new Error(`${failure.expected} run exited with status ${exitCode}`) + if (!stderr.includes(failure.expected)) + throw new Error(`Selection failure was not diagnosed by execution: ${stderr}`) + } + }) }) yield* journey.pipe(Effect.ensuring(Effect.promise(() => tmux(["kill-session", "-t", session], true)))) @@ -188,6 +270,19 @@ function captureVisiblePane(session) { return tmux(["capture-pane", "-p", "-t", session]) } +/** @param {string} session @param {string} text @param {number} [timeout] */ +async function waitForVisiblePane(session, text, timeout = 5_000) { + const deadline = Date.now() + timeout + let last = "" + while (Date.now() < deadline) { + last = await captureVisiblePane(session) + if (last.includes(text)) return last + if (!(await paneAlive(session))) throw new Error(`Mini exited before rendering ${JSON.stringify(text)}:\n${last}`) + await Bun.sleep(50) + } + throw new Error(`Timed out waiting for visible ${JSON.stringify(text)}:\n${last}`) +} + /** @param {string} session */ async function paneAlive(session) { return (await tmux(["display-message", "-p", "-t", session, "#{pane_dead}"], true)).trim() === "0" diff --git a/packages/cli/test/mini.test.ts b/packages/cli/test/mini.test.ts index 3eb418598117..ba2c51ab0450 100644 --- a/packages/cli/test/mini.test.ts +++ b/packages/cli/test/mini.test.ts @@ -138,32 +138,47 @@ describe("mini command", () => { expect(result.stderr).not.toContain("You must provide a message") }) - test("preserves a run failure exit code", async () => { - let modelRequests = 0 + test("passes explicit selections to session creation without catalog preflight", async () => { + const requests: string[] = [] + let session: unknown const server = Bun.serve({ port: 0, - fetch(request) { + async fetch(request) { const url = new URL(request.url) + requests.push(url.pathname) if (url.pathname === "/api/health") return Response.json({ healthy: true, version: OPENCODE_VERSION, pid: process.pid }) if (url.pathname === "/api/location") return Response.json({ directory: process.cwd(), project: { id: "global", directory: process.cwd() } }) - if (url.pathname === "/api/model") { - modelRequests++ - return Response.json({ - location: { directory: process.cwd(), project: { id: "global", directory: process.cwd() } }, - data: modelRequests === 1 ? [{ id: "missing", providerID: "definitely" }] : [], - }) + if (url.pathname === "/api/session") { + session = await request.json() + return new Response("boom", { status: 500 }) } return new Response(undefined, { status: 404 }) }, }) try { - const result = await cli(["run", "--server", server.url.toString(), "--model", "definitely/missing", "hi"]) + const result = await cli([ + "run", + "--server", + server.url.toString(), + "--model", + "definitely/missing", + "--agent", + "definitely-missing", + "hi", + ]) expect(result.exitCode).toBe(1) - expect(result.stderr).toContain("Model unavailable: definitely/missing") + expect(result.stderr).toContain("UnexpectedStatus") + expect(session).toMatchObject({ + agent: "definitely-missing", + model: { providerID: "definitely", id: "missing" }, + }) + expect(requests).not.toContain("/api/model") + expect(requests).not.toContain("/api/agent") + expect(requests).not.toContain("/api/location/wait") } finally { server.stop(true) } diff --git a/packages/cli/test/run/noninteractive.test.ts b/packages/cli/test/run/noninteractive.test.ts index f27869a991d5..58dea74a93aa 100644 --- a/packages/cli/test/run/noninteractive.test.ts +++ b/packages/cli/test/run/noninteractive.test.ts @@ -169,6 +169,7 @@ async function run(input: { renderToolError?: (part: SessionMessageAssistantTool) => Promise messages?: (inputID: string) => SessionMessageInfo[] wait?: () => Promise + terminalDelay?: number }) { const sdk = OpenCode.make({ baseUrl: "https://opencode.test" }) const values: V2Event[] = [{ id: "evt_connected", type: "server.connected", data: {} }] @@ -183,7 +184,10 @@ async function run(input: { }) continue } - if (value.type.startsWith("session.execution.")) setTimeout(wait.resolve, 0) + if (value.type.startsWith("session.execution.")) { + if (input.terminalDelay) await Bun.sleep(input.terminalDelay) + setTimeout(wait.resolve, 0) + } yield value } })() @@ -251,7 +255,7 @@ async function capture(input: Parameters[0]) { }) try { await run(input) - return { stdout: stdout.join(""), stderr: stderr.join("") } + return { stdout: stdout.join(""), stderr: stderr.join(""), exitCode: process.exitCode } } finally { process.exitCode = exitCode ?? 0 stdoutWrite.mockRestore() @@ -319,6 +323,26 @@ describe("runNonInteractivePrompt", () => { error: { type: "provider.transport", message: "instructions unavailable" }, }), ]) + expect(output.exitCode).toBe(1) + }) + + test("waits for a terminal failure when idle wins before projection", async () => { + for (const promotedBeforeFailure of [true, false]) { + const output = await capture({ + format: "json", + turn: (messageID) => [ + ...(promotedBeforeFailure ? [prompted(messageID)] : []), + executionFailed("selection unavailable"), + ], + messages: (messageID) => + promotedBeforeFailure ? [{ id: messageID, type: "user", text: "hello", time: { created: 1 } }] : [], + wait: () => Promise.resolve(), + terminalDelay: 10, + }) + + expect(output.exitCode).toBe(1) + expect(output.stdout).toContain("selection unavailable") + } }) test("cancels session and global form blockers and exits on pre-promotion interrupt", async () => { @@ -413,7 +437,7 @@ describe("runNonInteractivePrompt", () => { ], }) - expect(output).toEqual({ stdout: "", stderr: "" }) + expect(output).toEqual({ stdout: "", stderr: "", exitCode: 0 }) }) test("renders native failed tool output before the terminal error", async () => { diff --git a/packages/protocol/src/groups/model.ts b/packages/protocol/src/groups/model.ts index a5970192bb46..c3f6828770e7 100644 --- a/packages/protocol/src/groups/model.ts +++ b/packages/protocol/src/groups/model.ts @@ -17,7 +17,8 @@ export const ModelGroup = HttpApiGroup.make("server.model") OpenApi.annotations({ identifier: "v2.model.list", summary: "List models", - description: "Retrieve available models ordered by release date.", + description: + "Retrieve the current snapshot of available models ordered by release date. The snapshot may precede initial plugin settlement.", }), ), ) diff --git a/packages/tui/src/mini/catalog.shared.ts b/packages/tui/src/mini/catalog.shared.ts index b152a4926f39..941e44d9c529 100644 --- a/packages/tui/src/mini/catalog.shared.ts +++ b/packages/tui/src/mini/catalog.shared.ts @@ -85,67 +85,6 @@ export function runProviders(providers: CurrentProvider[], models: CurrentModel[ return [...grouped.values()] } -export async function waitForDefaultModel(input: { - sdk: OpenCodeClient - location: LocationRef - timeoutMs?: number - requestTimeoutMs?: number - active?: () => boolean - signal?: AbortSignal -}): Promise<{ providerID: string; modelID: string } | undefined> { - const deadline = Date.now() + (input.timeoutMs ?? 5_000) - while (Date.now() < deadline && !input.signal?.aborted && (input.active?.() ?? true)) { - const controller = new AbortController() - const timeout = setTimeout( - () => controller.abort(), - Math.min(input.requestTimeoutMs ?? 1_000, Math.max(1, deadline - Date.now())), - ) - const abort = () => controller.abort() - input.signal?.addEventListener("abort", abort, { once: true }) - const model = await abortable( - input.sdk.model - .default(location(input.location), { signal: controller.signal }) - .then((result) => result.data) - .catch(() => undefined), - controller.signal, - ).finally(() => { - clearTimeout(timeout) - input.signal?.removeEventListener("abort", abort) - }) - if (model) return { providerID: model.providerID, modelID: model.id } - await wait(25, input.signal) - } -} - -function abortable(task: Promise, signal: AbortSignal): Promise { - if (signal.aborted) return Promise.resolve(undefined) - return new Promise((resolve) => { - const abort = () => { - signal.removeEventListener("abort", abort) - resolve(undefined) - } - signal.addEventListener("abort", abort, { once: true }) - void task.then((value) => { - signal.removeEventListener("abort", abort) - resolve(value) - }) - }) -} - -function wait(delay: number, signal?: AbortSignal) { - if (!signal) return new Promise((resolve) => setTimeout(resolve, delay)) - if (signal.aborted) return Promise.resolve() - return new Promise((resolve) => { - const timer = setTimeout(done, delay) - signal.addEventListener("abort", done, { once: true }) - function done() { - clearTimeout(timer) - signal?.removeEventListener("abort", done) - resolve() - } - }) -} - export async function loadRunAgents(sdk: OpenCodeClient, ref: LocationRef, signal?: AbortSignal): Promise { const result = await sdk.agent.list(location(ref), ...requestOptions(signal)) return result.data.map(runAgent) diff --git a/packages/tui/src/mini/footer.view.tsx b/packages/tui/src/mini/footer.view.tsx index c1cf4cc89533..72e4cf68ca06 100644 --- a/packages/tui/src/mini/footer.view.tsx +++ b/packages/tui/src/mini/footer.view.tsx @@ -425,7 +425,7 @@ export function RunFooterView(props: RunFooterViewProps) { return props.mono ? usage().replaceAll(" · ", " - ") : usage() }) const modelStatus = createMemo(() => { - const current = model() + const current = model() ?? props.state().model.trim() if (!footerDetails() || !prompt() || !responsive().statusline.showModel || !current) return return { model: current, diff --git a/packages/tui/src/mini/runtime.lifecycle.ts b/packages/tui/src/mini/runtime.lifecycle.ts index 280cb2851785..2baacfe46f10 100644 --- a/packages/tui/src/mini/runtime.lifecycle.ts +++ b/packages/tui/src/mini/runtime.lifecycle.ts @@ -126,7 +126,7 @@ function footerLabels(input: Pick): Foo const agentLabel = Locale.titlecase(input.agent ?? "build") return { agentLabel, - modelLabel: input.model ? formatModelLabel(input.model, input.variant) : "", + modelLabel: input.model ? formatModelLabel(input.model, input.variant) : "Default model", } } diff --git a/packages/tui/src/mini/runtime.ts b/packages/tui/src/mini/runtime.ts index fac1efda117a..9f56d270b004 100644 --- a/packages/tui/src/mini/runtime.ts +++ b/packages/tui/src/mini/runtime.ts @@ -11,7 +11,7 @@ import { SessionMessage } from "@opencode-ai/schema/session-message" import type { LocationRef } from "@opencode-ai/client/promise" import type { Config } from "../config" -import { loadRunAgents, loadRunCommands, loadRunReferences, waitForDefaultModel } from "./catalog.shared" +import { loadRunAgents, loadRunCommands, loadRunReferences } from "./catalog.shared" import { resolveMiniSettings, resolveModelInfo, @@ -492,39 +492,20 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep const signal = AbortSignal.any([runtimeController.signal, controller.signal]) modelAttempt = controller try { - if (selected) { - const info = await abortable(resolveModelInfo(sdk, state.location, signal), signal) - if ( - !info || - !currentModelLoad(generation, sdk) || - state.model?.providerID !== selected.providerID || - state.model.modelID !== selected.modelID - ) - return - applyModelInfo(info, session.variant, { sdk, generation, signal }, true, savedVariant) + const info = await abortable(resolveModelInfo(sdk, state.location, signal), signal) + if ( + !info || + !currentModelLoad(generation, sdk) || + (selected && + (state.model?.providerID !== selected.providerID || state.model.modelID !== selected.modelID)) + ) return - } - - const model = await waitForDefaultModel({ - sdk, - location: state.location, - active: () => currentModelLoad(generation, sdk), - signal, - }) - if (!currentModelLoad(generation, sdk)) return - const [fallbackSavedVariant, info] = await Promise.all([ - input.host.preferences.resolveVariant(model), - abortable(resolveModelInfo(sdk, state.location, signal), signal), - ]) - if (!info || !currentModelLoad(generation, sdk)) return - if (model && !state.model) state.model = model - const boot = !!model && state.model?.providerID === model.providerID && state.model.modelID === model.modelID applyModelInfo( info, - boot ? session.variant : state.activeVariant, + selected ? session.variant : state.activeVariant, { sdk, generation, signal }, - boot, - fallbackSavedVariant, + !!selected, + savedVariant, ) } finally { if (modelAttempt === controller) modelAttempt = undefined diff --git a/packages/tui/test/mini/catalog.shared.test.ts b/packages/tui/test/mini/catalog.shared.test.ts index 334454c3aa4d..fbfdf40089fc 100644 --- a/packages/tui/test/mini/catalog.shared.test.ts +++ b/packages/tui/test/mini/catalog.shared.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, mock, spyOn, test } from "bun:test" import { OpenCode } from "@opencode-ai/client/promise" -import { loadRunReferences, runProviders, waitForDefaultModel } from "../../src/mini/catalog.shared" +import { loadRunReferences, runProviders } from "../../src/mini/catalog.shared" import { catalogModel, catalogProvider } from "./fixture/catalog" afterEach(() => { @@ -8,26 +8,6 @@ afterEach(() => { }) describe("run catalog shared", () => { - test("resolves the catalog-selected model for the footer", async () => { - const client = OpenCode.make({ baseUrl: "https://opencode.test" }) - const selected = spyOn(client.model, "default").mockImplementation( - () => - Promise.resolve({ - location: { directory: "/tmp", project: { id: "proj_1", directory: "/tmp" } }, - data: { id: "gpt-5", providerID: "openai" }, - }) as never, - ) - - await expect(waitForDefaultModel({ sdk: client, location: { directory: "/tmp" } })).resolves.toEqual({ - providerID: "openai", - modelID: "gpt-5", - }) - expect(selected).toHaveBeenCalledWith( - { location: { directory: "/tmp", workspace: undefined } }, - { signal: expect.any(AbortSignal) }, - ) - }) - test("loads visible project references from the current reference catalog", async () => { const client = OpenCode.make({ baseUrl: "https://opencode.test" }) const list = spyOn(client.reference, "list").mockImplementation( diff --git a/packages/tui/test/mini/footer.view.test.tsx b/packages/tui/test/mini/footer.view.test.tsx index a4a5936dba46..aa71307625f8 100644 --- a/packages/tui/test/mini/footer.view.test.tsx +++ b/packages/tui/test/mini/footer.view.test.tsx @@ -195,6 +195,16 @@ async function renderFooter( } } +test("direct footer shows the generic default model before resolution", async () => { + const app = await renderFooter({ state: { model: "Default model" } }) + try { + await app.renderOnce() + expect(app.captureCharFrame()).toContain("Default model") + } finally { + app.cleanup() + } +}) + test("direct footer preserves a partial multi-field form draft across permission preemption", async () => { const request: FormInfo = { id: "frm_preempted", From 6e826f3e22282a18a004945e53b64ce5ad62d58b Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Wed, 22 Jul 2026 10:34:08 +0200 Subject: [PATCH 013/150] fix(tui): hide project commands from mini palette (#38259) --- packages/tui/src/mini/footer.command.tsx | 32 --------------------- packages/tui/test/mini/footer.view.test.tsx | 12 +++++++- 2 files changed, 11 insertions(+), 33 deletions(-) diff --git a/packages/tui/src/mini/footer.command.tsx b/packages/tui/src/mini/footer.command.tsx index 4c7e5aef13cc..f2edfcbbf0c1 100644 --- a/packages/tui/src/mini/footer.command.tsx +++ b/packages/tui/src/mini/footer.command.tsx @@ -92,18 +92,6 @@ function countLabel(count: number, total: number, query: string) { return `${count}/${total}` } -function categoryRank(category: string) { - if (category === "Project Commands") { - return 0 - } - - if (category === "MCP Commands") { - return 1 - } - - return 2 -} - function subagentStatusLabel(status: FooterSubagentTab["status"]) { if (status === "completed") { return "done" @@ -374,7 +362,6 @@ export function RunCommandMenuBody(props: { const skills = createMemo(() => (props.commands() ?? []).filter((item) => item.source === "skill")) const activeSubagentCount = createMemo(() => props.subagents().filter((item) => item.status === "running").length) const entries = createMemo(() => { - const builtins = ["compact", "editor", "new", "settings"] const session: CommandEntry[] = [ { action: "editor", @@ -473,29 +460,10 @@ export function RunCommandMenuBody(props: { ] : []), ] - const commands = (props.commands() ?? []) - .filter((item) => item.source !== "skill" && !builtins.includes(item.name)) - .map( - (item) => - ({ - action: "slash", - category: item.source === "mcp" ? "MCP Commands" : "Project Commands", - name: item.name, - display: item.name, - footer: `/${item.name}`, - keywords: - item.source === "mcp" - ? `/${item.name} ${item.name} mcp ${item.description ?? ""}` - : `/${item.name} ${item.name} ${item.description ?? ""}`, - }) satisfies CommandEntry, - ) - .sort((a, b) => categoryRank(a.category) - categoryRank(b.category) || a.display.localeCompare(b.display)) - return [ ...session, ...prompt, ...agent, - ...commands, { action: "settings", category: "System", diff --git a/packages/tui/test/mini/footer.view.test.tsx b/packages/tui/test/mini/footer.view.test.tsx index aa71307625f8..93f4c1b03185 100644 --- a/packages/tui/test/mini/footer.view.test.tsx +++ b/packages/tui/test/mini/footer.view.test.tsx @@ -365,7 +365,7 @@ test("run entry content updates when live commit text changes", async () => { } }) -test("direct command panel renders grouped command palette", async () => { +test("direct command panel renders grouped actions without catalog commands", async () => { const [commands] = createSignal([ command({ name: "review", description: "Review code" }), command({ name: "deploy", description: "Deploy prompt", source: "mcp" }), @@ -433,6 +433,16 @@ test("direct command panel renders grouped command palette", async () => { expect(frame).not.toContain("Review code") expect(frame).not.toContain("Commands 8") + await app.mockInput.typeText("review") + await app.renderOnce() + expect(app.captureCharFrame()).toContain("No results found") + + app.mockInput.pressKey("u", { ctrl: true }) + await app.mockInput.typeText("deploy") + await app.renderOnce() + expect(app.captureCharFrame()).toContain("No results found") + + app.mockInput.pressKey("u", { ctrl: true }) await app.mockInput.typeText("status") await app.renderOnce() expect(app.captureCharFrame()).toContain("Show status") From ced3d5e02ac68eceebb77bc379ae17723d328e24 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Wed, 22 Jul 2026 10:34:39 +0200 Subject: [PATCH 014/150] fix(tui): submit prompt when resuming session (#38260) --- packages/cli/src/commands/commands.ts | 1 + packages/cli/src/commands/handlers/default.ts | 6 +- packages/tui/src/app.tsx | 8 +- packages/tui/src/routes/session/index.tsx | 21 ++++- packages/tui/test/app-lifecycle.test.tsx | 83 +++++++++++++++++++ 5 files changed, 112 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/commands/commands.ts b/packages/cli/src/commands/commands.ts index a287c3c9ecb8..c0b914fc62b8 100644 --- a/packages/cli/src/commands/commands.ts +++ b/packages/cli/src/commands/commands.ts @@ -32,6 +32,7 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO Flag.withDescription("Session ID to continue"), Flag.optional, ), + prompt: Flag.string("prompt").pipe(Flag.withDescription("Prompt to use"), Flag.optional), }, commands: [ Spec.make("acp", { description: "Start an Agent Client Protocol server" }), diff --git a/packages/cli/src/commands/handlers/default.ts b/packages/cli/src/commands/handlers/default.ts index 66f7469aa57e..f83a0ef7cb8f 100644 --- a/packages/cli/src/commands/handlers/default.ts +++ b/packages/cli/src/commands/handlers/default.ts @@ -55,7 +55,11 @@ export default Runtime.handler(Commands, (input) => } : undefined, }, - args: { continue: input.continue, sessionID: Option.getOrUndefined(input.session) }, + args: { + continue: input.continue, + sessionID: Option.getOrUndefined(input.session), + prompt: Option.getOrUndefined(input.prompt), + }, config: { path: config.path, get: () => runPromise(config.get()), diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index b4e6fb815ec8..704332977955 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -525,6 +525,7 @@ function App(props: { pair?: DialogPairCredentials; started: number }) { }) const args = useArgs() + const startupPrompt = args.prompt ? { text: args.prompt, files: [], agents: [], pasted: [] } : undefined onMount(() => { batch(() => { if (args.agent) local.agent.set(args.agent) @@ -542,6 +543,7 @@ function App(props: { pair?: DialogPairCredentials; started: number }) { route.navigate({ type: "session", sessionID: args.sessionID, + prompt: startupPrompt, }) } }) @@ -564,12 +566,12 @@ function App(props: { pair?: DialogPairCredentials; started: number }) { const match = response.data[0]?.id if (!match) return if (!args.fork) { - route.navigate({ type: "session", sessionID: match }) + route.navigate({ type: "session", sessionID: match, prompt: startupPrompt }) return } void client.api.session .fork({ sessionID: match }) - .then((result) => route.navigate({ type: "session", sessionID: result.id })) + .then((result) => route.navigate({ type: "session", sessionID: result.id, prompt: startupPrompt })) .catch(toast.error) }) .catch(toast.error) @@ -582,7 +584,7 @@ function App(props: { pair?: DialogPairCredentials; started: number }) { forked = true void client.api.session .fork({ sessionID: args.sessionID }) - .then((result) => route.navigate({ type: "session", sessionID: result.id })) + .then((result) => route.navigate({ type: "session", sessionID: result.id, prompt: startupPrompt })) .catch(toast.error) }) diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index acf38c353c3a..5a4fd04e828c 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -84,6 +84,7 @@ import { createSessionRows, messageBoundaryIDs, resolvePart, type PartRef, type import { switchLabel } from "../../util/model" import { findMessageBoundary, messageNavigationSlack } from "./message-navigation" import { stringWidth } from "../../util/string-width" +import { useArgs } from "../../context/args" addDefaultParsers(parsers.parsers) @@ -120,6 +121,7 @@ export function Session() { const { navigate } = useRoute() const data = useData() const local = useLocal() + const args = useArgs() const paths = useTuiPaths() const configState = useConfig() const config = configState.data @@ -216,6 +218,7 @@ export function Session() { const boundaries = createMemo(() => messageBoundaryIDs(rows, messages())) const [navigationMessage, setNavigationMessage] = createSignal() const [navigationSlack, setNavigationSlack] = createSignal(0) + const [synced, setSynced] = createSignal(false) const clearMessageNavigation = () => { setNavigationSlack(0) @@ -242,6 +245,7 @@ export function Session() { createEffect(() => { if (client.connection.status() !== "connected") return + setSynced(false) const sessionID = route.sessionID void (async () => { await Promise.all([ @@ -261,6 +265,7 @@ export function Session() { } editor.reconnect(info.location.directory) if (route.sessionID === sessionID && scroll) scroll.scrollBy(100_000) + setSynced(true) })().catch((error) => { if (route.sessionID !== sessionID) return toast.show({ @@ -273,15 +278,25 @@ export function Session() { }) let seeded = false + let sent = false let scroll: ScrollBoxRenderable - let prompt: PromptRef | undefined + const [prompt, setPrompt] = createSignal() const bind = (r: PromptRef | undefined) => { - prompt = r + setPrompt(r) promptRef.set(r) if (seeded || !route.prompt || !r) return seeded = true r.set(route.prompt) } + + createEffect(() => { + const current = prompt() + if (sent || !current || !synced() || !local.model.ready) return + if (!local.agent.current() || !local.model.current()) return + if (!args.prompt || route.prompt?.text !== args.prompt || current.current.text !== args.prompt) return + sent = true + current.submit() + }) const dialog = useDialog() const renderer = useRenderer() const unavailable = (feature: string) => { @@ -526,7 +541,7 @@ export function Session() { void client.api.session.revert .stage({ sessionID: route.sessionID, messageID: message.id }) .catch((error) => toast.show({ message: errorMessage(error), variant: "error", duration: 5000 })) - prompt?.set({ + prompt()?.set({ ...projectedPromptInput(message), pasted: [], }) diff --git a/packages/tui/test/app-lifecycle.test.tsx b/packages/tui/test/app-lifecycle.test.tsx index 81a61684baf9..a07e3e6b12b2 100644 --- a/packages/tui/test/app-lifecycle.test.tsx +++ b/packages/tui/test/app-lifecycle.test.tsx @@ -69,6 +69,7 @@ test("session lifecycle updates the terminal title and prints the epilogue after setTitle(title) } const events = createEventStream() + let promptRequests = 0 const calls = createFetch((url) => { const session = { id: "dummy", @@ -88,6 +89,10 @@ test("session lifecycle updates the terminal title and prints the epilogue after if (url.pathname === "/api/session/dummy/message") return json({ data: [], cursor: {} }) if (url.pathname === "/api/session/dummy/pending") return json({ data: [] }) if (url.pathname === "/api/session/dummy/permission") return json({ data: [] }) + if (url.pathname === "/api/session/dummy/prompt") { + promptRequests++ + return json({ data: {} }) + } }, events) const server = Bun.serve({ port: 0, fetch: (request) => calls.fetch(request) }) const originalWrite = process.stdout.write.bind(process.stdout) @@ -124,6 +129,7 @@ test("session lifecycle updates the terminal title and prints the epilogue after expect(stdout).toContain("Renamed session") expect(stdout).toContain("opencode2 -s dummy") + expect(promptRequests).toBe(0) } finally { process.stdout.write = originalWrite if (!setup.renderer.isDestroyed) setup.renderer.destroy() @@ -131,3 +137,80 @@ test("session lifecycle updates the terminal title and prints the epilogue after mock.restore() } }) + +test("session startup prompt is submitted exactly once", async () => { + const setup = await createTestRenderer({ width: 80, height: 24, useThread: false }) + const core = await import("@opentui/core") + mock.module("@opentui/core", () => ({ ...core, createCliRenderer: async () => setup.renderer })) + const events = createEventStream() + const cwd = process.cwd() + const location = { directory: cwd, project: { id: "project", directory: cwd } } + const session = { + id: "dummy", + title: "Demo session", + projectID: "project", + location: { directory: cwd }, + agent: "build", + model: { providerID: "provider", id: "model" }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { created: 0, updated: 0 }, + } + const bodies: unknown[] = [] + const promptSubmitted = Promise.withResolvers() + const calls = createFetch(async (url, request) => { + if (url.pathname === "/api/location") return json(location) + if (url.pathname === "/api/session") return json({ data: [session], cursor: {} }) + if (url.pathname === "/api/session/dummy") return json({ data: session }) + if (url.pathname === "/api/session/dummy/message") return json({ data: [], cursor: {} }) + if (url.pathname === "/api/session/dummy/pending") return json({ data: [] }) + if (url.pathname === "/api/session/dummy/permission") return json({ data: [] }) + if (url.pathname === "/api/agent") + return json({ + location, + data: [{ id: "build", mode: "primary", hidden: false, permissions: [] }], + }) + if (url.pathname === "/api/model") + return json({ + location, + data: [{ id: "model", providerID: "provider", name: "Model", variants: [] }], + }) + if (url.pathname === "/api/session/dummy/prompt") { + bodies.push(await request.json()) + promptSubmitted.resolve() + return json({ data: {} }) + } + }, events) + const server = Bun.serve({ port: 0, fetch: (request) => calls.fetch(request) }) + + try { + const { run } = await import("../src/app") + const task = Effect.runPromise( + run({ + app: { name: "test", version: "test", channel: "test" }, + server: { endpoint: { url: server.url.toString() } }, + config: { get: async () => ({}), update: async () => ({}) }, + packages: { resolve: async () => undefined }, + args: { sessionID: "dummy", prompt: "RESUME_READY" }, + log: () => {}, + }).pipe(Effect.provide(AppNodeBuilder.build(Global.node)), Effect.provide(FileSystem.layerNoop({}))), + ) + + await Promise.race([ + promptSubmitted.promise, + Bun.sleep(2000).then(() => { + throw new Error("startup prompt was not submitted") + }), + ]) + await Bun.sleep(20) + setup.renderer.destroy() + await task + + expect(bodies).toHaveLength(1) + expect(bodies[0]).toMatchObject({ text: "RESUME_READY" }) + } finally { + if (!setup.renderer.isDestroyed) setup.renderer.destroy() + await server.stop() + mock.restore() + } +}) From 0e08b7330f4ec82b80c00eacda3fd08478eab04b Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Wed, 22 Jul 2026 10:35:07 +0200 Subject: [PATCH 015/150] fix(tui): set mini terminal title on Linux (#38261) --- packages/tui/src/mini/runtime.lifecycle.ts | 9 +++++++ packages/tui/src/mini/runtime.ts | 6 +++++ packages/tui/src/mini/stream-v2.transport.ts | 5 ++++ packages/tui/test/mini/runtime.test.ts | 7 ++++++ .../tui/test/mini/stream-v2.transport.test.ts | 25 +++++++++++++++++++ 5 files changed, 52 insertions(+) diff --git a/packages/tui/src/mini/runtime.lifecycle.ts b/packages/tui/src/mini/runtime.lifecycle.ts index 2baacfe46f10..5354946d9195 100644 --- a/packages/tui/src/mini/runtime.lifecycle.ts +++ b/packages/tui/src/mini/runtime.lifecycle.ts @@ -82,6 +82,7 @@ export type Lifecycle = { footer: FooterApi onResize(fn: () => void): () => void refreshTheme(): void + setTitle(title?: string): void resetForReplay(input: { sessionTitle?: string; sessionID?: string; history: RunPrompt[] }): Promise close(input: { showExit: boolean; sessionTitle?: string; sessionID?: string; history?: RunPrompt[] }): Promise } @@ -184,6 +185,12 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise { + if (input.host.platform !== "linux") return + if (!title || isDefaultTitle(title)) return renderer.setTerminalTitle("OpenCode") + renderer.setTerminalTitle(`OC | ${title.length > 40 ? title.slice(0, 37) + "..." : title}`) + } + setTitle(input.sessionTitle) const theme = await resolveRunTheme(renderer, tuiConfig.theme, mono) renderer.setBackgroundColor(theme.background) const state: SplashState = { @@ -338,6 +345,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise {}) footer.destroy() + if (input.host.platform === "linux") renderer.setTerminalTitle("") shutdown(renderer) if (!wroteExit) { input.host.stdout.write("\n") @@ -350,6 +358,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise { + state.sessionTitle = title + shell.setTitle(title) + }, trace: log, onCatalogRefresh: requestCatalogRefresh, contextLimit: (model) => @@ -887,6 +892,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep state.shown = false state.sessionID = created.sessionID state.sessionTitle = created.sessionTitle + shell.setTitle(state.sessionTitle) state.agent = created.agent ?? state.agent state.location = created.location state.model = created.model diff --git a/packages/tui/src/mini/stream-v2.transport.ts b/packages/tui/src/mini/stream-v2.transport.ts index b90f5e682d70..b8aab5ee2491 100644 --- a/packages/tui/src/mini/stream-v2.transport.ts +++ b/packages/tui/src/mini/stream-v2.transport.ts @@ -46,6 +46,7 @@ type StreamInput = { replayLimit?: number footer: FooterApi onCommit?: (commit: StreamCommit) => void + onSessionTitle?: (title: string) => void trace?: Trace signal?: AbortSignal onCatalogRefresh?: (signal?: AbortSignal) => unknown | Promise @@ -828,6 +829,10 @@ export async function createSessionTransport(input: StreamInput): Promise { footer: api, onResize: () => () => {}, refreshTheme: () => {}, + setTitle: () => {}, resetForReplay: () => Promise.resolve(), close: () => Promise.resolve(), } @@ -180,6 +181,7 @@ describe("run interactive runtime", () => { footer: api, onResize: () => () => {}, refreshTheme: () => {}, + setTitle: () => {}, resetForReplay: () => Promise.resolve(), close: () => Promise.resolve(), } @@ -199,6 +201,7 @@ describe("run interactive runtime", () => { const lifecycleStarted = defer() const painted = defer() const events: FooterEvent[] = [] + const titles: Array = [] const api = footer(events) api.idle = () => painted.promise const event = api.event @@ -265,6 +268,7 @@ describe("run interactive runtime", () => { footer: api, onResize: () => () => {}, refreshTheme: () => {}, + setTitle: (title) => titles.push(title), resetForReplay: () => Promise.resolve(), close: () => Promise.resolve(), } @@ -282,6 +286,7 @@ describe("run interactive runtime", () => { history: [{ text: "previous prompt", parts: [] }], }) expect(events).toContainEqual({ type: "agent", agent: "review" }) + expect(titles).toEqual(["Resume"]) expect(events).toContainEqual({ type: "model", model: "Little Frank · OpenAI · high", @@ -344,6 +349,7 @@ describe("run interactive runtime", () => { footer: api, onResize: () => () => {}, refreshTheme: () => {}, + setTitle: () => {}, resetForReplay: () => Promise.resolve(), close: async (input) => { closedTitle = input.sessionTitle @@ -423,6 +429,7 @@ describe("run interactive runtime", () => { footer: api, onResize: () => () => {}, refreshTheme: () => {}, + setTitle: () => {}, resetForReplay: () => Promise.resolve(), close: () => Promise.resolve(), } diff --git a/packages/tui/test/mini/stream-v2.transport.test.ts b/packages/tui/test/mini/stream-v2.transport.test.ts index 371611972ece..18bbc0b08c75 100644 --- a/packages/tui/test/mini/stream-v2.transport.test.ts +++ b/packages/tui/test/mini/stream-v2.transport.test.ts @@ -200,6 +200,31 @@ afterEach(() => { }) describe("V2 mini transport", () => { + test("reports session title changes", async () => { + const events = feed() + events.push(connected()) + const titles: string[] = [] + const transport = await createSessionTransport({ + sdk: sdk({ streams: [events] }), + sessionID: "ses_1", + thinking: false, + footer: footer().api, + onSessionTitle: (title) => titles.push(title), + }) + + events.push({ + id: "evt_renamed", + created: 1, + type: "session.renamed", + durable: durable("ses_1", 1), + data: { sessionID: "ses_1", title: "Greeting" }, + }) + + while (titles.length === 0) await Bun.sleep(0) + expect(titles).toEqual(["Greeting"]) + await transport.close() + }) + test("formats footer usage with compact tokens and context percentage", async () => { const events = feed() events.push(connected()) From 794137b33b0f144531a9bf06028932cd06efbf7e Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Wed, 22 Jul 2026 10:35:35 +0200 Subject: [PATCH 016/150] refactor(tui): narrow mini compatibility surfaces (#38262) --- packages/cli/package.json | 1 - packages/cli/test/import-boundaries.test.ts | 2 + packages/tui/package.json | 2 +- packages/tui/src/mini/demo.ts | 58 +++++-------------- packages/tui/src/mini/footer.command.tsx | 7 +-- packages/tui/src/mini/footer.prompt.tsx | 15 ++--- packages/tui/src/mini/footer.ts | 2 - packages/tui/src/mini/footer.view.tsx | 3 - packages/tui/src/mini/form.shared.ts | 4 +- packages/tui/src/mini/permission.shared.ts | 2 +- packages/tui/src/mini/runtime.lifecycle.ts | 1 - packages/tui/src/mini/scrollback.writer.tsx | 2 +- packages/tui/src/mini/stream-v2.subagent.ts | 5 +- packages/tui/src/mini/stream.ts | 2 +- packages/tui/src/mini/tool.public.ts | 2 + packages/tui/src/mini/tool.ts | 28 ++++----- packages/tui/src/mini/types.ts | 31 ++++------ packages/tui/test/mini/fixture/tui-runtime.ts | 19 ------ packages/tui/test/mini/footer-keymap.test.tsx | 2 - packages/tui/test/mini/footer.view.test.tsx | 27 +++------ packages/tui/test/mini/runtime.boot.test.ts | 8 +-- packages/tui/test/mini/runtime.test.ts | 1 - packages/tui/test/mini/tool.test.ts | 2 +- 23 files changed, 67 insertions(+), 159 deletions(-) create mode 100644 packages/tui/src/mini/tool.public.ts delete mode 100644 packages/tui/test/mini/fixture/tui-runtime.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 7ace585e6a6c..acf8ec75dd4d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -11,7 +11,6 @@ "bin" ], "exports": { - "./daemon": "./src/daemon.ts", "./run": "./src/run/index.ts", "./server-process": "./src/server-process.ts" }, diff --git a/packages/cli/test/import-boundaries.test.ts b/packages/cli/test/import-boundaries.test.ts index f0eef3cbd8bf..8b189ce2c032 100644 --- a/packages/cli/test/import-boundaries.test.ts +++ b/packages/cli/test/import-boundaries.test.ts @@ -18,10 +18,12 @@ describe("CLI frontend import boundaries", () => { test("exposes only the intentional package entrypoints", async () => { const run = await import("@opencode-ai/cli/run") const mini = await import("@opencode-ai/tui/mini") + const tool = await import("@opencode-ai/tui/mini/tool") const cli = await Bun.file(path.join(root, "packages/cli/package.json")).json() expect(Object.keys(run).sort()).toEqual(["runNonInteractive", "runV1Bridge"]) expect(Object.keys(mini).sort()).toEqual(["runMiniFrontend"]) + expect(Object.keys(tool).sort()).toEqual(["toolInlineInfo", "toolOutputText"]) expect(Object.keys(cli.exports).filter((key) => key === "./mini" || key.startsWith("./mini/"))).toEqual([]) }) diff --git a/packages/tui/package.json b/packages/tui/package.json index 8ced93ff1fcb..0fb182f8d355 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -30,7 +30,7 @@ "./editor": "./src/editor.ts", "./editor-zed": "./src/editor-zed.ts", "./mini": "./src/mini/index.ts", - "./mini/tool": "./src/mini/tool.ts", + "./mini/tool": "./src/mini/tool.public.ts", "./model-preference": "./src/model-preference.ts", "./runtime": "./src/runtime.tsx", "./terminal-win32": "./src/terminal-win32.ts", diff --git a/packages/tui/src/mini/demo.ts b/packages/tui/src/mini/demo.ts index e4c11c9813af..a16ae991ac28 100644 --- a/packages/tui/src/mini/demo.ts +++ b/packages/tui/src/mini/demo.ts @@ -16,8 +16,9 @@ // the synthetic tool parts through the same callbacks used by the live footer. import path from "path" import type { JsonValue, SessionMessageAssistantTool } from "@opencode-ai/client/promise" +import { parseSlashHead } from "../prompt/parse" import { writeSessionOutput } from "./stream" -import { toolCommit } from "./stream-v2.subagent" +import { toolCommit, toolFinalPhase } from "./stream-v2.subagent" import type { FooterApi, FooterView, @@ -110,7 +111,6 @@ const SAMPLE_TABLE = [ type Ref = { msg: string - part: string call: string tool: string input: Record @@ -126,7 +126,6 @@ type FormRequest = { type Perm = { ref: Ref done: { - title: string output: string metadata?: Record } @@ -203,7 +202,6 @@ function showSubagent( description: input.description, status: input.status, title: input.title, - lastUpdatedAt: Date.now(), }, ], details: { @@ -337,7 +335,6 @@ async function emitReasoning(state: State, body: string, signal?: AbortSignal): function make(state: State, tool: string, input: Record): Ref { return { msg: open(state), - part: take(state, "part", "part"), call: take(state, "call", "call"), tool, input, @@ -346,7 +343,7 @@ function make(state: State, tool: string, input: Record): Ref } function startTool(state: State, ref: Ref, structured: Record = {}): SessionMessageAssistantTool { - state.started.add(ref.part) + state.started.add(ref.call) const part = { type: "tool" as const, id: ref.call, @@ -386,12 +383,11 @@ function doneTool( state: State, ref: Ref, output: { - title: string output: string metadata?: Record }, ): void { - if (!state.started.has(ref.part)) startTool(state, ref) + if (!state.started.has(ref.call)) startTool(state, ref) const part: SessionMessageAssistantTool = { type: "tool", id: ref.call, @@ -404,11 +400,11 @@ function doneTool( }, time: { created: ref.start, ran: ref.start, completed: Date.now() }, } - present(state, [toolCommit(part, ref.msg, output.output ? "progress" : "final")]) + present(state, [toolCommit(part, ref.msg, toolFinalPhase(part))]) } function failTool(state: State, ref: Ref, error: string): void { - if (!state.started.has(ref.part)) startTool(state, ref) + if (!state.started.has(ref.call)) startTool(state, ref) present(state, [ toolCommit( { @@ -443,7 +439,6 @@ async function emitBash(state: State, signal?: AbortSignal): Promise { startTool(state, ref) await wait(70, signal) doneTool(state, ref, { - title: "git status", output: `${process.cwd()}\ngit status\nOn branch demo\nnothing to commit, working tree clean\n`, metadata: { exit: 0, @@ -458,7 +453,6 @@ function emitWrite(state: State): void { content: "export const demo = 42\n", }) doneTool(state, ref, { - title: "write", output: "", metadata: {}, }) @@ -470,7 +464,6 @@ function emitEdit(state: State): void { path: file, }) doneTool(state, ref, { - title: "edit", output: "", metadata: { files: [ @@ -490,7 +483,6 @@ function emitPatch(state: State): void { patchText: "*** Begin Patch\n*** End Patch", }) doneTool(state, ref, { - title: "patch", output: "", metadata: { files: [ @@ -517,10 +509,9 @@ function emitTask(state: State): void { agent: "explore", }) doneTool(state, ref, { - title: "Reducer touchpoints found", output: "", metadata: { - sessionID: "sub_demo_1", + sessionID: "ses_demo_child", status: "completed", output: "", }, @@ -542,7 +533,7 @@ function emitTask(state: State): void { time: { created: Date.now(), ran: Date.now() }, } satisfies SessionMessageAssistantTool showSubagent(state, { - sessionID: "sub_demo_1", + sessionID: "ses_demo_child", label: "Explore", description: "Scan run/* for reducer touchpoints", status: "completed", @@ -562,16 +553,7 @@ function emitTask(state: State): void { messageID: "sub_demo_msg_reasoning", partID: "sub_demo_reasoning_1", }, - { - kind: "tool", - text: "running read", - phase: "start", - source: "tool", - messageID: "sub_demo_msg_tool", - partID: "sub_demo_tool_1", - tool: "read", - part, - }, + toolCommit(part, "sub_demo_msg_tool", "start"), { kind: "assistant", text: "Footer updates flow through stream.ts into RunFooter", @@ -610,7 +592,6 @@ function emitQuestionTool(state: State): void { ], }) doneTool(state, ref, { - title: "question", output: "", metadata: { answers: [["Diff"], ["Usage", "custom-note"]], @@ -635,7 +616,6 @@ function emitPermission(state: State, kind: PermissionKind = "edit"): void { patterns: [command], always: ["*"], done: { - title: "git status --short", output: `${root}\ngit status --short\n M src/demo-format.ts\n?? src/demo-permission.ts\n`, metadata: { exit: 0, @@ -658,7 +638,6 @@ function emitPermission(state: State, kind: PermissionKind = "edit"): void { patterns: [target], always: [target], done: { - title: "read", output: ["1: {", '2: "name": "opencode",', '3: "private": true', "4: }"].join("\n"), metadata: {}, }, @@ -677,7 +656,6 @@ function emitPermission(state: State, kind: PermissionKind = "edit"): void { patterns: ["explore"], always: ["*"], done: { - title: "Footer spacing checked", output: "", metadata: { sessionID: "sub_demo_perm_1", @@ -707,7 +685,6 @@ function emitPermission(state: State, kind: PermissionKind = "edit"): void { }, always: [`${dir}/**`], done: { - title: "read", output: `1: # External demo\n2: Shared preview file\nPath: ${target}`, metadata: {}, }, @@ -726,7 +703,6 @@ function emitPermission(state: State, kind: PermissionKind = "edit"): void { patterns: ["*"], always: ["*"], done: { - title: "Retry allowed", output: "Continuing after repeated failures.\n", metadata: {}, }, @@ -744,7 +720,6 @@ function emitPermission(state: State, kind: PermissionKind = "edit"): void { patterns: [file], always: [file], done: { - title: "edit", output: "", metadata: { files: [{ file, status: "modified", patch: diff }], @@ -955,8 +930,9 @@ export function createRunDemo(input: Input) { const prompt = async (line: RunPrompt, signal?: AbortSignal): Promise => { const text = line.text.trim() - const list = text.split(/\s+/) - const cmd = list[0] || "" + const head = parseSlashHead(text) + const list = head?.arguments.split(/\s+/).filter(Boolean) ?? [] + const cmd = head ? `/${head.name}` : "" clearSubagent(state.footer) @@ -966,7 +942,7 @@ export function createRunDemo(input: Input) { } if (cmd === "/permission") { - const kind = permissionKind(list[1]) + const kind = permissionKind(list[0]) if (!kind) { note(state.footer, `Pick a permission kind: ${PERMISSIONS.join(", ")}`) return true @@ -977,7 +953,7 @@ export function createRunDemo(input: Input) { } if (cmd === "/form") { - const kind = formKind(list[1]) + const kind = formKind(list[0]) if (!kind) { note(state.footer, `Pick a form kind: ${FORMS.join(", ")}`) return true @@ -988,8 +964,8 @@ export function createRunDemo(input: Input) { } if (cmd === "/fmt") { - const kind = (list[1] || "").toLowerCase() - const body = list.slice(2).join(" ") + const kind = (list[0] || "").toLowerCase() + const body = list.slice(1).join(" ") if (!kind) { note(state.footer, `Pick a kind: ${KINDS.join(", ")}`) return true @@ -1032,7 +1008,6 @@ export function createRunDemo(input: Input) { clearBlocker(state) if (form.kind === "question") { doneTool(state, form.ref, { - title: "question", output: "", metadata: { answers: form.request.fields.map((field) => { @@ -1045,7 +1020,6 @@ export function createRunDemo(input: Input) { return true } doneTool(state, form.ref, { - title: form.request.title, output: `Form submitted: ${Object.entries(input.answer) .map(([key, value]) => `${key}=${Array.isArray(value) ? value.join(", ") : String(value)}`) .join("; ")}\n`, diff --git a/packages/tui/src/mini/footer.command.tsx b/packages/tui/src/mini/footer.command.tsx index f2edfcbbf0c1..4dff4f8112e5 100644 --- a/packages/tui/src/mini/footer.command.tsx +++ b/packages/tui/src/mini/footer.command.tsx @@ -54,10 +54,6 @@ type SubagentEntry = PanelEntry & { current: boolean } -type QueuedEntry = PanelEntry & { - prompt: FooterQueuedPrompt -} - type SettingEntry = PanelEntry & { key: keyof MiniSettings } @@ -747,13 +743,12 @@ export function RunQueuedPromptSelectBody(props: { onRows?: (rows: number) => void mono?: boolean }) { - const entries = createMemo(() => + const entries = createMemo(() => props.prompts().map((prompt) => ({ category: "", display: prompt.prompt.text.replaceAll("\n", " "), footer: prompt.delivery, keywords: prompt.prompt.text, - prompt, })), ) const controller = createSearchablePanelController({ diff --git a/packages/tui/src/mini/footer.prompt.tsx b/packages/tui/src/mini/footer.prompt.tsx index 544d76007d20..8b8acf98484a 100644 --- a/packages/tui/src/mini/footer.prompt.tsx +++ b/packages/tui/src/mini/footer.prompt.tsx @@ -31,13 +31,13 @@ import { realignEditorPromptParts, resolveEditorSlashValue } from "./prompt.edit import { monoTruncateMiddle } from "./mono" import { FOOTER_MENU_ROWS, createFooterMenuState, type RunFooterMenuItem } from "./footer.menu" import type { RunFooterTheme } from "./theme" -import type { FooterState, RunAgent, RunCommand, RunPrompt, RunPromptPart, RunReference, RunTuiConfig } from "./types" +import type { FooterState, RunAgent, RunCommand, RunPrompt, RunPromptPart, RunReference } from "./types" const AUTOCOMPLETE_ROWS = FOOTER_MENU_ROWS const AUTOCOMPLETE_BOTTOM_ROWS = 1 export const TEXTAREA_MIN_ROWS = 1 -export const TEXTAREA_MAX_ROWS = 6 +const TEXTAREA_MAX_ROWS = 6 export const PROMPT_MAX_ROWS = TEXTAREA_MAX_ROWS + AUTOCOMPLETE_ROWS - 1 + AUTOCOMPLETE_BOTTOM_ROWS type Mention = Extract @@ -65,7 +65,6 @@ type PromptInput = { agents: Accessor references: Accessor commands: Accessor - tuiConfig: RunTuiConfig state: Accessor view: Accessor prompt: Accessor @@ -142,7 +141,7 @@ function parseSlashCommand(text: string, commands: RunCommand[] | undefined) { } } -export function selectedCommand(text: string, command: RunPrompt["command"], commands?: RunCommand[]) { +export function selectedCommand(text: string, command: RunPrompt["command"]) { if (!command) { return } @@ -152,14 +151,10 @@ export function selectedCommand(text: string, command: RunPrompt["command"], com return } - // Bound drafts (e.g. the skill picker) may predate or omit the catalog - // source; resolve it at submit time so routing never degrades to a plain - // command for a skill entry. - const source = command.source ?? commands?.find((item) => item.name === command.name)?.source return { name: command.name, arguments: head.arguments, - ...(source ? { source } : {}), + ...(command.source ? { source: command.source } : {}), } } @@ -1140,7 +1135,7 @@ export function createPromptState(input: PromptInput): PromptState { return } - const command = next.mode === "shell" ? undefined : selectedCommand(next.text, next.command, input.commands()) + const command = next.mode === "shell" ? undefined : selectedCommand(next.text, next.command) if (!command && next.mode !== "shell" && isExitCommand(next.text)) { input.onExit() return diff --git a/packages/tui/src/mini/footer.ts b/packages/tui/src/mini/footer.ts index b21c249612a7..2673cc5e6f45 100644 --- a/packages/tui/src/mini/footer.ts +++ b/packages/tui/src/mini/footer.ts @@ -74,7 +74,6 @@ type RunFooterOptions = { agents: RunAgent[] references: RunReference[] wrote?: boolean - sessionID: () => string | undefined agentLabel: string modelLabel: string model: RunInput["model"] @@ -312,7 +311,6 @@ export class RunFooter implements FooterApi { currentVariant: footer.currentVariant, theme: footer.theme, mono: options.mono, - tuiConfig: options.tuiConfig, miniSettings: footer.miniSettings, history: footer.history, onSubmit: footer.handlePrompt, diff --git a/packages/tui/src/mini/footer.view.tsx b/packages/tui/src/mini/footer.view.tsx index 72e4cf68ca06..dfc326f4b456 100644 --- a/packages/tui/src/mini/footer.view.tsx +++ b/packages/tui/src/mini/footer.view.tsx @@ -50,7 +50,6 @@ import type { RunPrompt, RunProvider, RunReference, - RunTuiConfig, } from "./types" import type { RunTheme } from "./theme" @@ -88,7 +87,6 @@ type RunFooterViewProps = { queuedPrompts?: () => FooterQueuedPrompt[] theme: () => RunTheme mono: boolean - tuiConfig: RunTuiConfig miniSettings: () => MiniSettings history?: () => RunPrompt[] onSubmit: (input: RunPrompt) => boolean @@ -359,7 +357,6 @@ export function RunFooterView(props: RunFooterViewProps) { agents: props.agents, references: props.references, commands: props.commands, - tuiConfig: props.tuiConfig, state: props.state, view: promptView, prompt, diff --git a/packages/tui/src/mini/form.shared.ts b/packages/tui/src/mini/form.shared.ts index 7fdcd7fe51c4..e571dabbdcf7 100644 --- a/packages/tui/src/mini/form.shared.ts +++ b/packages/tui/src/mini/form.shared.ts @@ -14,7 +14,7 @@ import { import type { FormAnswerField } from "../util/form" import type { FormReply, MiniFormRequest } from "./types" -export { formCustom, formLabel, formRows, formSelected, formTextual, formValidateValue } +export { formCustom, formLabel, formRows, formTextual, formValidateValue } export type FormBodyState = { formID: string @@ -97,7 +97,7 @@ export function formSetSelected(state: FormBodyState, selected: number): FormBod return { ...state, selected, error: "" } } -export function formSetEditing(state: FormBodyState, editing: boolean): FormBodyState { +function formSetEditing(state: FormBodyState, editing: boolean): FormBodyState { return { ...state, editing, error: "" } } diff --git a/packages/tui/src/mini/permission.shared.ts b/packages/tui/src/mini/permission.shared.ts index fa4610efcbf4..e293d3ef17b7 100644 --- a/packages/tui/src/mini/permission.shared.ts +++ b/packages/tui/src/mini/permission.shared.ts @@ -76,7 +76,7 @@ export function permissionLabel(option: PermissionOption): string { export { permissionAlwaysLines } -export function permissionReply( +function permissionReply( sessionID: string, requestID: string, reply: PermissionReply["reply"], diff --git a/packages/tui/src/mini/runtime.lifecycle.ts b/packages/tui/src/mini/runtime.lifecycle.ts index 5354946d9195..9c43d7fd6f3a 100644 --- a/packages/tui/src/mini/runtime.lifecycle.ts +++ b/packages/tui/src/mini/runtime.lifecycle.ts @@ -232,7 +232,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise input.sessionID), ...labels, model: input.model, variant: input.variant, diff --git a/packages/tui/src/mini/scrollback.writer.tsx b/packages/tui/src/mini/scrollback.writer.tsx index 436397248a70..6b141e28e832 100644 --- a/packages/tui/src/mini/scrollback.writer.tsx +++ b/packages/tui/src/mini/scrollback.writer.tsx @@ -30,7 +30,7 @@ export function sameEntryGroup(left: StreamCommit | undefined, right: StreamComm return Boolean(current && next && current === next) } -export function entryLayout(commit: StreamCommit, body: RunEntryBody = entryBody(commit)): EntryLayout { +function entryLayout(commit: StreamCommit, body: RunEntryBody = entryBody(commit)): EntryLayout { if (commit.kind === "tool") { if (body.type === "structured" || body.type === "markdown") { return "block" diff --git a/packages/tui/src/mini/stream-v2.subagent.ts b/packages/tui/src/mini/stream-v2.subagent.ts index 164aa9a1798d..7ae8865cad4c 100644 --- a/packages/tui/src/mini/stream-v2.subagent.ts +++ b/packages/tui/src/mini/stream-v2.subagent.ts @@ -194,7 +194,6 @@ function tab(child: ChildState): FooterSubagentTab { status: child.status, background: child.background ? true : undefined, title: child.title, - lastUpdatedAt: child.lastUpdatedAt, } } @@ -1084,11 +1083,11 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac input.emit() }, snapshot() { - const tabs = [...children.values()].map(tab).toSorted((a, b) => { + const tabs = [...children.values()].toSorted((a, b) => { const active = Number(b.status === "running") - Number(a.status === "running") if (active !== 0) return active return b.lastUpdatedAt - a.lastUpdatedAt - }) + }).map(tab) const child = selected ? children.get(selected) : undefined const details: Record = child && !child.detailStale ? { [child.sessionID]: { commits: child.frames.map((item) => item.commit) } } : {} diff --git a/packages/tui/src/mini/stream.ts b/packages/tui/src/mini/stream.ts index c22131faaac1..b67962765c13 100644 --- a/packages/tui/src/mini/stream.ts +++ b/packages/tui/src/mini/stream.ts @@ -85,7 +85,7 @@ function traceCommit(commit: StreamCommit) { } } -export function traceSubagentState(state: FooterSubagentState) { +function traceSubagentState(state: FooterSubagentState) { return { tabs: state.tabs, details: Object.fromEntries( diff --git a/packages/tui/src/mini/tool.public.ts b/packages/tui/src/mini/tool.public.ts new file mode 100644 index 000000000000..820230897402 --- /dev/null +++ b/packages/tui/src/mini/tool.public.ts @@ -0,0 +1,2 @@ +export { toolInlineInfo, toolOutputText } from "./tool" +export type { MiniToolPart } from "./types" diff --git a/packages/tui/src/mini/tool.ts b/packages/tui/src/mini/tool.ts index a51d0d79ae14..09d0ff471f97 100644 --- a/packages/tui/src/mini/tool.ts +++ b/packages/tui/src/mini/tool.ts @@ -27,18 +27,17 @@ import { import { formatPath } from "../util/path-format" import type { RunEntryBody, StreamCommit, ToolSnapshot } from "./types" -export type { MiniToolPart } from "./types" export { canonicalToolName } from "../util/tool-display" -export type ToolView = { +type ToolView = { output: boolean final: boolean snap?: "code" | "diff" | "structured" } -export type ToolPhase = "start" | "progress" | "final" +type ToolPhase = "start" | "progress" | "final" -export type ToolDict = Record +type ToolDict = Record type PatchFile = { status?: string @@ -78,7 +77,7 @@ type ToolMetadata = ToolDict & { exit?: number } -export type ToolFrame = { +type ToolFrame = { directory?: string raw: string name: string @@ -94,7 +93,7 @@ export type ToolFrame = { } } -export type ToolInline = { +type ToolInline = { icon: string title: string description?: string @@ -102,7 +101,7 @@ export type ToolInline = { body?: string } -export type ToolProps = { +type ToolProps = { input: ToolInput metadata: ToolMetadata frame: ToolFrame @@ -166,7 +165,7 @@ export function toolOutputText(name: string, content: ReadonlyArray<{ type: stri function normalizeInput(name: string, value: unknown) { const input = dict(value) - const path = typeof input.path === "string" ? input.path : text(input.filePath) || text(input.filepath) + const path = typeof input.path === "string" ? input.path : text(input.filePath) const agent = typeof input.agent === "string" ? input.agent : text(input.subagent_type) return { ...input, @@ -191,7 +190,7 @@ function normalizeFile(value: unknown): PatchFile | undefined { : legacy === "move" ? "moved" : legacy) - const patch = typeof file.patch === "string" ? file.patch : text(file.diff) || undefined + const patch = typeof file.patch === "string" ? file.patch : undefined const deletions = finiteNumber(file.deletions) return { ...file, @@ -214,11 +213,6 @@ function normalizeStructured(name: string, value: unknown) { ...structured, ...(["edit", "patch"].includes(name) && Array.isArray(structured.files) ? { files } : {}), ...(name === "subagent" && sessionID ? { sessionID } : {}), - ...(name === "shell" && - finiteNumber(structured.exit) === undefined && - finiteNumber(structured.exitCode) !== undefined - ? { exit: finiteNumber(structured.exitCode) } - : {}), } } @@ -674,7 +668,7 @@ function scrollShellFinal(p: ToolProps): string { return fail(p.frame) } - const code = p.metadata.exit ?? finiteNumber(p.frame.meta.exitCode) ?? finiteNumber(p.frame.meta.exit_code) + const code = p.metadata.exit const time = span(p.frame) if (code === undefined) { if (!time) { @@ -1113,7 +1107,7 @@ function frame(part: SessionMessageAssistantTool, directory?: string): ToolFrame } } -export function toolFrame(commit: StreamCommit, raw: string): ToolFrame { +function toolFrame(commit: StreamCommit, raw: string): ToolFrame { const current = commit.part ? frame(commit.part, commit.directory) : undefined return { directory: commit.directory, @@ -1198,7 +1192,7 @@ export function toolScroll(phase: ToolPhase, ctx: ToolFrame): string { return fallbackFinal(ctx) } -export function toolSnapshot(commit: StreamCommit, raw: string): ToolSnapshot | undefined { +function toolSnapshot(commit: StreamCommit, raw: string): ToolSnapshot | undefined { const ctx = toolFrame(commit, raw) const draw = rule(ctx.name)?.snap if (!draw) { diff --git a/packages/tui/src/mini/types.ts b/packages/tui/src/mini/types.ts index 232f74c1e5bd..3d7920f93e3a 100644 --- a/packages/tui/src/mini/types.ts +++ b/packages/tui/src/mini/types.ts @@ -53,7 +53,7 @@ export type RunCommand = { source?: string } -export type RunProviderModel = { +type RunProviderModel = { name?: string cost?: { input: number @@ -159,7 +159,7 @@ export type MiniHost = { export type EntryKind = "system" | "user" | "assistant" | "reasoning" | "tool" | "error" // Whether the assistant is actively processing a turn. -export type FooterPhase = "idle" | "running" +type FooterPhase = "idle" | "running" // Full snapshot of footer status bar state. Every update replaces the whole // object in the SolidJS signal so the view re-renders atomically. @@ -188,14 +188,14 @@ export type ScrollbackOptions = { mono?: boolean } -export type ToolCodeSnapshot = { +type ToolCodeSnapshot = { kind: "code" title: string content: string file?: string } -export type ToolDiffSnapshot = { +type ToolDiffSnapshot = { kind: "diff" items: Array<{ title: string @@ -205,14 +205,14 @@ export type ToolDiffSnapshot = { }> } -export type ToolTaskSnapshot = { +type ToolTaskSnapshot = { kind: "task" title: string rows: string[] tail: string } -export type ToolQuestionSnapshot = { +type ToolQuestionSnapshot = { kind: "question" items: Array<{ question: string @@ -223,15 +223,7 @@ export type ToolQuestionSnapshot = { export type ToolSnapshot = ToolCodeSnapshot | ToolDiffSnapshot | ToolTaskSnapshot | ToolQuestionSnapshot -export type MiniToolState = - | { status: "pending"; input: Record; raw?: string } - | { - status: "running" - input: Record - title?: string - metadata?: Record - time: { start: number } - } +type MiniToolState = | { status: "completed" input: Record @@ -304,7 +296,6 @@ export type FooterSubagentTab = { status: "running" | "completed" | "cancelled" | "error" background?: boolean title?: string - lastUpdatedAt: number } export type FooterSubagentDetail = { @@ -392,7 +383,7 @@ export type FormCancel = { location?: LocationRef } -export type RunTuiConfig = Pick +export type RunTuiConfig = Pick export type MiniSettings = { thinking: "show" | "hide" @@ -408,11 +399,11 @@ export type MiniSettingChange = { // Lifecycle phase of a scrollback entry. "start" opens the entry, "progress" // appends content (coalesced in the footer queue), "final" closes it. -export type StreamPhase = "start" | "progress" | "final" +type StreamPhase = "start" | "progress" | "final" -export type StreamSource = "assistant" | "reasoning" | "tool" | "system" +type StreamSource = "assistant" | "reasoning" | "tool" | "system" -export type StreamToolState = "running" | "completed" | "error" +type StreamToolState = "running" | "completed" | "error" // A single append-only commit to scrollback. The transport produces these from // V2 events, and RunFooter.append() queues them for the next diff --git a/packages/tui/test/mini/fixture/tui-runtime.ts b/packages/tui/test/mini/fixture/tui-runtime.ts deleted file mode 100644 index 0334ea5abe83..000000000000 --- a/packages/tui/test/mini/fixture/tui-runtime.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { resolve, type Info, type Resolved } from "../../../src/config" -import { TuiKeybind } from "../../../src/config/keybind" - -type ResolvedInput = Omit & { - attention?: Partial - keybinds?: Partial - leader_timeout?: number -} - -export function createTuiResolvedConfig(input: ResolvedInput = {}) { - const { leader_timeout, ...current } = input - return resolve( - { - ...current, - leader: leader_timeout === undefined ? undefined : { timeout: leader_timeout }, - }, - { terminalSuspend: process.platform !== "win32" }, - ) -} diff --git a/packages/tui/test/mini/footer-keymap.test.tsx b/packages/tui/test/mini/footer-keymap.test.tsx index 15ddbaacbfae..da2513dba4b4 100644 --- a/packages/tui/test/mini/footer-keymap.test.tsx +++ b/packages/tui/test/mini/footer-keymap.test.tsx @@ -26,7 +26,6 @@ test("down opens subagents from an empty prompt", async () => { label: "Explore", description: "Inspect the keymap", status: "running", - lastUpdatedAt: 1, }, ], details: {}, @@ -54,7 +53,6 @@ test("down opens subagents from an empty prompt", async () => { view={view} subagent={subagents} theme={() => RUN_THEME_FALLBACK} - tuiConfig={config} miniSettings={() => ({ thinking: "hide", shell_output: "hide", turn_summary: "show", footer: "show", mono: false })} mono={false} onSubmit={() => true} diff --git a/packages/tui/test/mini/footer.view.test.tsx b/packages/tui/test/mini/footer.view.test.tsx index 93f4c1b03185..38eabf85ae9c 100644 --- a/packages/tui/test/mini/footer.view.test.tsx +++ b/packages/tui/test/mini/footer.view.test.tsx @@ -35,7 +35,7 @@ import type { } from "../../src/mini/types" import { selectedCommand } from "../../src/mini/footer.prompt" import { RejectField } from "../../src/mini/footer.permission" -import { createTuiResolvedConfig } from "./fixture/tui-runtime" +import { createTuiResolvedConfig } from "../fixture/tui-runtime" const tuiConfig = createTuiResolvedConfig() @@ -87,7 +87,6 @@ function subagent(input: { label: input.label, description: input.description, status: input.status ?? "running", - lastUpdatedAt: 1, } satisfies FooterSubagentTab } @@ -152,7 +151,6 @@ async function renderFooter( subagent={subagents} theme={input.theme ?? (() => RUN_THEME_FALLBACK)} mono={input.mono ?? false} - tuiConfig={config} miniSettings={miniSettings} onSubmit={input.onSubmit ?? (() => true)} onPermissionReply={() => {}} @@ -996,27 +994,17 @@ test("direct footer closes settings with ctrl-c instead of arming exit", async ( } }) -test("selectedCommand backfills the catalog source for bound drafts", () => { - const catalog = [command({ name: "opencode-ts", description: "TS skill", source: "skill" })] - - // The skill picker binds `/name ` drafts; older drafts may lack source. - expect(selectedCommand("/opencode-ts fix it", { name: "opencode-ts", arguments: "" }, catalog)).toEqual({ - name: "opencode-ts", - arguments: "fix it", - source: "skill", - }) - // An explicit source wins without a catalog lookup. +test("selectedCommand validates the bound command and refreshes its arguments", () => { expect(selectedCommand("/opencode-ts", { name: "opencode-ts", arguments: "", source: "skill" })).toEqual({ name: "opencode-ts", arguments: "", source: "skill", }) - // Plain commands stay untagged. - expect( - selectedCommand("/deploy prod", { name: "deploy", arguments: "" }, [ - command({ name: "deploy", description: "Deploy" }), - ]), - ).toEqual({ name: "deploy", arguments: "prod" }) + expect(selectedCommand("/deploy prod", { name: "deploy", arguments: "" })).toEqual({ + name: "deploy", + arguments: "prod", + }) + expect(selectedCommand("/other", { name: "deploy", arguments: "" })).toBeUndefined() }) test("direct footer tags skill slash submissions with their catalog source", async () => { @@ -1162,7 +1150,6 @@ test("direct footer shows authoritative pending work while running", async () => }, ]} theme={() => RUN_THEME_FALLBACK} - tuiConfig={tuiConfig} miniSettings={() => ({ thinking: "hide", shell_output: "hide", turn_summary: "show", footer: "show", mono: false })} mono={false} onSubmit={() => true} diff --git a/packages/tui/test/mini/runtime.boot.test.ts b/packages/tui/test/mini/runtime.boot.test.ts index 8d85cfa1f0fe..431b5bdc0198 100644 --- a/packages/tui/test/mini/runtime.boot.test.ts +++ b/packages/tui/test/mini/runtime.boot.test.ts @@ -3,7 +3,7 @@ import { OpenCode } from "@opencode-ai/client/promise" import type { Resolved } from "../../src/config" import { resolveMiniSettings, resolveModelInfo, resolveRunTuiConfig } from "../../src/mini/runtime.boot" import { catalogModel, catalogProvider } from "./fixture/catalog" -import { createTuiResolvedConfig } from "./fixture/tui-runtime" +import { createTuiResolvedConfig } from "../fixture/tui-runtime" function config(input?: { leader?: string @@ -21,7 +21,7 @@ function config(input?: { }): Resolved { const bind = input?.bindings return createTuiResolvedConfig({ - leader_timeout: input?.leaderTimeout, + leader: input?.leaderTimeout === undefined ? undefined : { timeout: input.leaderTimeout }, keybinds: { ...(input?.leader && { leader: input.leader }), ...(bind?.commandList && { command_list: bind.commandList }), @@ -95,14 +95,12 @@ describe("run runtime boot", () => { const result = await resolveRunTuiConfig( createTuiResolvedConfig({ theme: { mode: "light" }, - leader_timeout: 450, - session: { thinking: "show" }, + leader: { timeout: 450 }, }), ) expect(result.theme).toEqual({ mode: "light" }) expect(result.leader.timeout).toBe(450) - expect(result.session?.thinking).toBe("show") expect(resolveMiniSettings(result)).toEqual({ thinking: "hide", shell_output: "hide", diff --git a/packages/tui/test/mini/runtime.test.ts b/packages/tui/test/mini/runtime.test.ts index 26acbb64d3d0..6aff69274d04 100644 --- a/packages/tui/test/mini/runtime.test.ts +++ b/packages/tui/test/mini/runtime.test.ts @@ -5,7 +5,6 @@ import type { LifecycleInput } from "../../src/mini/runtime.lifecycle" import type { FooterEvent, MiniHost } from "../../src/mini/types" import { catalogModel, catalogProvider, stubCatalogLists } from "./fixture/catalog" import { createFooterApiFixture } from "./fixture/footer-api" -import { createTuiResolvedConfig } from "./fixture/tui-runtime" function defer() { let resolve!: (value: T | PromiseLike) => void diff --git a/packages/tui/test/mini/tool.test.ts b/packages/tui/test/mini/tool.test.ts index 4ebd16c8168a..86c40d1492a2 100644 --- a/packages/tui/test/mini/tool.test.ts +++ b/packages/tui/test/mini/tool.test.ts @@ -33,7 +33,7 @@ describe("Mini tool presentation", () => { type: "update", filePath: "/tmp/project/src/a.ts", relativePath: "src/a.ts", - diff: "@@ -1 +1 @@\n-old\n+new", + patch: "@@ -1 +1 @@\n-old\n+new", }, ], }, From dc9fd126a0b55a0f4c6cc125a045bd604d2e5a97 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Wed, 22 Jul 2026 11:16:14 +0200 Subject: [PATCH 017/150] test(cli): cover interactive command flags (#38273) --- packages/cli/test/mini.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/mini.test.ts b/packages/cli/test/mini.test.ts index ba2c51ab0450..34cf2a2cd275 100644 --- a/packages/cli/test/mini.test.ts +++ b/packages/cli/test/mini.test.ts @@ -121,11 +121,12 @@ describe("mini command", () => { expect(result.stdout).toContain("run Run OpenCode with a message") }) - test("exposes run without legacy attach or command modes", async () => { + test("exposes run without legacy interactive, attach, or command modes", async () => { const result = await cli(["run", "--help"]) expect(result.exitCode).toBe(0) expect(result.stdout).toContain("--server string") + expect(result.stdout).not.toContain("--interactive") expect(result.stdout).not.toContain("--variant") expect(result.stdout).not.toContain("--attach") expect(result.stdout).not.toContain("--command") @@ -212,6 +213,7 @@ describe("mini command", () => { expect(result.exitCode).toBe(0) expect(result.stdout).toContain("--server string") + expect(result.stdout).toContain("--prompt string") expect(result.stdout).not.toContain("SUBCOMMANDS") }) From 58d18be59084bf46bdebfbd29e14e845ca52d66a Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Wed, 22 Jul 2026 11:50:21 +0200 Subject: [PATCH 018/150] oc mini v2 (#38278) --- packages/tui/src/mini/runtime.ts | 72 ++++++++++++-- packages/tui/src/mini/stream-v2.transport.ts | 14 ++- packages/tui/test/mini/runtime.test.ts | 97 +++++++++++++++++++ .../tui/test/mini/stream-v2.transport.test.ts | 7 +- 4 files changed, 178 insertions(+), 12 deletions(-) diff --git a/packages/tui/src/mini/runtime.ts b/packages/tui/src/mini/runtime.ts index dc12edd3152e..a8fbb8b5565b 100644 --- a/packages/tui/src/mini/runtime.ts +++ b/packages/tui/src/mini/runtime.ts @@ -115,6 +115,7 @@ type RuntimeState = { shown: boolean aborting: boolean model: RunInput["model"] + defaultModel: RunInput["model"] providers: RunProvider[] variants: string[] activeVariant: string | undefined @@ -212,6 +213,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep shown: !session.first, aborting: false, model: ctx.model ?? session.model, + defaultModel: undefined, providers: [], variants: [], activeVariant: resolveVariant(ctx.variant, session.variant, savedVariant, []), @@ -286,17 +288,19 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep await settleForm(next.sessionID, next.formID) }, onCycleVariant: () => { - if (!state.model || state.variants.length === 0) { + const model = state.model ?? state.defaultModel + if (!model || state.variants.length === 0) { return { status: "no variants available", } } + if (!state.model) state.model = model state.activeVariant = cycleVariant(state.activeVariant, state.variants) - void input.host.preferences.saveVariant(state.model, state.activeVariant) + void input.host.preferences.saveVariant(model, state.activeVariant) return { status: state.activeVariant ? `variant ${state.activeVariant}` : "variant default", - modelLabel: formatModelLabel(state.model, state.activeVariant, state.providers), + modelLabel: formatModelLabel(model, state.activeVariant, state.providers), variant: state.activeVariant, } }, @@ -335,7 +339,8 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep } }, onVariantSelect: async (variant) => { - if (!state.model || state.variants.length === 0) { + const model = state.model ?? state.defaultModel + if (!model || state.variants.length === 0) { return { status: "no variants available", } @@ -347,11 +352,12 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep } } + if (!state.model) state.model = model state.activeVariant = variant - void input.host.preferences.saveVariant(state.model, state.activeVariant) + void input.host.preferences.saveVariant(model, state.activeVariant) return { status: state.activeVariant ? `variant ${state.activeVariant}` : "variant default", - modelLabel: formatModelLabel(state.model, state.activeVariant, state.providers), + modelLabel: formatModelLabel(model, state.activeVariant, state.providers), variant: state.activeVariant, variants: state.variants, } @@ -602,7 +608,8 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep ) { if (!currentClient(attempt)) return state.providers = info.providers - state.variants = variantsFor(state.providers, state.model) + const model = state.model ?? state.defaultModel + state.variants = variantsFor(state.providers, model) state.activeVariant = boot ? resolveVariant(ctx.variant, current, saved, state.variants) : current && !state.variants.includes(current) @@ -611,11 +618,11 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep if (footer.isClosed) return footer.event({ type: "models", providers: info.providers }) footer.event({ type: "variants", variants: state.variants, current: state.activeVariant }) - if (state.model) + if (model) footer.event({ type: "model", - model: formatModelLabel(state.model, state.activeVariant, state.providers), - selection: state.model, + model: formatModelLabel(model, state.activeVariant, state.providers), + selection: model, }) } @@ -627,6 +634,50 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep task: Promise } | undefined + let defaultModelLoad: Promise | undefined + let defaultModelQueued = false + const loadDefaultModel = (attempt: ClientAttempt) => { + if (state.model || !currentClient(attempt)) return + if (defaultModelLoad) { + defaultModelQueued = true + return + } + defaultModelQueued = false + defaultModelLoad = attempt.sdk.model + .default( + { + location: { + directory: state.location.directory, + workspace: state.location.workspaceID, + }, + }, + { signal: attempt.signal }, + ) + .then(async (result) => { + if (!result.data || state.model || !currentClient(attempt)) return + const model = { providerID: result.data.providerID, modelID: result.data.id } + const changed = + state.defaultModel?.providerID !== model.providerID || state.defaultModel.modelID !== model.modelID + const saved = changed ? await input.host.preferences.resolveVariant(model) : undefined + if (state.model || !currentClient(attempt)) return + state.defaultModel = model + state.variants = variantsFor(state.providers, model) + if (changed) + state.activeVariant = resolveVariant(ctx.variant, state.activeVariant, saved, state.variants) + if (state.activeVariant) state.model = model + footer.event({ type: "variants", variants: state.variants, current: state.activeVariant }) + footer.event({ + type: "model", + model: formatModelLabel(model, state.activeVariant, state.providers), + selection: model, + }) + }) + .catch(() => {}) + .finally(() => { + defaultModelLoad = undefined + if (defaultModelQueued) loadDefaultModel(clientAttempt()) + }) + } const requestCatalogRefresh = (signal?: AbortSignal): Promise => { const attempt = clientAttempt(signal) if (!currentClient(attempt)) return Promise.resolve() @@ -658,6 +709,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep if (!currentClient(attempt)) return if (catalog) applyCatalog(catalog, attempt) if (info) applyModelInfo(info, state.activeVariant, attempt) + loadDefaultModel(attempt) } })() refresh.task = task diff --git a/packages/tui/src/mini/stream-v2.transport.ts b/packages/tui/src/mini/stream-v2.transport.ts index b8aab5ee2491..7fcd3054f96d 100644 --- a/packages/tui/src/mini/stream-v2.transport.ts +++ b/packages/tui/src/mini/stream-v2.transport.ts @@ -380,7 +380,19 @@ async function resolveSelectedModel( .then((response) => response.model) if (session) return { ...session, variant: next.variant } - const fallback = await sdk.model.default(undefined, { signal: next.signal }).then((response) => response.data) + const fallback = await sdk.model + .default( + input.location + ? { + location: { + directory: input.location.directory, + workspace: input.location.workspaceID, + }, + } + : undefined, + { signal: next.signal }, + ) + .then((response) => response.data) if (!fallback) return return { providerID: fallback.providerID, id: fallback.id, variant: next.variant } } diff --git a/packages/tui/test/mini/runtime.test.ts b/packages/tui/test/mini/runtime.test.ts index 6aff69274d04..1955b4a4700e 100644 --- a/packages/tui/test/mini/runtime.test.ts +++ b/packages/tui/test/mini/runtime.test.ts @@ -48,6 +48,103 @@ afterEach(() => { }) describe("run interactive runtime", () => { + test("resolves the default model reactively without blocking catalog startup", async () => { + const sdk = OpenCode.make({ baseUrl: "https://opencode.test" }) + const events: FooterEvent[] = [] + const ui = createFooterApiFixture({ events }) + const api = ui.api + const selected = defer>>() + const catalogLoaded = defer() + const model = catalogModel({ + id: "resolved", + providerID: "test", + name: "Resolved Model", + variants: ["low", "high"], + }) + let lifecycle!: LifecycleInput + let turnModel: { providerID: string; modelID: string } | undefined + let refreshCatalog: (() => Promise) | undefined + stubCatalogLists(sdk, { + providers: [catalogProvider("test", "Test Provider")], + models: [model], + }) + const defaultModel = spyOn(sdk.model, "default").mockImplementation(() => selected.promise) + + const task = runInteractiveDeferredMode( + { + host: host(), + sdk, + directory: "/tmp", + target: async () => ({ + sessionID: "ses_root", + location: { directory: "/tmp", project: { id: "pro-1", directory: "/tmp" } }, + agent: "build", + model: undefined, + variant: undefined, + resume: false, + }), + agent: "build", + model: undefined, + variant: undefined, + files: [], + }, + { + createRuntimeLifecycle: async (input) => { + lifecycle = input + return { + footer: api, + onResize: () => () => {}, + refreshTheme: () => {}, + setTitle: () => {}, + resetForReplay: () => Promise.resolve(), + close: () => Promise.resolve(), + } + }, + streamTransport: Promise.resolve({ + createSessionTransport: async (input) => { + refreshCatalog = () => Promise.resolve(input.onCatalogRefresh?.()) + await refreshCatalog() + catalogLoaded.resolve() + return { + runPromptTurn: async (input) => { + turnModel = input.model + api.close() + }, + queuePromptTurn: async () => {}, + waitForIdle: async () => {}, + interruptActiveTurn: async () => {}, + selectSubagent: () => {}, + replayOnResize: async () => false, + close: async () => {}, + } + }, + formatUnknownError: (error: unknown) => String(error), + }), + }, + ) + + await catalogLoaded.promise + expect(events.some((event) => event.type === "model")).toBe(false) + await refreshCatalog?.() + expect(defaultModel).toHaveBeenCalledTimes(1) + selected.resolve({ + location: { directory: "/tmp", project: { id: "pro-1", directory: "/tmp" } }, + data: model, + }) + while (defaultModel.mock.calls.length < 2) await Bun.sleep(0) + while (!events.some((event) => event.type === "model")) await Bun.sleep(0) + expect(events).toContainEqual({ + type: "model", + model: "Resolved Model · Test Provider", + selection: { providerID: "test", modelID: "resolved" }, + }) + expect(lifecycle.onCycleVariant?.()).toMatchObject({ status: "variant low", variant: "low" }) + ui.submit("hello") + while (!turnModel) await Bun.sleep(0) + expect(turnModel).toEqual({ providerID: "test", modelID: "resolved" }) + await task + }) + test("routes form responses to their owners with global location and local settlement", async () => { const sdk = OpenCode.make({ baseUrl: "https://opencode.test" }) const api = footer() diff --git a/packages/tui/test/mini/stream-v2.transport.test.ts b/packages/tui/test/mini/stream-v2.transport.test.ts index 18bbc0b08c75..98ff08aac76b 100644 --- a/packages/tui/test/mini/stream-v2.transport.test.ts +++ b/packages/tui/test/mini/stream-v2.transport.test.ts @@ -2085,12 +2085,13 @@ describe("V2 mini transport", () => { const ui = footer() const transport = await createSessionTransport({ sdk: client, + location: { directory: "/project", workspaceID: "wrk_1" }, sessionID: "ses_1", thinking: false, footer: ui.api, }) spyOn(client.session, "get").mockImplementation(() => ok({ model: undefined }) as never) - spyOn(client.model, "default").mockImplementation( + const defaultModel = spyOn(client.model, "default").mockImplementation( () => ok({ location: { directory: "/tmp", project: { id: "proj_1", directory: "/tmp" } }, @@ -2138,6 +2139,10 @@ describe("V2 mini transport", () => { { sessionID: "ses_1", model: { providerID: "openai", id: "gpt-5", variant: "high" } }, { signal: undefined }, ) + expect(defaultModel).toHaveBeenCalledWith( + { location: { directory: "/project", workspace: "wrk_1" } }, + { signal: undefined }, + ) await transport.close() }) From 648183cecbde61d895c4674a0326a065a4660473 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Wed, 22 Jul 2026 12:21:43 +0200 Subject: [PATCH 019/150] fix(tui): quiet hidden mini footer (#38279) --- packages/tui/src/mini/footer.ts | 43 ++++--------------- packages/tui/src/mini/footer.view.tsx | 31 +++---------- packages/tui/src/mini/types.ts | 1 + packages/tui/test/mini/footer-keymap.test.tsx | 1 + packages/tui/test/mini/footer.view.test.tsx | 16 +++++-- 5 files changed, 31 insertions(+), 61 deletions(-) diff --git a/packages/tui/src/mini/footer.ts b/packages/tui/src/mini/footer.ts index 2673cc5e6f45..cc5520b4dbe4 100644 --- a/packages/tui/src/mini/footer.ts +++ b/packages/tui/src/mini/footer.ts @@ -194,8 +194,6 @@ export class RunFooter implements FooterApi { private interruptTimeout: NodeJS.Timeout | undefined private exitTimeout: NodeJS.Timeout | undefined private noticeTimeout: NodeJS.Timeout | undefined - private noticeRestoreStatus = "" - private statusVersion = 0 private requestExitHandler: (() => boolean) | undefined private scrollback: RunScrollbackStream private themes: RunTheme[] @@ -225,6 +223,7 @@ export class RunFooter implements FooterApi { const [state, setState] = createSignal({ phase: "idle", status: "", + notice: "", model: options.modelLabel, usage: "", first: options.first, @@ -449,6 +448,7 @@ export class RunFooter implements FooterApi { if (patch) { if (typeof patch.status === "string") { this.clearNoticeTimer() + patch.notice = "" } if (next.type === "turn.send") { this.clearInterruptTimer() @@ -479,12 +479,10 @@ export class RunFooter implements FooterApi { } const prev = this.state() - if (typeof next.status === "string") { - this.statusVersion++ - } const state = { phase: next.phase ?? prev.phase, status: typeof next.status === "string" ? next.status : prev.status, + notice: typeof next.notice === "string" ? next.notice : prev.notice, model: typeof next.model === "string" ? next.model : prev.model, usage: typeof next.usage === "string" ? next.usage : prev.usage, first: typeof next.first === "boolean" ? next.first : prev.first, @@ -635,26 +633,13 @@ export class RunFooter implements FooterApi { } private setNotice(status: string): void { - const restore = this.noticeTimeout ? this.noticeRestoreStatus : this.state().status - this.clearNoticeTimer(false) - this.patch({ status }) - if (!status) { - this.noticeRestoreStatus = "" - return - } + this.clearNoticeTimer() + this.patch({ notice: status }) + if (!status) return - this.noticeRestoreStatus = restore - const version = this.statusVersion this.noticeTimeout = setTimeout(() => { this.noticeTimeout = undefined - if (this.isGone || version !== this.statusVersion) { - this.noticeRestoreStatus = "" - return - } - - const next = this.noticeRestoreStatus - this.noticeRestoreStatus = "" - this.patch({ status: next }) + this.patch({ notice: "" }) }, NOTICE_DURATION) } @@ -895,19 +880,9 @@ export class RunFooter implements FooterApi { this.interruptTimeout = undefined } - private clearNoticeTimer(reset = true): void { - if (!this.noticeTimeout) { - if (reset) { - this.noticeRestoreStatus = "" - } - return - } - - clearTimeout(this.noticeTimeout) + private clearNoticeTimer(): void { + if (this.noticeTimeout) clearTimeout(this.noticeTimeout) this.noticeTimeout = undefined - if (reset) { - this.noticeRestoreStatus = "" - } } private armInterruptTimer(): void { diff --git a/packages/tui/src/mini/footer.view.tsx b/packages/tui/src/mini/footer.view.tsx index dfc326f4b456..3a6b622bfaa3 100644 --- a/packages/tui/src/mini/footer.view.tsx +++ b/packages/tui/src/mini/footer.view.tsx @@ -55,8 +55,6 @@ import type { RunTheme } from "./theme" registerOpencodeSpinner() -const FOOTER_DETAIL_DURATION = 3000 - const EMPTY_BORDER = { topLeft: "", bottomLeft: "", @@ -228,23 +226,6 @@ export function RunFooterView(props: RunFooterViewProps) { if (activeTabs().length > 0) details.push(`${activeTabs().length} subagent${activeTabs().length === 1 ? "" : "s"}`) return details.join(props.mono ? " - " : " · ") }) - const [footerNotice, setFooterNotice] = createSignal("") - let footerNoticeTimeout: ReturnType | undefined - let previousFooterStatus: string | undefined - const showFooterStatus = () => { - if (footerNoticeTimeout) clearTimeout(footerNoticeTimeout) - setFooterNotice(footerStatus()) - footerNoticeTimeout = setTimeout(() => { - footerNoticeTimeout = undefined - setFooterNotice("") - }, FOOTER_DETAIL_DURATION) - } - - createEffect(() => { - const current = footerStatus() - if (previousFooterStatus !== undefined && previousFooterStatus !== current && !footerDetails()) showFooterStatus() - previousFooterStatus = current - }) const permission = createMemo | undefined>(() => { const view = active() return view.type === "permission" ? view : undefined @@ -379,6 +360,7 @@ export function RunFooterView(props: RunFooterViewProps) { const shell = createMemo(() => prompt() && composer.shell()) const menu = createMemo(() => prompt() && composer.visible()) const stateStatus = createMemo(() => props.state().status.trim()) + const notice = createMemo(() => props.state().notice.trim()) const modeLabel = createMemo(() => { if (exiting()) { return "EXIT" @@ -404,7 +386,9 @@ export function RunFooterView(props: RunFooterViewProps) { if (busy() && armed()) return "again to interrupt" - if (footerNotice()) return footerNotice() + if (notice()) return notice() + + if (!footerDetails()) return shell() ? "Shell mode" : "" if (busy()) return "interrupt" @@ -438,7 +422,7 @@ export function RunFooterView(props: RunFooterViewProps) { return theme().highlight } - if (busy() || footerNotice().length > 0 || stateStatus().length > 0) { + if (busy() || notice().length > 0 || stateStatus().length > 0) { return theme().text } @@ -488,7 +472,6 @@ export function RunFooterView(props: RunFooterViewProps) { onCleanup(() => { props.onRequestExit?.(undefined) - if (footerNoticeTimeout) clearTimeout(footerNoticeTimeout) }) Keymap.createLayer(() => ({ @@ -730,7 +713,7 @@ export function RunFooterView(props: RunFooterViewProps) { closePanel() }} onStatus={() => { - showFooterStatus() + props.onStatus(footerStatus()) closePanel() }} onCommand={(name) => { @@ -898,7 +881,7 @@ export function RunFooterView(props: RunFooterViewProps) { - + {(label) => {label()} } diff --git a/packages/tui/src/mini/types.ts b/packages/tui/src/mini/types.ts index 3d7920f93e3a..0bafcbbe5dd6 100644 --- a/packages/tui/src/mini/types.ts +++ b/packages/tui/src/mini/types.ts @@ -166,6 +166,7 @@ type FooterPhase = "idle" | "running" export type FooterState = { phase: FooterPhase status: string + notice: string model: string usage: string first: boolean diff --git a/packages/tui/test/mini/footer-keymap.test.tsx b/packages/tui/test/mini/footer-keymap.test.tsx index da2513dba4b4..5bd264fa4208 100644 --- a/packages/tui/test/mini/footer-keymap.test.tsx +++ b/packages/tui/test/mini/footer-keymap.test.tsx @@ -12,6 +12,7 @@ test("down opens subagents from an empty prompt", async () => { const [state] = createSignal({ phase: "idle", status: "", + notice: "", model: "gpt-5", usage: "", first: false, diff --git a/packages/tui/test/mini/footer.view.test.tsx b/packages/tui/test/mini/footer.view.test.tsx index 38eabf85ae9c..418689f14c62 100644 --- a/packages/tui/test/mini/footer.view.test.tsx +++ b/packages/tui/test/mini/footer.view.test.tsx @@ -94,6 +94,7 @@ function footerState(input: Partial = {}) { return createSignal({ phase: "idle", status: "", + notice: "", model: "gpt-5", usage: "", first: false, @@ -1109,6 +1110,7 @@ test("direct footer shows authoritative pending work while running", async () => const [state] = createSignal({ phase: "running", status: "", + notice: "", model: "gpt-5", usage: "", first: false, @@ -1324,7 +1326,7 @@ test("direct footer shows full usage metadata when room is available", async () } }) -test("direct footer can hide persistent details and briefly reveal changes", async () => { +test("direct footer hides routine activity and shows explicit notices", async () => { const app = await renderFooter({ state: { usage: "159.6K (16%) · $4.23" }, miniSettings: { @@ -1345,13 +1347,21 @@ test("direct footer can hide persistent details and briefly reveal changes", asy expect(initial).not.toContain("gpt-5") expect(initial).not.toContain("159.6K") - app.setState((state) => ({ ...state, phase: "running" })) + app.setState((state) => ({ ...state, phase: "running", status: "assistant responding" })) await app.renderOnce() const changed = app.captureCharFrame() const statusline = footerStatusline(app.renderer.root) - expect(changed).toContain("running - gpt-5 - 159.6K (16%) - $4.23") + expect(changed).not.toContain("running") + expect(changed).not.toContain("assistant responding") + expect(changed).not.toContain("interrupt") + expect(changed).not.toContain("gpt-5") + expect(changed).not.toContain("159.6K") expect(boxPath(statusline, "SpinnerRenderable")).toBeUndefined() + + app.setState((state) => ({ ...state, notice: "variant high" })) + await app.renderOnce() + expect(app.captureCharFrame()).toContain("variant high") } finally { app.cleanup() } From a3e2cc0dcd2f9bf8ec3c2a29de60a4af24590880 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Wed, 22 Jul 2026 13:14:10 +0200 Subject: [PATCH 020/150] feat(tui): add mini agent switching (#38287) Fixes #36121 --- packages/cli/src/session-target.ts | 2 +- packages/cli/test/session-target.test.ts | 9 ++ packages/tui/src/mini/catalog.shared.ts | 1 + packages/tui/src/mini/footer.command.tsx | 79 +++++++++++++++++ packages/tui/src/mini/footer.ts | 34 +++++++- packages/tui/src/mini/footer.view.tsx | 35 +++++++- packages/tui/src/mini/footer.width.ts | 1 + packages/tui/src/mini/runtime.lifecycle.ts | 24 +----- packages/tui/src/mini/runtime.ts | 3 + packages/tui/src/mini/stream-v2.transport.ts | 4 + packages/tui/src/mini/types.ts | 2 + packages/tui/test/mini/footer-keymap.test.tsx | 3 + packages/tui/test/mini/footer.view.test.tsx | 86 ++++++++++++++++++- packages/tui/test/mini/footer.width.test.ts | 2 + packages/tui/test/mini/runtime.test.ts | 4 + .../tui/test/mini/stream-v2.transport.test.ts | 12 ++- 16 files changed, 268 insertions(+), 33 deletions(-) diff --git a/packages/cli/src/session-target.ts b/packages/cli/src/session-target.ts index 40412473ace9..975f4829b217 100644 --- a/packages/cli/src/session-target.ts +++ b/packages/cli/src/session-target.ts @@ -74,7 +74,7 @@ export async function resolveSessionTarget(input: { session, location, model: prepared.model, - agent: prepared.agent, + agent: prepared.agent ?? session.agent, resume: selected !== undefined, } } diff --git a/packages/cli/test/session-target.test.ts b/packages/cli/test/session-target.test.ts index fef8faf72a08..ea34aada7f4c 100644 --- a/packages/cli/test/session-target.test.ts +++ b/packages/cli/test/session-target.test.ts @@ -78,6 +78,15 @@ describe("session target resolver", () => { expect(order).toEqual(["prepare", "create"]) }) + test("uses the agent resolved by the server for a fresh Session", async () => { + const client = OpenCode.make({ baseUrl: "https://opencode.test" }) + spyOn(client.location, "get").mockResolvedValue(location("/project")) + spyOn(client.session, "create").mockResolvedValue({ ...session("ses_fresh", "/project"), agent: "review" }) + + const target = await resolveSessionTarget({ client, prepare }) + expect(target.agent).toBe("review") + }) + test("does not retry an ambiguous Session creation", async () => { const client = OpenCode.make({ baseUrl: "https://opencode.test" }) spyOn(client.location, "get").mockResolvedValue(location("/project")) diff --git a/packages/tui/src/mini/catalog.shared.ts b/packages/tui/src/mini/catalog.shared.ts index 941e44d9c529..2210cb3eca39 100644 --- a/packages/tui/src/mini/catalog.shared.ts +++ b/packages/tui/src/mini/catalog.shared.ts @@ -34,6 +34,7 @@ function runAgent(input: CurrentAgent): RunAgent { return { id: input.id, name: input.name, + description: input.description, mode: input.mode, hidden: input.hidden, } diff --git a/packages/tui/src/mini/footer.command.tsx b/packages/tui/src/mini/footer.command.tsx index 4dff4f8112e5..48c11f951c05 100644 --- a/packages/tui/src/mini/footer.command.tsx +++ b/packages/tui/src/mini/footer.command.tsx @@ -10,6 +10,7 @@ import type { FooterSubagentTab, MiniSettingChange, MiniSettings, + RunAgent, RunCommand, RunInput, RunProvider, @@ -21,6 +22,7 @@ type PanelEntry = RunFooterMenuItem & { } type CommandEntry = + | (PanelEntry & { action: "agent" }) | (PanelEntry & { action: "model" }) | (PanelEntry & { action: "editor" }) | (PanelEntry & { action: "skill" }) @@ -40,6 +42,11 @@ type ModelEntry = PanelEntry & { current: boolean } +type AgentEntry = PanelEntry & { + id: string + current: boolean +} + type VariantEntry = PanelEntry & { variant: string | undefined current: boolean @@ -341,6 +348,7 @@ export function RunCommandMenuBody(props: { variants: Accessor variantCycle: string onClose: () => void + onAgent: () => void onModel: () => void onEditor: () => void onSkill: () => void @@ -419,6 +427,11 @@ export function RunCommandMenuBody(props: { ] : [] const agent: CommandEntry[] = [ + { + action: "agent", + category: "Agent", + display: "Switch agent", + }, { action: "model", category: "Agent", @@ -471,6 +484,11 @@ export function RunCommandMenuBody(props: { ] }) const pick = (item: CommandEntry) => { + if (item.action === "agent") { + props.onAgent() + return + } + if (item.action === "model") { props.onModel() return @@ -568,6 +586,67 @@ export function RunCommandMenuBody(props: { ) } +export function RunAgentSelectBody(props: { + theme: Accessor + agents: Accessor + current: Accessor + onClose: () => void + onSelect: (agent: string) => void + mono?: boolean +}) { + const entries = createMemo(() => + props + .agents() + .filter((agent) => agent.mode !== "subagent" && !agent.hidden) + .map((agent) => ({ + category: "", + display: agent.id, + description: agent.description, + footer: props.current() === agent.id ? "current" : undefined, + keywords: `${agent.id} ${agent.name} ${agent.description ?? ""}`, + id: agent.id, + current: props.current() === agent.id, + })), + ) + const controller = createSearchablePanelController({ + entries, + limit: PANEL_LIST_ROWS, + onClose: props.onClose, + onSelect: (item) => props.onSelect(item.id), + isCurrent: (item) => item.current, + }) + + return ( + + PANEL_LIST_ROWS} + limit={PANEL_LIST_ROWS} + empty="No agents found" + border={false} + paddingLeft={panelPad(props.mono)} + paddingRight={panelPad(props.mono)} + grouped={false} + background + mono={props.mono} + /> + + ) +} + export function RunSettingsBody(props: { theme: Accessor settings: Accessor diff --git a/packages/tui/src/mini/footer.ts b/packages/tui/src/mini/footer.ts index cc5520b4dbe4..54b50953bf2e 100644 --- a/packages/tui/src/mini/footer.ts +++ b/packages/tui/src/mini/footer.ts @@ -74,7 +74,7 @@ type RunFooterOptions = { agents: RunAgent[] references: RunReference[] wrote?: boolean - agentLabel: string + agent: string | undefined modelLabel: string model: RunInput["model"] variant: string | undefined @@ -91,6 +91,7 @@ type RunFooterOptions = { onFormReply: (input: FormReply) => void | Promise onFormCancel: (input: FormCancel) => void | Promise onCycleVariant?: () => CycleResult | void + onAgentSelect?: (agent: string) => void onModelSelect?: (model: NonNullable) => CycleResult | void | Promise onVariantSelect?: (variant: string | undefined) => CycleResult | void | Promise onInterrupt?: () => void @@ -169,6 +170,9 @@ export class RunFooter implements FooterApi { private setCommands: Setter private providers: Accessor private setProviders: Setter + private currentAgent: Accessor + private currentAgentID: Accessor + private setCurrentAgentID: Setter private currentModel: Accessor private setCurrentModel: Setter private variants: Accessor @@ -194,6 +198,7 @@ export class RunFooter implements FooterApi { private interruptTimeout: NodeJS.Timeout | undefined private exitTimeout: NodeJS.Timeout | undefined private noticeTimeout: NodeJS.Timeout | undefined + private turnAgent: string | undefined private requestExitHandler: (() => boolean) | undefined private scrollback: RunScrollbackStream private themes: RunTheme[] @@ -247,6 +252,14 @@ export class RunFooter implements FooterApi { const [providers, setProviders] = createSignal() this.providers = providers this.setProviders = setProviders + const [currentAgentID, setCurrentAgentID] = createSignal(options.agent) + this.currentAgentID = currentAgentID + this.setCurrentAgentID = setCurrentAgentID + this.currentAgent = () => { + const agent = currentAgentID() + if (!agent) return "Default" + return this.agents().find((item) => item.id === agent)?.name ?? Locale.titlecase(agent) + } const [currentModel, setCurrentModel] = createSignal(options.model) this.currentModel = currentModel this.setCurrentModel = setCurrentModel @@ -305,6 +318,8 @@ export class RunFooter implements FooterApi { references: footer.references, commands: footer.commands, providers: footer.providers, + currentAgent: footer.currentAgent, + currentAgentID: footer.currentAgentID, currentModel: footer.currentModel, variants: footer.variants, currentVariant: footer.currentVariant, @@ -324,6 +339,7 @@ export class RunFooter implements FooterApi { onExitRequest: footer.handleExit, onRequestExit: footer.setRequestExitHandler, onExit: () => footer.close(), + onAgentSelect: footer.handleAgentSelect, onModelSelect: footer.handleModelSelect, onVariantSelect: footer.handleVariantSelect, onRows: footer.syncRows, @@ -377,7 +393,7 @@ export class RunFooter implements FooterApi { } if (next.type === "agent") { - this.options.agentLabel = Locale.titlecase(next.agent ?? "build") + this.setCurrentAgentID(next.agent) return } @@ -386,13 +402,15 @@ export class RunFooter implements FooterApi { } if (next.type === "turn.duration") { + const agent = this.turnAgent ?? this.currentAgent() + this.turnAgent = undefined if (this.miniSettings().turn_summary === "hide") return const current = this.currentModel() this.flush() this.flushing = this.flushing .then(() => this.scrollback.writeTurnSummary({ - agent: this.options.agentLabel, + agent, model: current ? modelInfo(this.providers(), current).model : this.state().model, duration: next.duration, }), @@ -451,6 +469,7 @@ export class RunFooter implements FooterApi { patch.notice = "" } if (next.type === "turn.send") { + this.turnAgent = this.currentAgent() this.clearInterruptTimer() this.clearExitTimer() } @@ -667,7 +686,7 @@ export class RunFooter implements FooterApi { ? this.base + PERMISSION_ROWS : type === "form" ? this.base + FORM_ROWS - : ["command", "skill", "model", "variant", "settings"].includes(route) + : ["command", "skill", "agent", "model", "variant", "settings"].includes(route) ? 1 + RUN_COMMAND_PANEL_ROWS : route === "queued-menu" || route === "subagent-menu" ? 1 + this.subagentMenuRows @@ -815,6 +834,13 @@ export class RunFooter implements FooterApi { .catch(() => {}) } + private handleAgentSelect = (agent: string): void => { + if (this.isClosed || this.currentAgentID() === agent) return + this.setCurrentAgentID(agent) + this.options.onAgentSelect?.(agent) + this.setNotice(`agent ${this.currentAgent()}`) + } + private handleVariantSelect = (variant: string | undefined): void => { if (this.isClosed) { return diff --git a/packages/tui/src/mini/footer.view.tsx b/packages/tui/src/mini/footer.view.tsx index 3a6b622bfaa3..956168565cf5 100644 --- a/packages/tui/src/mini/footer.view.tsx +++ b/packages/tui/src/mini/footer.view.tsx @@ -14,6 +14,7 @@ import { registerOpencodeSpinner } from "../component/register-spinner" import { createColors, createFrames } from "../ui/spinner" import { RUN_SUBAGENT_PANEL_ROWS, + RunAgentSelectBody, RunCommandMenuBody, RunModelSelectBody, RunQueuedPromptSelectBody, @@ -76,6 +77,8 @@ type RunFooterViewProps = { references: () => RunReference[] commands: () => RunCommand[] | undefined providers: () => RunProvider[] | undefined + currentAgent: () => string + currentAgentID: () => string | undefined currentModel: () => RunInput["model"] variants: () => string[] currentVariant: () => string | undefined @@ -99,6 +102,7 @@ type RunFooterViewProps = { onExitRequest?: () => boolean onRequestExit?: (fn: (() => boolean) | undefined) => void onExit: () => void + onAgentSelect: (agent: string) => void onModelSelect: (model: NonNullable) => void onVariantSelect: (variant: string | undefined) => void onRows: (rows: number) => void @@ -134,6 +138,7 @@ export function RunFooterView(props: RunFooterViewProps) { const inspecting = createMemo(() => active().type === "prompt" && route().type === "subagent") const commanding = createMemo(() => active().type === "prompt" && route().type === "command") const skilling = createMemo(() => active().type === "prompt" && route().type === "skill") + const agenting = createMemo(() => active().type === "prompt" && route().type === "agent") const modeling = createMemo(() => active().type === "prompt" && route().type === "model") const varianting = createMemo(() => active().type === "prompt" && route().type === "variant") const setting = createMemo(() => active().type === "prompt" && route().type === "settings") @@ -145,6 +150,7 @@ export function RunFooterView(props: RunFooterViewProps) { selectingSubagent() || commanding() || skilling() || + agenting() || modeling() || varianting() || setting(), @@ -219,7 +225,7 @@ export function RunFooterView(props: RunFooterViewProps) { const footerStatus = createMemo(() => { const current = model() ?? props.state().model.trim() const variant = props.currentVariant() - const details = [busy() ? "running" : "idle"] + const details = [busy() ? "running" : "idle", `agent ${props.currentAgent()}`] if (current) details.push(variant ? `${current} ${variant}` : current) if (usage()) details.push(props.mono ? usage().replaceAll(" · ", " - ") : usage()) if (queuedPrompts().length > 0) details.push(`${queuedPrompts().length} pending`) @@ -268,6 +274,11 @@ export function RunFooterView(props: RunFooterViewProps) { props.onSubagentSelect?.(undefined) } + const openAgent = () => { + setRoute({ type: "agent" }) + props.onSubagentSelect?.(undefined) + } + const openSkillMenu = () => { if (props.commands() && skills().length === 0) { return @@ -407,8 +418,9 @@ export function RunFooterView(props: RunFooterViewProps) { }) const modelStatus = createMemo(() => { const current = model() ?? props.state().model.trim() - if (!footerDetails() || !prompt() || !responsive().statusline.showModel || !current) return + if (!footerDetails() || !prompt() || shell() || !responsive().statusline.showModel || !current) return return { + agent: props.currentAgent(), model: current, variant: responsive().statusline.showModelVariant ? props.currentVariant() : undefined, } @@ -593,6 +605,7 @@ export function RunFooterView(props: RunFooterViewProps) { if ( current.type !== "command" && current.type !== "skill" && + current.type !== "agent" && current.type !== "model" && current.type !== "variant" && current.type !== "settings" && @@ -698,6 +711,7 @@ export function RunFooterView(props: RunFooterViewProps) { variants={props.variants} variantCycle={variantCycle()} onClose={closePanel} + onAgent={openAgent} onModel={openModel} onEditor={() => { closePanel() @@ -748,6 +762,19 @@ export function RunFooterView(props: RunFooterViewProps) { mono={props.mono} /> + + { + props.onAgentSelect(agent) + closePanel() + }} + mono={props.mono} + /> + + + {info().agent} + {props.mono ? " - " : " · "} + {info().model} {(variant) => {variant()}} diff --git a/packages/tui/src/mini/footer.width.ts b/packages/tui/src/mini/footer.width.ts index 35670d0ef702..e7fd07b5bf91 100644 --- a/packages/tui/src/mini/footer.width.ts +++ b/packages/tui/src/mini/footer.width.ts @@ -20,6 +20,7 @@ export function footerWidthPolicy(width: number) { }, statusline: { showActivityMeta: compact, + showAgent: compact, showCommandHint: width >= FOOTER_WIDTH_BREAKPOINTS.commandHint, showModel: width >= FOOTER_WIDTH_BREAKPOINTS.model, showModelVariant: width >= FOOTER_WIDTH_BREAKPOINTS.modelVariant, diff --git a/packages/tui/src/mini/runtime.lifecycle.ts b/packages/tui/src/mini/runtime.lifecycle.ts index 9c43d7fd6f3a..8bc68066899f 100644 --- a/packages/tui/src/mini/runtime.lifecycle.ts +++ b/packages/tui/src/mini/runtime.lifecycle.ts @@ -11,7 +11,6 @@ import path from "path" import { CliRenderEvents, createCliRenderer, type CliRenderer, type ScrollbackWriter } from "@opentui/core" import { isDefaultTitle } from "../util/session" -import { Locale } from "../util/locale" import { entrySplash, exitSplash, splashMeta } from "./splash" import { resolveRunTheme } from "./theme" import type { @@ -45,11 +44,6 @@ type CycleResult = { variants?: string[] } -type FooterLabels = { - agentLabel: string - modelLabel: string -} - export type LifecycleInput = { host: MiniHost getDirectory: () => string @@ -70,6 +64,7 @@ export type LifecycleInput = { onFormReply: (input: FormReply) => void | Promise onFormCancel: (input: FormCancel) => void | Promise onCycleVariant?: () => CycleResult | void + onAgentSelect?: (agent: string) => void onModelSelect?: (model: NonNullable) => CycleResult | void | Promise onVariantSelect?: (variant: string | undefined) => CycleResult | void | Promise onInterrupt?: () => void @@ -123,14 +118,6 @@ function splashInfo(title: string | undefined, history: RunPrompt[]) { } } -function footerLabels(input: Pick): FooterLabels { - const agentLabel = Locale.titlecase(input.agent ?? "build") - return { - agentLabel, - modelLabel: input.model ? formatModelLabel(input.model, input.variant) : "Default model", - } -} - function directoryLabel(directory: string, home: string) { const resolved = path.resolve(directory) const display = @@ -203,11 +190,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise { + state.agent = agent + }, onModelSelect: async (model) => { if (state.model?.providerID === model.providerID && state.model.modelID === model.modelID) { return diff --git a/packages/tui/src/mini/stream-v2.transport.ts b/packages/tui/src/mini/stream-v2.transport.ts index 7fcd3054f96d..964d78e1efb3 100644 --- a/packages/tui/src/mini/stream-v2.transport.ts +++ b/packages/tui/src/mini/stream-v2.transport.ts @@ -1553,6 +1553,8 @@ export async function createSessionTransport(input: StreamInput): Promise { references={() => []} commands={() => []} providers={() => undefined} + currentAgent={() => "Build"} + currentAgentID={() => "build"} currentModel={() => undefined} variants={() => []} currentVariant={() => undefined} @@ -65,6 +67,7 @@ test("down opens subagents from an empty prompt", async () => { onEditorOpen={async () => undefined} onInputClear={() => {}} onExit={() => {}} + onAgentSelect={() => {}} onModelSelect={() => {}} onVariantSelect={() => {}} onRows={() => {}} diff --git a/packages/tui/test/mini/footer.view.test.tsx b/packages/tui/test/mini/footer.view.test.tsx index 418689f14c62..d200fe80ad5c 100644 --- a/packages/tui/test/mini/footer.view.test.tsx +++ b/packages/tui/test/mini/footer.view.test.tsx @@ -8,6 +8,7 @@ import { Keymap } from "../../src/context/keymap" import { RUN_COMMAND_PANEL_ROWS, RUN_SUBAGENT_PANEL_ROWS, + RunAgentSelectBody, RunCommandMenuBody, RunModelSelectBody, RunQueuedPromptSelectBody, @@ -26,6 +27,7 @@ import type { FooterView, MiniSettingChange, MiniSettings, + RunAgent, RunCommand, RunInput, RunPrompt, @@ -110,6 +112,7 @@ async function renderFooter( commands?: RunCommand[] theme?: () => RunTheme providers?: RunProvider[] + currentAgent?: string currentModel?: RunInput["model"] currentVariant?: string subagents?: FooterSubagentState @@ -122,6 +125,7 @@ async function renderFooter( onFormReply?: (input: unknown) => void miniSettings?: MiniSettings mono?: boolean + onStatus?: (status: string) => void onMiniSettingChange?: (change: MiniSettingChange) => void } = {}, ) { @@ -144,6 +148,8 @@ async function renderFooter( references={() => []} commands={() => input.commands ?? []} providers={() => input.providers} + currentAgent={() => input.currentAgent ?? "Build"} + currentAgentID={() => input.currentAgent?.toLowerCase() ?? "build"} currentModel={() => input.currentModel} variants={() => []} currentVariant={() => input.currentVariant} @@ -162,11 +168,12 @@ async function renderFooter( onEditorOpen={async () => undefined} onInputClear={() => {}} onExit={() => {}} + onAgentSelect={() => {}} onModelSelect={() => {}} onVariantSelect={() => {}} onRows={() => {}} onLayout={() => {}} - onStatus={() => {}} + onStatus={(status) => input.onStatus?.(status)} onMiniSettingChange={(change) => input.onMiniSettingChange?.(change)} /> @@ -385,6 +392,7 @@ test("direct command panel renders grouped actions without catalog commands", as variants={variants} variantCycle="ctrl+t" onClose={() => {}} + onAgent={() => {}} onModel={() => {}} onEditor={() => {}} onSkill={() => {}} @@ -432,6 +440,11 @@ test("direct command panel renders grouped actions without catalog commands", as expect(frame).not.toContain("Review code") expect(frame).not.toContain("Commands 8") + await app.mockInput.typeText("agent") + await app.renderOnce() + expect(app.captureCharFrame()).toContain("Switch agent") + + app.mockInput.pressKey("u", { ctrl: true }) await app.mockInput.typeText("review") await app.renderOnce() expect(app.captureCharFrame()).toContain("No results found") @@ -615,6 +628,7 @@ test("direct command panel shows subagent entry when available", async () => { variants={variants} variantCycle="ctrl+t" onClose={() => {}} + onAgent={() => {}} onModel={() => {}} onEditor={() => {}} onSkill={() => {}} @@ -665,6 +679,7 @@ test("direct command panel keeps completed subagents available", async () => { variants={variants} variantCycle="ctrl+t" onClose={() => {}} + onAgent={() => {}} onModel={() => {}} onEditor={() => {}} onSkill={() => {}} @@ -1134,6 +1149,8 @@ test("direct footer shows authoritative pending work while running", async () => references={() => []} commands={() => []} providers={() => undefined} + currentAgent={() => "Build"} + currentAgentID={() => "build"} currentModel={() => ({ providerID: "opencode", modelID: "a-model-name-long-enough-to-force-responsive-truncation", @@ -1163,6 +1180,7 @@ test("direct footer shows authoritative pending work while running", async () => onEditorOpen={async () => undefined} onInputClear={() => {}} onExit={() => {}} + onAgentSelect={() => {}} onModelSelect={() => {}} onVariantSelect={() => {}} onRows={() => {}} @@ -1221,12 +1239,14 @@ test("direct footer shows authoritative pending work while running", async () => test("direct footer progressively adds model details after the command hint", async () => { for (const expected of [ - { width: 24, model: false, variant: false }, - { width: 32, model: true, variant: false }, - { width: 40, model: true, variant: true }, + { width: 24, agent: false, model: false, variant: false }, + { width: 32, agent: false, model: true, variant: false }, + { width: 40, agent: false, model: true, variant: true }, + { width: 80, agent: true, model: true, variant: true }, ]) { const app = await renderFooter({ providers: [provider()], + currentAgent: "Plan", currentModel: { providerID: "opencode", modelID: "gpt-5" }, currentVariant: "xhigh", width: expected.width, @@ -1238,6 +1258,7 @@ test("direct footer progressively adds model details after the command hint", as expect({ width: expected.width, command: frame.includes("ctrl+p cmd"), + agent: frame.includes("Plan"), model: frame.includes("GPT-5"), variant: frame.includes("xhigh"), }).toEqual({ ...expected, command: true }) @@ -1327,8 +1348,10 @@ test("direct footer shows full usage metadata when room is available", async () }) test("direct footer hides routine activity and shows explicit notices", async () => { + let status = "" const app = await renderFooter({ state: { usage: "159.6K (16%) · $4.23" }, + currentAgent: "Plan", miniSettings: { thinking: "hide", shell_output: "hide", @@ -1337,6 +1360,7 @@ test("direct footer hides routine activity and shows explicit notices", async () mono: true, }, mono: true, + onStatus: (value) => (status = value), width: 160, }) @@ -1344,6 +1368,7 @@ test("direct footer hides routine activity and shows explicit notices", async () await app.renderOnce() const initial = app.captureCharFrame() expect(initial).toContain("ctrl+p cmd") + expect(initial).not.toContain("Plan") expect(initial).not.toContain("gpt-5") expect(initial).not.toContain("159.6K") @@ -1359,6 +1384,12 @@ test("direct footer hides routine activity and shows explicit notices", async () expect(changed).not.toContain("159.6K") expect(boxPath(statusline, "SpinnerRenderable")).toBeUndefined() + app.mockInput.pressKey("p", { ctrl: true }) + await app.renderOnce() + await app.mockInput.typeText("status") + app.mockInput.pressEnter() + expect(status).toBe("running - agent Plan - gpt-5 - 159.6K (16%) - $4.23") + app.setState((state) => ({ ...state, notice: "variant high" })) await app.renderOnce() expect(app.captureCharFrame()).toContain("variant high") @@ -1473,6 +1504,53 @@ test("direct model panel renders current model selector", async () => { } }) +test("direct agent panel shows eligible agents and marks the current agent", async () => { + const [agents] = createSignal([ + { id: "build", name: "Build", description: "Build software", mode: "all", hidden: false }, + { id: "review", name: "Review", description: "Review changes", mode: "primary", hidden: false }, + { id: "explore", name: "Explore", mode: "subagent", hidden: false }, + { id: "secret", name: "Secret", mode: "all", hidden: true }, + ]) + const [current] = createSignal("review") + let selected: string | undefined + + const app = await testRender( + () => ( + + RUN_THEME_FALLBACK.footer} + agents={agents} + current={current} + onClose={() => {}} + onSelect={(agent) => (selected = agent)} + /> + + ), + { + width: 100, + height: RUN_COMMAND_PANEL_ROWS, + }, + ) + + try { + await app.renderOnce() + const frame = app.captureCharFrame() + + expect(frame).toContain("Select agent") + expect(frame).toContain("build") + expect(frame).toContain("review") + expect(frame).toContain("Review changes") + expect(frame).toContain("current") + expect(frame).not.toContain("explore") + expect(frame).not.toContain("secret") + + app.mockInput.pressEnter() + expect(selected).toBe("review") + } finally { + app.renderer.destroy() + } +}) + test("direct variant panel renders current variant selector", async () => { const [variants] = createSignal(["high", "minimal"]) const [current] = createSignal("high") diff --git a/packages/tui/test/mini/footer.width.test.ts b/packages/tui/test/mini/footer.width.test.ts index fa8ec5d31a71..244ec83cb329 100644 --- a/packages/tui/test/mini/footer.width.test.ts +++ b/packages/tui/test/mini/footer.width.test.ts @@ -10,12 +10,14 @@ describe("run footer width", () => { const narrow = footerWidthPolicy(79) expect(narrow.dialog.narrow).toBe(true) expect(narrow.statusline.showActivityMeta).toBe(false) + expect(narrow.statusline.showAgent).toBe(false) expect(narrow.statusline.showContextHints).toBe(false) expect(narrow.statusline.contextHintLimit).toBe(0) const compact = footerWidthPolicy(80) expect(compact.dialog.narrow).toBe(false) expect(compact.statusline.showActivityMeta).toBe(true) + expect(compact.statusline.showAgent).toBe(true) expect(compact.statusline.showContextHints).toBe(true) expect(compact.statusline.contextHintLimit).toBe(1) diff --git a/packages/tui/test/mini/runtime.test.ts b/packages/tui/test/mini/runtime.test.ts index 1955b4a4700e..e55e150bc004 100644 --- a/packages/tui/test/mini/runtime.test.ts +++ b/packages/tui/test/mini/runtime.test.ts @@ -62,6 +62,7 @@ describe("run interactive runtime", () => { variants: ["low", "high"], }) let lifecycle!: LifecycleInput + let turnAgent: string | undefined let turnModel: { providerID: string; modelID: string } | undefined let refreshCatalog: (() => Promise) | undefined stubCatalogLists(sdk, { @@ -107,6 +108,7 @@ describe("run interactive runtime", () => { catalogLoaded.resolve() return { runPromptTurn: async (input) => { + turnAgent = input.agent turnModel = input.model api.close() }, @@ -139,8 +141,10 @@ describe("run interactive runtime", () => { selection: { providerID: "test", modelID: "resolved" }, }) expect(lifecycle.onCycleVariant?.()).toMatchObject({ status: "variant low", variant: "low" }) + lifecycle.onAgentSelect?.("review") ui.submit("hello") while (!turnModel) await Bun.sleep(0) + expect(turnAgent).toBe("review") expect(turnModel).toEqual({ providerID: "test", modelID: "resolved" }) await task }) diff --git a/packages/tui/test/mini/stream-v2.transport.test.ts b/packages/tui/test/mini/stream-v2.transport.test.ts index 98ff08aac76b..efe18ac64ae3 100644 --- a/packages/tui/test/mini/stream-v2.transport.test.ts +++ b/packages/tui/test/mini/stream-v2.transport.test.ts @@ -624,13 +624,17 @@ describe("V2 mini transport", () => { ok({ ...promptAdmission(request), admittedSeq: 2 }) as never, ) await transport.queuePromptTurn({ - agent: undefined, + agent: "review", model: undefined, variant: undefined, prompt: { messageID: "msg_next", text: "another", parts: [] }, files: [], includeFiles: false, }) + expect(client.session.switchAgent).toHaveBeenCalledWith( + { sessionID: "ses_1", agent: "review" }, + expect.anything(), + ) expect(prompt).toHaveBeenCalledWith(expect.objectContaining({ delivery: "queue" }), expect.anything()) events.push({ id: "evt_earlier_admission", @@ -2714,7 +2718,7 @@ describe("V2 mini transport", () => { }) await transport.runPromptTurn({ - agent: undefined, + agent: "review", model: undefined, variant: undefined, prompt: { @@ -2727,6 +2731,10 @@ describe("V2 mini transport", () => { includeFiles: true, }) + expect(client.session.switchAgent).toHaveBeenCalledWith( + { sessionID: "ses_1", agent: "review" }, + expect.anything(), + ) expect(request).toMatchObject({ sessionID: "ses_1", id: "msg_skill", skill: "tigerstyle" }) expect(command).not.toHaveBeenCalled() expect(prompt).not.toHaveBeenCalled() From e12ec8681bc834ab76e1318bb294dfe7ef647887 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Wed, 22 Jul 2026 15:32:43 +0200 Subject: [PATCH 021/150] fix(tui): render mini markdown in mono mode (#38313) --- packages/tui/src/mini/entry.body.ts | 2 +- packages/tui/src/mini/mono.ts | 93 +++++++++++++++++++ packages/tui/src/mini/runtime.lifecycle.ts | 3 + packages/tui/src/mini/scrollback.surface.ts | 7 +- packages/tui/src/mini/scrollback.writer.tsx | 6 +- .../tui/test/mini/scrollback.surface.test.ts | 42 ++++++++- 6 files changed, 147 insertions(+), 6 deletions(-) diff --git a/packages/tui/src/mini/entry.body.ts b/packages/tui/src/mini/entry.body.ts index 2d4a03f7f609..499ed091da6f 100644 --- a/packages/tui/src/mini/entry.body.ts +++ b/packages/tui/src/mini/entry.body.ts @@ -214,7 +214,7 @@ export function entryBody(commit: StreamCommit, options?: ScrollbackOptions): Ru return commit.interrupted ? textBody("assistant interrupted") : RUN_ENTRY_NONE } - return mono ? textBody(raw) : markdownBody(raw) + return markdownBody(raw) } if (commit.kind === "reasoning") { diff --git a/packages/tui/src/mini/mono.ts b/packages/tui/src/mini/mono.ts index 389cc9d702cc..109daa0a4bd5 100644 --- a/packages/tui/src/mini/mono.ts +++ b/packages/tui/src/mini/mono.ts @@ -1,3 +1,12 @@ +import { + BoxRenderable, + RGBA, + type BorderCharacters, + type CliRendererExternalOutputEvent, + type MarkdownOptions, + type Renderable, +} from "@opentui/core" + const prefixes: Record = { 0x2192: "->", 0x2190: "<-", @@ -10,6 +19,90 @@ const prefixes: Record = { 0x27f3: "*", } +const markdown: Record = { + ...prefixes, + 0x00a0: " ", + 0x00b7: "-", + 0x2010: "-", + 0x2011: "-", + 0x2012: "-", + 0x2013: "-", + 0x2014: "--", + 0x2018: "'", + 0x2019: "'", + 0x201c: '"', + 0x201d: '"', + 0x2022: "*", + 0x2026: "...", + 0x2191: "up", + 0x2193: "down", +} + +const asciiBorder: BorderCharacters = { + topLeft: "+", + topRight: "+", + bottomLeft: "+", + bottomRight: "+", + horizontal: "-", + vertical: "|", + topT: "+", + bottomT: "+", + leftT: "+", + rightT: "+", + cross: "+", +} + +export const monoMarkdownTableOptions = { + style: "columns" as const, + widthMode: "content" as const, + borders: false, +} + +export const monoMarkdownRenderNode: NonNullable = (token, context) => { + if (token.type !== "blockquote" && token.type !== "hr" && token.type !== "list") return + const renderable = context.defaultRender() + if (!renderable) return renderable + monoBorders(renderable) + return renderable +} + +function monoBorders(renderable: Renderable): void { + if (renderable instanceof BoxRenderable) renderable.customBorderChars = asciiBorder + renderable.getChildren().forEach(monoBorders) +} + +export function monoMarkdown(value: string, mono: boolean): string { + if (!mono) return value + return value.replace(/[^\t\n\x20-\x7e]/gu, (char) => markdown[char.codePointAt(0)!] ?? "?") +} + +export function monoSnapshot(event: CliRendererExternalOutputEvent): void { + const buffers = event.snapshot.buffers + const chars = buffers.char + for (let index = 0; index < chars.length; index += 1) { + const point = chars[index]! + if (point <= 0x7f) continue + const offset = index * 4 + event.snapshot.setCell( + index % event.snapshot.width, + Math.floor(index / event.snapshot.width), + monoCell(point), + RGBA.fromArray(buffers.fg.subarray(offset, offset + 4)), + RGBA.fromArray(buffers.bg.subarray(offset, offset + 4)), + buffers.attributes[index], + ) + } +} + +function monoCell(point: number): string { + const kind = point >>> 30 + if (kind === 2) return "?" + if (kind === 3) return " " + if (point === 0x2500) return "-" + if (point === 0x2502) return "|" + return markdown[point]?.[0] ?? "?" +} + export function monoPrefix(value: string, mono: boolean): string { if (!mono) return value const point = value.codePointAt(0) diff --git a/packages/tui/src/mini/runtime.lifecycle.ts b/packages/tui/src/mini/runtime.lifecycle.ts index 8bc68066899f..4d715add8316 100644 --- a/packages/tui/src/mini/runtime.lifecycle.ts +++ b/packages/tui/src/mini/runtime.lifecycle.ts @@ -11,6 +11,7 @@ import path from "path" import { CliRenderEvents, createCliRenderer, type CliRenderer, type ScrollbackWriter } from "@opentui/core" import { isDefaultTitle } from "../util/session" +import { monoSnapshot } from "./mono" import { entrySplash, exitSplash, splashMeta } from "./splash" import { resolveRunTheme } from "./theme" import type { @@ -172,6 +173,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise { if (input.host.platform !== "linux") return if (!title || isDefaultTitle(title)) return renderer.setTerminalTitle("OpenCode") @@ -329,6 +331,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise {}) footer.destroy() if (input.host.platform === "linux") renderer.setTerminalTitle("") + if (mono) renderer.off(CliRenderEvents.EXTERNAL_OUTPUT, monoSnapshot) shutdown(renderer) if (!wroteExit) { input.host.stdout.write("\n") diff --git a/packages/tui/src/mini/scrollback.surface.ts b/packages/tui/src/mini/scrollback.surface.ts index 293335238f0e..9d81c2bb987b 100644 --- a/packages/tui/src/mini/scrollback.surface.ts +++ b/packages/tui/src/mini/scrollback.surface.ts @@ -14,6 +14,7 @@ import { type ScrollbackSurface, } from "@opentui/core" import { entryBody, entryCanStream, entryDone, entryFlags } from "./entry.body" +import { monoMarkdown, monoMarkdownRenderNode, monoMarkdownTableOptions } from "./mono" import { entryColor, entryLook, entrySyntax } from "./scrollback.shared" import { turnSummaryCommit } from "./turn-summary" import { entryWriter, sameEntryGroup, separatorRows, spacerWriter, turnSummaryWriter } from "./scrollback.writer" @@ -179,7 +180,8 @@ export class RunScrollbackStream { width: "100%", streaming: true, internalBlockMode: "top-level", - tableOptions: { widthMode: "content" }, + tableOptions: this.mono ? monoMarkdownTableOptions : { widthMode: "content" }, + renderNode: this.mono ? monoMarkdownRenderNode : undefined, fg: entryColor(commit, this.theme), treeSitterClient, }) @@ -281,7 +283,7 @@ export class RunScrollbackStream { } const renderable = active.renderable - renderable.content = active.content + renderable.content = monoMarkdown(active.content, this.mono) renderable.streaming = !done await active.surface.settle() this.releasePendingThemes() @@ -395,6 +397,7 @@ export class RunScrollbackStream { commit, body: staticBody(commit, body, spaced), theme: this.theme, + opts: { mono: this.mono }, }), ) this.markRendered(commit) diff --git a/packages/tui/src/mini/scrollback.writer.tsx b/packages/tui/src/mini/scrollback.writer.tsx index 6b141e28e832..c74f09168b62 100644 --- a/packages/tui/src/mini/scrollback.writer.tsx +++ b/packages/tui/src/mini/scrollback.writer.tsx @@ -2,6 +2,7 @@ import { createScrollbackWriter } from "@opentui/solid" import { TextRenderable, type ColorInput, type ScrollbackRenderContext, type ScrollbackWriter } from "@opentui/core" import { Match, Switch, createMemo } from "solid-js" import { entryBody, entryFlags } from "./entry.body" +import { monoMarkdown, monoMarkdownRenderNode, monoMarkdownTableOptions } from "./mono" import { entryColor, entryLook, entrySyntax } from "./scrollback.shared" import { toolFiletype, toolStructuredFinal } from "./tool" import { RUN_THEME_FALLBACK, transparent, type RunTheme } from "./theme" @@ -239,9 +240,10 @@ export function RunEntryContent(props: { width="100%" syntaxStyle={syntax()} streaming={streaming()} - content={markdown()!.content} + content={monoMarkdown(markdown()!.content, props.opts?.mono === true)} fg={color()} - tableOptions={{ widthMode: "content" }} + tableOptions={props.opts?.mono ? monoMarkdownTableOptions : { widthMode: "content" }} + renderNode={props.opts?.mono ? monoMarkdownRenderNode : undefined} /> diff --git a/packages/tui/test/mini/scrollback.surface.test.ts b/packages/tui/test/mini/scrollback.surface.test.ts index 98182c7edaee..5db71b1045f3 100644 --- a/packages/tui/test/mini/scrollback.surface.test.ts +++ b/packages/tui/test/mini/scrollback.surface.test.ts @@ -1,7 +1,8 @@ import { afterEach, expect, test } from "bun:test" import type { SessionMessageAssistantTool } from "@opencode-ai/client/promise" -import { RGBA, SyntaxStyle } from "@opentui/core" +import { CliRenderEvents, MarkdownRenderable, RGBA, SyntaxStyle, TextRenderable } from "@opentui/core" import { MockTreeSitterClient, createTestRenderer, type TestRenderer } from "@opentui/core/testing" +import { monoSnapshot } from "../../src/mini/mono" import { RunScrollbackStream } from "../../src/mini/scrollback.surface" import { entryGroupKey } from "../../src/mini/scrollback.writer" import { RUN_THEME_FALLBACK, type RunTheme } from "../../src/mini/theme" @@ -67,6 +68,7 @@ async function setup( wrote?: boolean theme?: RunTheme onThemeRelease?: (theme: RunTheme) => void + mono?: boolean } = {}, ) { const out = await createTestRenderer({ @@ -77,6 +79,7 @@ async function setup( consoleMode: "disabled", }) active.push(out.renderer) + if (input.mono) out.renderer.on(CliRenderEvents.EXTERNAL_OUTPUT, monoSnapshot) const treeSitterClient = new MockTreeSitterClient({ autoResolveTimeout: 0 }) treeSitterClient.setMockResult({ highlights: [] }) @@ -87,6 +90,7 @@ async function setup( treeSitterClient, wrote: input.wrote ?? false, onThemeRelease: input.onThemeRelease, + mono: input.mono, }), } } @@ -203,6 +207,42 @@ test("theme swaps preserve streamed markdown parser state", async () => { } }) +test("renders monochrome scrollback as ASCII markdown", async () => { + const out = await setup({ mono: true, width: 60 }) + const output: string[] = [] + out.renderer.on(CliRenderEvents.EXTERNAL_OUTPUT, (event) => { + output.push(decoder.decode(event.snapshot.getRealCharBytes(true))) + }) + + try { + await out.scrollback.append(assistant("# H")) + expect(Reflect.get(out.scrollback, "active")?.renderable).toBeInstanceOf(MarkdownRenderable) + await out.scrollback.append(assistant('éading →\n\n> “quote”\n\n---\n\n| A | B |\n| - | - |\n| α | β |')) + await out.scrollback.complete() + out.renderer.writeToScrollback((ctx) => ({ + root: new TextRenderable(ctx.renderContext, { + content: "plain │ emoji 🙂", + width: ctx.width, + height: 1, + }), + width: ctx.width, + height: 1, + trailingNewline: false, + })) + + const rendered = output.join("").replace(/ +\n/g, "\n") + expect(rendered).toContain("# H?ading ->") + expect(rendered).toContain('| "quote"') + expect(rendered).toContain("------------------------------------------------------------") + expect(rendered).toContain("? ?") + expect(rendered).toContain("plain ? emoji ?") + expect(rendered).not.toMatch(/[^\x00-\x7f]/) + } finally { + out.scrollback.destroy() + destroy(claim(out.renderer)) + } +}) + function user(text: string): StreamCommit { return { kind: "user", From 5841b04fe73dae63ff18714a22e19804f029e87c Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Wed, 22 Jul 2026 15:42:12 +0200 Subject: [PATCH 022/150] tui: resolve default mini footer agent (#38315) --- packages/tui/src/mini/footer.ts | 17 ++++++++++++----- packages/tui/test/mini/footer.test.ts | 17 +++++++++++++++-- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/packages/tui/src/mini/footer.ts b/packages/tui/src/mini/footer.ts index 54b50953bf2e..a403542219c8 100644 --- a/packages/tui/src/mini/footer.ts +++ b/packages/tui/src/mini/footer.ts @@ -102,6 +102,11 @@ type RunFooterOptions = { subscribeThemeSignal: (listener: () => void) => () => void } +export function resolveRunAgent(agents: RunAgent[], current: string | undefined) { + const selectable = agents.filter((agent) => agent.mode !== "subagent" && !agent.hidden) + return selectable.find((agent) => agent.id === current) ?? selectable.at(0) +} + const PERMISSION_ROWS = 12 const FORM_ROWS = 14 const SUBAGENT_ROWS = RUN_SUBAGENT_PANEL_ROWS @@ -252,13 +257,15 @@ export class RunFooter implements FooterApi { const [providers, setProviders] = createSignal() this.providers = providers this.setProviders = setProviders - const [currentAgentID, setCurrentAgentID] = createSignal(options.agent) - this.currentAgentID = currentAgentID + const [selectedAgentID, setCurrentAgentID] = createSignal(options.agent) + const currentAgent = () => resolveRunAgent(this.agents(), selectedAgentID()) + this.currentAgentID = () => currentAgent()?.id ?? selectedAgentID() this.setCurrentAgentID = setCurrentAgentID this.currentAgent = () => { - const agent = currentAgentID() - if (!agent) return "Default" - return this.agents().find((item) => item.id === agent)?.name ?? Locale.titlecase(agent) + const agent = currentAgent() + if (agent) return agent.name + const selected = selectedAgentID() + return selected ? Locale.titlecase(selected) : "Default" } const [currentModel, setCurrentModel] = createSignal(options.model) this.currentModel = currentModel diff --git a/packages/tui/test/mini/footer.test.ts b/packages/tui/test/mini/footer.test.ts index e59813f9f7f4..8fbf7fc686b4 100644 --- a/packages/tui/test/mini/footer.test.ts +++ b/packages/tui/test/mini/footer.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "bun:test" -import { coalesceProgressCommit } from "../../src/mini/footer" -import type { StreamCommit } from "../../src/mini/types" +import { coalesceProgressCommit, resolveRunAgent } from "../../src/mini/footer" +import type { RunAgent, StreamCommit } from "../../src/mini/types" function progress(input: Partial = {}): StreamCommit { return { @@ -23,3 +23,16 @@ test("coalesces progress only within the same message and tool state", () => { progress({ text: "onetwo", directory: "/latest" }), ) }) + +test("resolves the first selectable agent when none is selected", () => { + const agents: RunAgent[] = [ + { id: "task", name: "Task", mode: "subagent", hidden: false }, + { id: "secret", name: "Secret", mode: "primary", hidden: true }, + { id: "build", name: "Build", mode: "primary", hidden: false }, + { id: "plan", name: "Plan", mode: "primary", hidden: false }, + ] + + expect(resolveRunAgent(agents, undefined)?.id).toBe("build") + expect(resolveRunAgent(agents, "plan")?.id).toBe("plan") + expect(resolveRunAgent(agents, "missing")?.id).toBe("build") +}) From 8fad13365bdc4e2234d3848360d0565b55fcb3a6 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Wed, 22 Jul 2026 15:46:50 +0200 Subject: [PATCH 023/150] feat(tui): add mini splash setting (#38317) Fix #38010 --- packages/cli/test/config.test.ts | 4 +- packages/tui/src/config/index.tsx | 3 ++ packages/tui/src/mini/footer.command.tsx | 7 +++ packages/tui/src/mini/footer.ts | 4 ++ packages/tui/src/mini/runtime.boot.ts | 1 + packages/tui/src/mini/runtime.lifecycle.ts | 23 +++++----- packages/tui/src/mini/types.ts | 1 + packages/tui/test/mini/footer-keymap.test.tsx | 9 +++- packages/tui/test/mini/footer.view.test.tsx | 44 +++++++++++++++++-- packages/tui/test/mini/runtime.boot.test.ts | 11 ++++- 10 files changed, 89 insertions(+), 18 deletions(-) diff --git a/packages/cli/test/config.test.ts b/packages/cli/test/config.test.ts index 6af4b66d26be..a0e32c4aa899 100644 --- a/packages/cli/test/config.test.ts +++ b/packages/cli/test/config.test.ts @@ -131,7 +131,7 @@ test("updates a config draft while preserving JSONC comments", async () => { const service = yield* Config.Service return yield* service.update((draft) => { draft.prompt = { paste: "compact" } - draft.mini = { thinking: "hide", shell_output: "hide", turn_summary: "hide", mono: true } + draft.mini = { thinking: "hide", shell_output: "hide", turn_summary: "hide", splash: "hide", mono: true } }) }), ) @@ -139,7 +139,7 @@ test("updates a config draft while preserving JSONC comments", async () => { expect(config).toEqual({ animations: true, prompt: { paste: "compact" }, - mini: { thinking: "hide", shell_output: "hide", turn_summary: "hide", mono: true }, + mini: { thinking: "hide", shell_output: "hide", turn_summary: "hide", splash: "hide", mono: true }, }) expect(await Bun.file(path.join(directory, "cli.json")).text()).toContain("// Keep this comment") } finally { diff --git a/packages/tui/src/config/index.tsx b/packages/tui/src/config/index.tsx index 5afb61ae1813..6f5b78185e11 100644 --- a/packages/tui/src/config/index.tsx +++ b/packages/tui/src/config/index.tsx @@ -136,6 +136,9 @@ export const Info = Schema.Struct({ footer: Schema.optional(Schema.Literals(["show", "hide"])).annotate({ description: "Show or hide persistent activity, model, usage, and context details in the footer", }), + splash: Schema.optional(Schema.Literals(["show", "hide"])).annotate({ + description: "Show or hide the entry and exit splash banners", + }), mono: Schema.optional(Schema.Boolean).annotate({ description: "Use monochrome ASCII output", }), diff --git a/packages/tui/src/mini/footer.command.tsx b/packages/tui/src/mini/footer.command.tsx index 48c11f951c05..38be78a68205 100644 --- a/packages/tui/src/mini/footer.command.tsx +++ b/packages/tui/src/mini/footer.command.tsx @@ -684,6 +684,13 @@ export function RunSettingsBody(props: { keywords: `footer status activity model context usage ${props.settings().footer}`, key: "footer", }, + { + category: "Terminal", + display: "Splash", + footer: saving() === "splash" ? "saving" : props.settings().splash, + keywords: `splash entry exit banner ${props.settings().splash}`, + key: "splash", + }, { category: "Terminal", display: "Monochrome UI", diff --git a/packages/tui/src/mini/footer.ts b/packages/tui/src/mini/footer.ts index a403542219c8..002cc626168a 100644 --- a/packages/tui/src/mini/footer.ts +++ b/packages/tui/src/mini/footer.ts @@ -616,6 +616,10 @@ export class RunFooter implements FooterApi { return this.theme() } + public currentMiniSettings(): MiniSettings { + return this.miniSettings() + } + private destroyTheme(theme: RunTheme): void { const index = this.themes.indexOf(theme) if (index === -1) { diff --git a/packages/tui/src/mini/runtime.boot.ts b/packages/tui/src/mini/runtime.boot.ts index 2f5e807d5e69..3a65ce19c0d2 100644 --- a/packages/tui/src/mini/runtime.boot.ts +++ b/packages/tui/src/mini/runtime.boot.ts @@ -90,6 +90,7 @@ export function resolveMiniSettings(config?: { mini?: Partial }): shell_output: config?.mini?.shell_output ?? "hide", turn_summary: config?.mini?.turn_summary ?? "show", footer: config?.mini?.footer ?? "show", + splash: config?.mini?.splash ?? "show", mono: config?.mini?.mono ?? false, } } diff --git a/packages/tui/src/mini/runtime.lifecycle.ts b/packages/tui/src/mini/runtime.lifecycle.ts index 4d715add8316..315335da822b 100644 --- a/packages/tui/src/mini/runtime.lifecycle.ts +++ b/packages/tui/src/mini/runtime.lifecycle.ts @@ -196,13 +196,15 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise {}) @@ -224,7 +226,9 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise {}) - const show = renderer.isDestroyed ? false : next.showExit - if (!renderer.isDestroyed && show) { + if (!renderer.isDestroyed && next.showExit && footer.currentMiniSettings().splash === "show") { const sessionID = next.sessionID || input.getSessionID?.() || input.sessionID const splash = splashInfo(next.sessionTitle ?? input.sessionTitle, next.history ?? input.history) wroteExit = queueSplash( diff --git a/packages/tui/src/mini/types.ts b/packages/tui/src/mini/types.ts index ea300c322241..2ee854010bfb 100644 --- a/packages/tui/src/mini/types.ts +++ b/packages/tui/src/mini/types.ts @@ -393,6 +393,7 @@ export type MiniSettings = { shell_output: "show" | "hide" turn_summary: "show" | "hide" footer: "show" | "hide" + splash: "show" | "hide" mono: boolean } diff --git a/packages/tui/test/mini/footer-keymap.test.tsx b/packages/tui/test/mini/footer-keymap.test.tsx index 65562a4f02b7..cca3526576ef 100644 --- a/packages/tui/test/mini/footer-keymap.test.tsx +++ b/packages/tui/test/mini/footer-keymap.test.tsx @@ -56,7 +56,14 @@ test("down opens subagents from an empty prompt", async () => { view={view} subagent={subagents} theme={() => RUN_THEME_FALLBACK} - miniSettings={() => ({ thinking: "hide", shell_output: "hide", turn_summary: "show", footer: "show", mono: false })} + miniSettings={() => ({ + thinking: "hide", + shell_output: "hide", + turn_summary: "show", + footer: "show", + splash: "show", + mono: false, + })} mono={false} onSubmit={() => true} onPermissionReply={() => {}} diff --git a/packages/tui/test/mini/footer.view.test.tsx b/packages/tui/test/mini/footer.view.test.tsx index d200fe80ad5c..6fb78b1bea0f 100644 --- a/packages/tui/test/mini/footer.view.test.tsx +++ b/packages/tui/test/mini/footer.view.test.tsx @@ -136,7 +136,14 @@ async function renderFooter( const [state, setState] = footerState(input.state) const config = input.tuiConfig ?? tuiConfig const [miniSettings] = createSignal( - input.miniSettings ?? { thinking: "hide", shell_output: "hide", turn_summary: "show", footer: "show", mono: false }, + input.miniSettings ?? { + thinking: "hide", + shell_output: "hide", + turn_summary: "show", + footer: "show", + splash: "show", + mono: false, + }, ) function Harness() { return ( @@ -471,6 +478,7 @@ test("direct settings panel changes Mini transcript preferences", async () => { shell_output: "hide", turn_summary: "show", footer: "show", + splash: "show", mono: false, }) const app = await testRender( @@ -499,6 +507,7 @@ test("direct settings panel changes Mini transcript preferences", async () => { expect(frame).toContain("Shell") expect(frame).toContain("Turn summary") expect(frame).toContain("Footer details") + expect(frame).toContain("Splash") expect(frame).toContain("Monochrome UI") expect(frame).toContain("left/right change") expect(frame).not.toMatch(/[^\x00-\x7F]/) @@ -506,16 +515,35 @@ test("direct settings panel changes Mini transcript preferences", async () => { app.mockInput.pressKey("ARROW_RIGHT") await app.renderOnce() - expect(settings()).toEqual({ thinking: "show", shell_output: "hide", turn_summary: "show", footer: "show", mono: false }) + expect(settings()).toEqual({ + thinking: "show", + shell_output: "hide", + turn_summary: "show", + footer: "show", + splash: "show", + mono: false, + }) app.mockInput.pressKey("ARROW_DOWN") app.mockInput.pressKey("ARROW_DOWN") app.mockInput.pressKey("ARROW_RIGHT") await app.renderOnce() - expect(settings()).toEqual({ thinking: "show", shell_output: "hide", turn_summary: "hide", footer: "show", mono: false }) + expect(settings()).toEqual({ + thinking: "show", + shell_output: "hide", + turn_summary: "hide", + footer: "show", + splash: "show", + mono: false, + }) app.mockInput.pressKey("ARROW_DOWN") + app.mockInput.pressKey("ARROW_DOWN") + app.mockInput.pressKey("ARROW_RIGHT") + await app.renderOnce() + expect(settings().splash).toBe("hide") + app.mockInput.pressKey("ARROW_DOWN") app.mockInput.pressKey("ARROW_RIGHT") await app.renderOnce() @@ -1169,7 +1197,14 @@ test("direct footer shows authoritative pending work while running", async () => }, ]} theme={() => RUN_THEME_FALLBACK} - miniSettings={() => ({ thinking: "hide", shell_output: "hide", turn_summary: "show", footer: "show", mono: false })} + miniSettings={() => ({ + thinking: "hide", + shell_output: "hide", + turn_summary: "show", + footer: "show", + splash: "show", + mono: false, + })} mono={false} onSubmit={() => true} onPermissionReply={() => {}} @@ -1357,6 +1392,7 @@ test("direct footer hides routine activity and shows explicit notices", async () shell_output: "hide", turn_summary: "show", footer: "hide", + splash: "show", mono: true, }, mono: true, diff --git a/packages/tui/test/mini/runtime.boot.test.ts b/packages/tui/test/mini/runtime.boot.test.ts index 431b5bdc0198..734d2a49233b 100644 --- a/packages/tui/test/mini/runtime.boot.test.ts +++ b/packages/tui/test/mini/runtime.boot.test.ts @@ -106,17 +106,26 @@ describe("run runtime boot", () => { shell_output: "hide", turn_summary: "show", footer: "show", + splash: "show", mono: false, }) expect( resolveMiniSettings({ - mini: { thinking: "show", shell_output: "show", turn_summary: "hide", footer: "hide", mono: true }, + mini: { + thinking: "show", + shell_output: "show", + turn_summary: "hide", + footer: "hide", + splash: "hide", + mono: true, + }, }), ).toEqual({ thinking: "show", shell_output: "show", turn_summary: "hide", footer: "hide", + splash: "hide", mono: true, }) }) From 4204b9d087b681d93072535144102b0b64111e47 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Wed, 22 Jul 2026 16:02:17 +0200 Subject: [PATCH 024/150] fix(tui): show providers in Mini model search (#38321) --- packages/tui/src/mini/footer.command.tsx | 6 +++++- packages/tui/test/mini/footer.view.test.tsx | 13 ++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/tui/src/mini/footer.command.tsx b/packages/tui/src/mini/footer.command.tsx index 38be78a68205..e7c8da0d8b03 100644 --- a/packages/tui/src/mini/footer.command.tsx +++ b/packages/tui/src/mini/footer.command.tsx @@ -1069,7 +1069,11 @@ export function RunModelSelectBody(props: { > + controller.query().trim() + ? controller.items().map((item) => ({ ...item, footer: item.providerName })) + : controller.items() + } selected={controller.menu.selected} offset={controller.menu.offset} rows={() => PANEL_LIST_ROWS} diff --git a/packages/tui/test/mini/footer.view.test.tsx b/packages/tui/test/mini/footer.view.test.tsx index 6fb78b1bea0f..f86287124e08 100644 --- a/packages/tui/test/mini/footer.view.test.tsx +++ b/packages/tui/test/mini/footer.view.test.tsx @@ -1498,7 +1498,10 @@ test("direct permission rejection submits through keymap return binding", async }) test("direct model panel renders current model selector", async () => { - const [providers] = createSignal([provider()]) + const [providers] = createSignal([ + provider(), + { id: "openai", name: "OpenAI", models: { "gpt-5": model({ id: "gpt-5", name: "GPT-5" }) } }, + ]) const [current] = createSignal({ providerID: "opencode", modelID: "gpt-5" }) const app = await testRender( @@ -1535,6 +1538,14 @@ test("direct model panel renders current model selector", async () => { expect(frame).not.toContain("┃") expect(frame).not.toContain("Old Model") expectPaletteList(list, 2) + + "gpt-5".split("").forEach((key) => app.mockInput.pressKey(key)) + await app.renderOnce() + const search = app.captureCharFrame() + + expect(search.match(/GPT-5/g)).toHaveLength(2) + expect(search).toContain("opencode") + expect(search).toContain("OpenAI") } finally { app.renderer.destroy() } From 5a9ed4d350d5e9d490c08547830d38342730d077 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 22 Jul 2026 10:40:07 -0400 Subject: [PATCH 025/150] fix: make tool progress live-only (#38217) --- packages/cli/src/acp/event.ts | 4 +- packages/cli/src/run/noninteractive.ts | 12 ++-- packages/cli/test/acp/event-behavior.test.ts | 6 +- packages/cli/test/run/noninteractive.test.ts | 7 +- .../client/src/promise/generated/types.ts | 52 +++++++------- packages/core/src/database/migration.gen.ts | 1 + ...60722011141_delete_tool_progress_events.ts | 11 +++ packages/core/src/session/message-updater.ts | 4 +- packages/core/src/session/projector.ts | 1 - packages/core/src/session/runner/llm.ts | 11 +-- .../src/session/runner/publish-llm-event.ts | 29 ++++++++ packages/core/src/tool/shell.ts | 15 +++- packages/core/test/database-migration.test.ts | 26 +++++++ .../test/session-runner-tool-events.test.ts | 42 +++++++++-- packages/core/test/session-runner.test.ts | 46 ++++++++++++ .../core/test/session-tool-progress.test.ts | 13 ++-- packages/core/test/tool-shell.test.ts | 31 ++++++++ packages/schema/src/session-event.ts | 13 ++-- packages/schema/test/event-manifest.test.ts | 3 +- packages/tui/src/context/data.tsx | 4 +- packages/tui/src/mini/stream-v2.subagent.ts | 18 ++--- packages/tui/src/mini/stream-v2.transport.ts | 5 +- packages/tui/test/cli/tui/data.test.tsx | 1 - .../tui/test/mini/stream-v2.transport.test.ts | 71 ++++++++++++++++++- 24 files changed, 337 insertions(+), 89 deletions(-) create mode 100644 packages/core/src/database/migration/20260722011141_delete_tool_progress_events.ts diff --git a/packages/cli/src/acp/event.ts b/packages/cli/src/acp/event.ts index 11bf4c11c55e..da8bfe168bf4 100644 --- a/packages/cli/src/acp/event.ts +++ b/packages/cli/src/acp/event.ts @@ -198,8 +198,8 @@ export async function streamTurn(input: { toolCallId: event.data.callID, toolName: current.name, input: current.input, - structured: current.structured, - content: current.content, + structured: event.data.metadata ?? current.structured, + content: event.data.content ?? current.content, error: event.data.error.message, cwd: input.cwd, }), diff --git a/packages/cli/src/run/noninteractive.ts b/packages/cli/src/run/noninteractive.ts index 5ea239ca1b6c..41859d3b64fc 100644 --- a/packages/cli/src/run/noninteractive.ts +++ b/packages/cli/src/run/noninteractive.ts @@ -398,6 +398,8 @@ export async function runNonInteractivePrompt(input: Input) { const key = toolKey(event.data.assistantMessageID, event.data.callID) const current = tools.get(key) ?? fallbackTool(event) const error = event.data.error.message + const structured = event.data.metadata ?? current.structured + const content = event.data.content ?? current.content const tool: SessionMessageAssistantTool = { type: "tool", id: event.data.callID, @@ -408,8 +410,8 @@ export async function runNonInteractivePrompt(input: Input) { state: { status: "error", input: current.input, - structured: current.structured, - content: current.content, + structured, + content, error: event.data.error, result: event.data.result, }, @@ -439,14 +441,14 @@ export async function runNonInteractivePrompt(input: Input) { renderedTools.add(key) if (input.compatibility === "v1" && (permissionRejected || formCancelled)) continue if (!emit("tool_use", time, { part })) { - if (toolOutputText(current.tool, current.content).trim()) + if (toolOutputText(current.tool, content).trim()) await input.renderTool({ ...tool, state: { status: "completed", input: current.input, - structured: current.structured, - content: current.content, + structured, + content, result: event.data.result, }, }) diff --git a/packages/cli/test/acp/event-behavior.test.ts b/packages/cli/test/acp/event-behavior.test.ts index 9de17cf4e1d1..1f9935f5c8e3 100644 --- a/packages/cli/test/acp/event-behavior.test.ts +++ b/packages/cli/test/acp/event-behavior.test.ts @@ -214,7 +214,7 @@ describe("acp event behavior", () => { }), ) send( - durableEvent("session.tool.progress", { + ephemeralEvent("session.tool.progress", { sessionID: "ses_tools", assistantMessageID: "msg_tools", callID: "call_ok", @@ -251,7 +251,7 @@ describe("acp event behavior", () => { }), ) send( - durableEvent("session.tool.progress", { + ephemeralEvent("session.tool.progress", { sessionID: "ses_tools", assistantMessageID: "msg_tools", callID: "call_fail", @@ -265,6 +265,8 @@ describe("acp event behavior", () => { assistantMessageID: "msg_tools", callID: "call_fail", error: { type: "tool.error", message: "not found" }, + metadata: { bytes: 0 }, + content: [{ type: "text", text: "opening" }], executed: true, }), ) diff --git a/packages/cli/test/run/noninteractive.test.ts b/packages/cli/test/run/noninteractive.test.ts index 58dea74a93aa..904ced747434 100644 --- a/packages/cli/test/run/noninteractive.test.ts +++ b/packages/cli/test/run/noninteractive.test.ts @@ -130,7 +130,6 @@ function failedTool(inputID: string): V2Event[] { id: "evt_failed_tool_progress", created: 3, type: "session.tool.progress", - durable: { aggregateID: "ses_1", seq: 3, version: 1 }, data: { sessionID: "ses_1", assistantMessageID: "msg_failed_tool", @@ -149,6 +148,8 @@ function failedTool(inputID: string): V2Event[] { assistantMessageID: "msg_failed_tool", callID: "call_failed_tool", error: { type: "unknown", message: "tool failed" }, + metadata: { checkpoint: 1 }, + content: [{ type: "text", text: "partial output" }], executed: true, }, }, @@ -440,11 +441,11 @@ describe("runNonInteractivePrompt", () => { expect(output).toEqual({ stdout: "", stderr: "", exitCode: 0 }) }) - test("renders native failed tool output before the terminal error", async () => { + test("renders a native terminal failure snapshot when live progress was missed", async () => { const rendered: SessionMessageAssistantTool[] = [] const failed: SessionMessageAssistantTool[] = [] await capture({ - turn: failedTool, + turn: (inputID) => failedTool(inputID).filter((event) => event.type !== "session.tool.progress"), renderTool: (part) => { rendered.push(part) return Promise.resolve() diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index d09aa48238da..6c869b0e2e6b 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -1387,24 +1387,6 @@ export type SessionToolCalled = { } } -export type SessionToolFailed = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.tool.failed" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - callID: string - error: SessionStructuredError - result?: any - executed: boolean - resultState?: SessionMessageProviderState7 - } -} - export type ModelCompatibility = { reasoningField?: ModelReasoningField } export type ModelCost = { @@ -1851,11 +1833,11 @@ export type SessionMessageToolStateError = { result?: JsonValue } -export type SessionToolProgress = { +export type SessionToolSuccess = { id: string created: number metadata?: { [x: string]: any } - type: "session.tool.progress" + type: "session.tool.success" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef data: { @@ -1864,25 +1846,44 @@ export type SessionToolProgress = { callID: string structured: { [x: string]: any } content: Array + result?: any + executed: boolean + resultState?: SessionMessageProviderState6 } } -export type SessionToolSuccess = { +export type SessionToolFailed = { id: string created: number metadata?: { [x: string]: any } - type: "session.tool.success" + type: "session.tool.failed" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef data: { sessionID: string assistantMessageID: string callID: string - structured: { [x: string]: any } - content: Array + error: SessionStructuredError + content?: [LLMToolContent, ...Array] + metadata?: { [x: string]: any } result?: any executed: boolean - resultState?: SessionMessageProviderState6 + resultState?: SessionMessageProviderState7 + } +} + +export type SessionToolProgress = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.tool.progress" + location?: LocationRef + data: { + sessionID: string + assistantMessageID: string + callID: string + structured: { [x: string]: any } + content: Array } } @@ -2224,7 +2225,6 @@ export type SessionEventDurable = | SessionToolInputStarted | SessionToolInputEnded | SessionToolCalled - | SessionToolProgress | SessionToolSuccess | SessionToolFailed | SessionRetryScheduled diff --git a/packages/core/src/database/migration.gen.ts b/packages/core/src/database/migration.gen.ts index c4067c4765ed..dd6214b095a6 100644 --- a/packages/core/src/database/migration.gen.ts +++ b/packages/core/src/database/migration.gen.ts @@ -55,5 +55,6 @@ export const migrations = ( import("./migration/20260709190621_session_pending_table"), import("./migration/20260710025429_instruction_sync"), import("./migration/20260716020354_kv"), + import("./migration/20260722011141_delete_tool_progress_events"), ]) ).map((module) => module.default) satisfies DatabaseMigration.Migration[] diff --git a/packages/core/src/database/migration/20260722011141_delete_tool_progress_events.ts b/packages/core/src/database/migration/20260722011141_delete_tool_progress_events.ts new file mode 100644 index 000000000000..afb8f4433471 --- /dev/null +++ b/packages/core/src/database/migration/20260722011141_delete_tool_progress_events.ts @@ -0,0 +1,11 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260722011141_delete_tool_progress_events", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DELETE FROM \`event\` WHERE \`type\` = 'session.tool.progress.1';`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/session/message-updater.ts b/packages/core/src/session/message-updater.ts index 00b0927cf44f..ecc6bf6257a3 100644 --- a/packages/core/src/session/message-updater.ts +++ b/packages/core/src/session/message-updater.ts @@ -402,8 +402,8 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { status: "error", error: event.data.error, input: typeof match.state.input === "string" ? {} : match.state.input, - structured: match.state.status === "running" ? match.state.structured : {}, - content: match.state.status === "running" ? match.state.content : [], + structured: event.data.metadata ?? (match.state.status === "running" ? match.state.structured : {}), + content: event.data.content ?? (match.state.status === "running" ? match.state.content : []), result: event.data.result, }), ) diff --git a/packages/core/src/session/projector.ts b/packages/core/src/session/projector.ts index 97044675d165..f759a3757f28 100644 --- a/packages/core/src/session/projector.ts +++ b/packages/core/src/session/projector.ts @@ -697,7 +697,6 @@ const layer = Layer.effectDiscard( yield* events.project(SessionEvent.Tool.Input.Started, (event) => run(db, event)) yield* events.project(SessionEvent.Tool.Input.Ended, (event) => run(db, event)) yield* events.project(SessionEvent.Tool.Called, (event) => run(db, event)) - yield* events.project(SessionEvent.Tool.Progress, (event) => run(db, event)) yield* events.project(SessionEvent.Tool.Success, (event) => run(db, event)) yield* events.project(SessionEvent.Tool.Failed, (event) => run(db, event)) yield* events.project(SessionEvent.Reasoning.Started, (event) => run(db, event)) diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 2a8aee08cc82..ad792ce1c74d 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -163,16 +163,7 @@ const layer = Layer.effect( agent: agent.id, messageID: assistantMessageID, call: event, - progress: (update) => - serialized( - events.publish(SessionEvent.Tool.Progress, { - sessionID: session.id, - assistantMessageID, - callID: event.id, - structured: { ...update.structured }, - content: [...update.content], - }), - ), + progress: (update) => serialized(publisher.progress(event.id, update)), }), ).pipe( Effect.flatMap((settlement) => diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts index 1344d2627a73..df3e95089597 100644 --- a/packages/core/src/session/runner/publish-llm-event.ts +++ b/packages/core/src/session/runner/publish-llm-event.ts @@ -11,6 +11,7 @@ import { AgentV2 } from "../../agent" import { Snapshot } from "../../snapshot" import { RelativePath } from "../../schema" import { SessionUsage } from "../usage" +import type { ToolRegistry } from "../../tool/registry" type Input = { readonly sessionID: SessionSchema.ID @@ -54,8 +55,17 @@ export const createLLMEventPublisher = (events: Pick() + const failureSnapshot = (tool: { readonly progress?: ToolRegistry.Progress }) => { + if (!tool.progress) return {} + const first = tool.progress.content[0] + return { + ...(first === undefined ? {} : { content: [first, ...tool.progress.content.slice(1)] as const }), + metadata: tool.progress.structured, + } + } let assistantMessageID = input.assistantMessageID let stepStarted = false let stepFailed = false @@ -232,6 +242,7 @@ export const createLLMEventPublisher = (events: Pick - context.progress({ - structured: { truncated: capture.truncated }, - content: [{ type: "text", text: capture.output }], + Effect.gen(function* () { + if ( + previousProgress?.output === capture.output && + previousProgress.truncated === capture.truncated + ) + return + previousProgress = capture + yield* context.progress({ + structured: { truncated: capture.truncated }, + content: [{ type: "text", text: capture.output }], + }) }), ), ), diff --git a/packages/core/test/database-migration.test.ts b/packages/core/test/database-migration.test.ts index 2c68d2e186fc..58673f20393c 100644 --- a/packages/core/test/database-migration.test.ts +++ b/packages/core/test/database-migration.test.ts @@ -24,6 +24,7 @@ import renameInstructionsMigration from "@opencode-ai/core/database/migration/20 import addSessionForkMigration from "@opencode-ai/core/database/migration/20260706223930_add-session-fork" import timeSuspendedMigration from "@opencode-ai/core/database/migration/20260709163752_time_suspended" import instructionSyncMigration from "@opencode-ai/core/database/migration/20260710025429_instruction_sync" +import deleteToolProgressEventsMigration from "@opencode-ai/core/database/migration/20260722011141_delete_tool_progress_events" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { EventV2 } from "@opencode-ai/core/event" @@ -557,6 +558,31 @@ describe("DatabaseMigration", () => { ) }) + test("deletes durable tool progress without changing aggregate sequence watermarks", async () => { + await run( + Effect.gen(function* () { + const db = yield* makeDb + yield* db.run(sql`CREATE TABLE event_sequence (aggregate_id text PRIMARY KEY, seq integer NOT NULL)`) + yield* db.run( + sql`CREATE TABLE event (id text PRIMARY KEY, aggregate_id text NOT NULL, seq integer NOT NULL, type text NOT NULL, data text NOT NULL)`, + ) + yield* db.run(sql`INSERT INTO event_sequence VALUES ('ses_test', 5)`) + yield* db.run(sql`INSERT INTO event VALUES ('evt_success', 'ses_test', 4, 'session.tool.success.1', '{}')`) + yield* db.run(sql`INSERT INTO event VALUES ('evt_progress', 'ses_test', 5, 'session.tool.progress.1', '{}')`) + + yield* DatabaseMigration.applyOnly(db, [deleteToolProgressEventsMigration]) + + expect(yield* db.all(sql`SELECT id, seq, type, data FROM event ORDER BY seq`)).toEqual([ + { id: "evt_success", seq: 4, type: "session.tool.success.1", data: "{}" }, + ]) + expect(yield* db.get(sql`SELECT aggregate_id, seq FROM event_sequence`)).toEqual({ + aggregate_id: "ses_test", + seq: 5, + }) + }), + ) + }) + test("records the authoritative parent sequence on existing forks", async () => { await run( Effect.gen(function* () { diff --git a/packages/core/test/session-runner-tool-events.test.ts b/packages/core/test/session-runner-tool-events.test.ts index b6f44dccdbc8..66a0a43f545f 100644 --- a/packages/core/test/session-runner-tool-events.test.ts +++ b/packages/core/test/session-runner-tool-events.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test" -import { Effect, Schema } from "effect" +import { Cause, Effect, Exit, Schema } from "effect" import { LLMEvent } from "@opencode-ai/ai" import { Money } from "@opencode-ai/schema/money" import { EventV2 } from "@opencode-ai/core/event" @@ -16,11 +16,11 @@ import { createLLMEventPublisher } from "@opencode-ai/core/session/runner/publis const sessionID = SessionV2.ID.make("ses_tool_event_test") const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB" -const capture = (providerMetadataKey = "anthropic") => { +const capture = (providerMetadataKey = "anthropic", options?: { readonly interruptProgress?: boolean }) => { const published: Array<{ readonly type: string; readonly data: unknown }> = [] const events: Pick = { - publish: (definition, data) => - Effect.sync(() => { + publish: (definition, data) => { + const publish = Effect.sync(() => { const event = { id: EventV2.ID.create(), type: definition.type, data } as EventV2.Payload published.push({ type: definition.durable @@ -29,7 +29,11 @@ const capture = (providerMetadataKey = "anthropic") => { data, }) return event - }), + }) + return definition.type === SessionEvent.Tool.Progress.type && options?.interruptProgress + ? publish.pipe(Effect.andThen(Effect.interrupt)) + : publish + }, } return { published, @@ -92,6 +96,34 @@ test("provider-executed success retains its raw provider result", async () => { expect(success?.data).toHaveProperty("result") }) +test("interrupted progress publication remains in the terminal failure snapshot", async () => { + const { published, publisher } = capture("anthropic", { interruptProgress: true }) + await Effect.runPromise(publisher.publish(call)) + const exit = await Effect.runPromiseExit( + publisher.progress(call.id, { + structured: { phase: "visible" }, + content: [{ type: "text", text: "visible" }], + }), + ) + expect(Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)).toBe(true) + await Effect.runPromise(publisher.failUnsettledTools({ type: "aborted", message: "interrupted" })) + + expect(published.find((event) => event.type === "session.tool.failed.1")?.data).toMatchObject({ + metadata: { phase: "visible" }, + content: [{ type: "text", text: "visible" }], + }) +}) + +test("failure before progress omits partial output fields", async () => { + const { published, publisher } = capture() + await Effect.runPromise(publisher.publish(call)) + await Effect.runPromise(publisher.failUnsettledTools({ type: "aborted", message: "interrupted" })) + + const failed = published.find((event) => event.type === "session.tool.failed.1")?.data + expect(failed).not.toHaveProperty("content") + expect(failed).not.toHaveProperty("metadata") +}) + test("provider metadata is flattened using the route key", async () => { const { published, publisher } = capture() await Effect.runPromise( diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index dc2cb2d9dff9..6ce9cf0f203d 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -892,6 +892,52 @@ describe("SessionRunnerLLM", () => { }), ) + it.effect("persists the latest partial snapshot when a tool fails", () => + Effect.gen(function* () { + const session = yield* setup + const registry = yield* ToolRegistry.Service + yield* registry.register({ + failing_progress: Tool.make({ + description: "Report progress and fail", + input: Schema.Struct({}), + output: Schema.Struct({}), + execute: (_, context) => + Effect.gen(function* () { + yield* context.progress({ + structured: { phase: "running" }, + content: [{ type: "text", text: "before failure" }], + }) + return yield* new ToolFailure({ message: "failed after progress" }) + }), + }), + }, { codemode: false }) + yield* admit(session, "Run failing progress") + responses = [reply.tool("call-failing-progress", "failing_progress", {}), reply.stop()] + + yield* session.resume(sessionID) + + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Run failing progress" }, + { + type: "assistant", + content: [ + { + type: "tool", + id: "call-failing-progress", + state: { + status: "error", + structured: { phase: "running" }, + content: [{ type: "text", text: "before failure" }], + error: { message: "failed after progress" }, + }, + }, + ], + }, + { type: "assistant", finish: "stop" }, + ]) + }), + ) + it.effect("executes the tool advertised before a registry reload", () => Effect.gen(function* () { const session = yield* setup diff --git a/packages/core/test/session-tool-progress.test.ts b/packages/core/test/session-tool-progress.test.ts index 0935ee36338b..cc81820e39f1 100644 --- a/packages/core/test/session-tool-progress.test.ts +++ b/packages/core/test/session-tool-progress.test.ts @@ -22,10 +22,10 @@ const it = testEffect(LayerNode.compile(LayerNode.group([Database.node, EventV2. const timestamp = DateTime.makeUnsafe(1) const model = { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") } -const content = (text: string) => [{ type: "text" as const, text }] +const content = (text: string) => [{ type: "text" as const, text }] as const describe("Tool.Progress", () => { - it.effect("projects durable progress and keeps final settlements durable", () => + it.effect("keeps progress live-only and terminal settlements durable", () => Effect.gen(function* () { const { db } = yield* Database.Service const service = yield* EventV2.Service @@ -87,7 +87,7 @@ describe("Tool.Progress", () => { state: { status: "running", structured: {}, content: [] }, }) - yield* service.publish(SessionEvent.Tool.Progress, { + const progress = yield* service.publish(SessionEvent.Tool.Progress, { sessionID, assistantMessageID, callID: "call-success", @@ -95,7 +95,7 @@ describe("Tool.Progress", () => { content: content("saved"), }) expect((yield* readAssistant).content[0]).toMatchObject({ - state: { status: "running", structured: { phase: "checkpoint" }, content: content("saved") }, + state: { status: "running", structured: {}, content: [] }, }) const success = yield* service.publish(SessionEvent.Tool.Success, { @@ -123,6 +123,8 @@ describe("Tool.Progress", () => { assistantMessageID, callID: "call-failed", error: { type: "unknown", message: "boom" }, + metadata: { phase: "checkpoint" }, + content: content("before failure"), executed: false, }) expect((yield* readAssistant).content[1]).toMatchObject({ @@ -133,6 +135,7 @@ describe("Tool.Progress", () => { error: { type: "unknown", message: "boom" }, }, }) + expect(Schema.is(SessionEvent.Durable)(progress)).toBe(false) expect(Schema.is(SessionEvent.Durable)(success)).toBe(true) expect(Schema.is(SessionEvent.Durable)(failed)).toBe(true) @@ -143,7 +146,7 @@ describe("Tool.Progress", () => { .orderBy(asc(EventTable.seq)) .all() .pipe(Effect.orDie) - expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Progress.type, 1)) + expect(rows.map((row) => row.type)).not.toContain(EventV2.versionedType(SessionEvent.Tool.Progress.type, 1)) expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Success.type, 1)) expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Failed.type, 1)) }), diff --git a/packages/core/test/tool-shell.test.ts b/packages/core/test/tool-shell.test.ts index 2b66b2ec8db1..8d79cde7a911 100644 --- a/packages/core/test/tool-shell.test.ts +++ b/packages/core/test/tool-shell.test.ts @@ -159,6 +159,9 @@ const mixedOutputCommand = isWindows ? "[Console]::Out.Write('stdout'); Start-Sleep -Milliseconds 50; [Console]::Error.Write('stderr'); Start-Sleep -Milliseconds 100" : "printf stdout; sleep 0.05; printf stderr >&2" const idleCommand = isWindows ? "Start-Sleep -Seconds 60" : "sleep 60" +const steadyProgressCommand = isWindows + ? "[Console]::Out.Write('steady'); Start-Sleep -Milliseconds 3400" + : "printf steady; sleep 3.4" const bodyExitCommand = isWindows ? "[Console]::Out.Write('body'); Start-Sleep -Milliseconds 100; exit 7" : "printf body && exit 7" @@ -462,6 +465,34 @@ describe("ShellTool", () => { { timeout: 15_000 }, ) + it.live( + "does not repeat unchanged shell progress", + () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + return withSession(tmp.path, (registry) => + Effect.gen(function* () { + const updates: ToolRegistry.Progress[] = [] + yield* settleTool(registry, { + ...call({ command: steadyProgressCommand }, "call-steady-progress"), + progress: (update) => Effect.sync(() => updates.push(update)), + }) + expect(updates).toEqual([ + { + structured: { truncated: false }, + content: [{ type: "text", text: "steady" }], + }, + ]) + }), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]().then(() => undefined)), + ), + { timeout: 10_000 }, + ) + it.live("returns a useful timeout settlement", () => Effect.acquireUseRelease( Effect.promise(() => tmpdir()), diff --git a/packages/schema/src/session-event.ts b/packages/schema/src/session-event.ts index 730b0b1ffb55..cc56a83eab94 100644 --- a/packages/schema/src/session-event.ts +++ b/packages/schema/src/session-event.ts @@ -3,8 +3,7 @@ export * as SessionEvent from "./session-event.js" import { Schema } from "effect" import { optional } from "./schema.js" import { Event } from "./event.js" -import { ToolContent } from "./llm.js" -import { FinishReason } from "./llm.js" +import { FinishReason, ToolContent } from "./llm.js" import { Model } from "./model.js" import { NonNegativeInt, PositiveInt, RelativePath } from "./schema.js" import { FileAttachment } from "./prompt.js" @@ -410,13 +409,9 @@ export namespace Tool { }) 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 = Event.durable({ + /** Live replacement snapshot for a running tool. */ + export const Progress = Event.ephemeral({ type: "session.tool.progress", - ...options, schema: { ...ToolBase, structured: Schema.Record(Schema.String, Schema.Unknown), @@ -445,6 +440,8 @@ export namespace Tool { schema: { ...ToolBase, error: SessionError.Error, + content: Schema.NonEmptyArray(ToolContent).pipe(optional), + metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(optional), result: Schema.Unknown.pipe(optional), executed: Schema.Boolean, resultState: SessionMessage.ProviderState.pipe(optional), diff --git a/packages/schema/test/event-manifest.test.ts b/packages/schema/test/event-manifest.test.ts index 512528f5560b..89766d3d262e 100644 --- a/packages/schema/test/event-manifest.test.ts +++ b/packages/schema/test/event-manifest.test.ts @@ -125,7 +125,6 @@ describe("public event manifest", () => { "session.tool.input.started.1", "session.tool.input.ended.1", "session.tool.called.1", - "session.tool.progress.1", "session.tool.success.1", "session.tool.failed.1", "session.reasoning.started.1", @@ -152,6 +151,8 @@ describe("public event manifest", () => { expect(EventManifest.Latest.has("session.usage.recorded")).toBe(false) expect(SessionEvent.UsageUpdated.durability).toBe("ephemeral") expect(SessionEvent.Compaction.Delta.durability).toBe("ephemeral") + expect(SessionEvent.Tool.Progress.durability).toBe("ephemeral") + expect(EventManifest.Server.get("session.tool.progress")).toBe(SessionEvent.Tool.Progress) expect(EventManifest.Durable.has("session.compaction.delta.1")).toBe(false) expect(EventManifest.ServerDefinitions).toContain(SessionEvent.UsageUpdated) expect(EventManifest.Definitions.every((definition) => definition.durability !== undefined)).toBe(true) diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index 41e9b68236fa..ddf3823e756c 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -643,8 +643,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ status: "error", error: event.data.error, input: typeof match.state.input === "string" ? {} : match.state.input, - structured: match.state.status === "running" ? match.state.structured : {}, - content: match.state.status === "running" ? match.state.content : [], + structured: event.data.metadata ?? (match.state.status === "running" ? match.state.structured : {}), + content: event.data.content ?? (match.state.status === "running" ? match.state.content : []), result: event.data.result, } match.executed = event.data.executed || match.executed === true diff --git a/packages/tui/src/mini/stream-v2.subagent.ts b/packages/tui/src/mini/stream-v2.subagent.ts index 7ae8865cad4c..ea3b566af43f 100644 --- a/packages/tui/src/mini/stream-v2.subagent.ts +++ b/packages/tui/src/mini/stream-v2.subagent.ts @@ -837,8 +837,9 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac ? { status: "error", input: part && part.state.status !== "streaming" ? part.state.input : {}, - structured: part && part.state.status !== "streaming" ? part.state.structured : {}, - content: part && part.state.status !== "streaming" ? part.state.content : [], + structured: + event.data.metadata ?? (part && part.state.status !== "streaming" ? part.state.structured : {}), + content: event.data.content ?? (part && part.state.status !== "streaming" ? part.state.content : []), error: event.data.error, result: event.data.result, } @@ -957,15 +958,16 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac if (pendingCalls.has(key)) pendingCalls.set(key, event.data.input) return } - if (event.type === "session.tool.failed") { - pendingCalls.delete(sourceKey(event.data.assistantMessageID, event.data.callID)) + if ( + event.type !== "session.tool.progress" && + event.type !== "session.tool.success" && + event.type !== "session.tool.failed" + ) return - } - if (event.type !== "session.tool.progress" && event.type !== "session.tool.success") return const key = sourceKey(event.data.assistantMessageID, event.data.callID) const pending = pendingCalls.get(key) - if (event.type === "session.tool.success") pendingCalls.delete(key) - const found = childSessionID(record(event.data.structured)) + if (event.type !== "session.tool.progress") pendingCalls.delete(key) + const found = childSessionID(record(event.type === "session.tool.failed" ? event.data.metadata : event.data.structured)) if (!found) return const child = admitChild(found.sessionID) if (!child) return diff --git a/packages/tui/src/mini/stream-v2.transport.ts b/packages/tui/src/mini/stream-v2.transport.ts index 964d78e1efb3..496bda83b387 100644 --- a/packages/tui/src/mini/stream-v2.transport.ts +++ b/packages/tui/src/mini/stream-v2.transport.ts @@ -1084,8 +1084,9 @@ export async function createSessionTransport(input: StreamInput): Promise { id: "evt_progress_1", created: 0, type: "session.tool.progress", - durable: durable("session-1", 5), data: { sessionID: "session-1", assistantMessageID: "msg_explicit_assistant_9", diff --git a/packages/tui/test/mini/stream-v2.transport.test.ts b/packages/tui/test/mini/stream-v2.transport.test.ts index efe18ac64ae3..f2c72ea6f051 100644 --- a/packages/tui/test/mini/stream-v2.transport.test.ts +++ b/packages/tui/test/mini/stream-v2.transport.test.ts @@ -2044,7 +2044,6 @@ describe("V2 mini transport", () => { id: "evt_progress", created: 3, type: "session.tool.progress", - durable: durable("ses_1", 2), data: { sessionID: "ses_1", assistantMessageID: "msg_progress", @@ -2063,6 +2062,8 @@ describe("V2 mini transport", () => { assistantMessageID: "msg_progress", callID: "call_progress", error: { type: "unknown", message: "boom" }, + metadata: { checkpoint: 1 }, + content: [{ type: "text", text: "partial" }], executed: true, }, }) @@ -2844,6 +2845,70 @@ describe("V2 mini transport", () => { await transport.close() }) + test("discovers a subagent from its terminal failure snapshot", async () => { + const events = feed() + events.push(connected()) + const client = sdk({ streams: [events], messages: { ses_child_failed: [] } }) + const ui = footer() + const transport = await createSessionTransport({ + sdk: client, + sessionID: "ses_1", + thinking: false, + footer: ui.api, + }) + const states = () => ui.events.flatMap((event) => (event.type === "stream.subagent" ? [event.state] : [])) + events.push({ + id: "evt_failed_subagent_input", + created: 1, + type: "session.tool.input.started", + durable: durable("ses_1"), + data: { + sessionID: "ses_1", + assistantMessageID: "msg_failed_subagent", + callID: "call_failed_subagent", + name: "subagent", + }, + }) + events.push({ + id: "evt_failed_subagent_called", + created: 2, + type: "session.tool.called", + durable: durable("ses_1", 1), + data: { + sessionID: "ses_1", + assistantMessageID: "msg_failed_subagent", + callID: "call_failed_subagent", + input: { agent: "explore", description: "Inspect failure", prompt: "inspect" }, + executed: true, + }, + }) + events.push({ + id: "evt_failed_subagent", + created: 3, + type: "session.tool.failed", + durable: durable("ses_1", 2), + data: { + sessionID: "ses_1", + assistantMessageID: "msg_failed_subagent", + callID: "call_failed_subagent", + error: { type: "unknown", message: "subagent failed" }, + metadata: { sessionID: "ses_child_failed", status: "running" }, + executed: true, + }, + }) + + while (!states().some((state) => state.tabs.some((tab) => tab.sessionID === "ses_child_failed"))) + await Bun.sleep(0) + expect(states().at(-1)?.tabs).toMatchObject([ + { + sessionID: "ses_child_failed", + label: "Explore", + description: "Inspect failure", + }, + ]) + await transport.close() + }) + test("discovers current subagents from progress and reduces descendant tool state", async () => { const events = feed() events.push(connected()) @@ -2885,7 +2950,6 @@ describe("V2 mini transport", () => { id: "evt_subagent_progress", created: 3, type: "session.tool.progress", - durable: durable("ses_1", 2), data: { sessionID: "ses_1", assistantMessageID: "msg_subagent", @@ -2937,7 +3001,6 @@ describe("V2 mini transport", () => { id: "evt_child_tool_progress", created: 6, type: "session.tool.progress", - durable: durable("ses_child_progress", 2), data: { sessionID: "ses_child_progress", assistantMessageID: "msg_child_tool", @@ -2968,6 +3031,8 @@ describe("V2 mini transport", () => { assistantMessageID: "msg_child_tool", callID: "call_child_shell", error: { type: "unknown", message: "child boom" }, + metadata: { checkpoint: "child" }, + content: [{ type: "text", text: "child partial" }], executed: true, }, }) From 03474816ea61f6fe190b242bc36f3fc1069a2285 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 09:40:31 -0500 Subject: [PATCH 026/150] test(core): stabilize wellknown event subscriptions (#38331) Co-authored-by: Aiden Cline --- packages/core/test/wellknown.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/test/wellknown.test.ts b/packages/core/test/wellknown.test.ts index 829f60ccd789..266e96f7546c 100644 --- a/packages/core/test/wellknown.test.ts +++ b/packages/core/test/wellknown.test.ts @@ -69,7 +69,7 @@ serviceIt.live("persists sources in one KV value", () => const events = yield* EventV2.Service const changed = yield* events .subscribe(WellKnown.Event.Updated) - .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) + .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped({ startImmediately: true })) const entry = yield* wellknown.add(`${server.url.origin}/`) expect(entry.origin).toBe(server.url.origin) @@ -108,7 +108,7 @@ serviceIt.live("refreshes changed manifests", () => const changed = yield* events .subscribe(WellKnown.Event.Updated) - .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) + .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped({ startImmediately: true })) update() expect(yield* wellknown.refresh()).toBe(true) expect(yield* Fiber.join(changed)).toHaveLength(1) From aea36d7630244570bcc158cf91f2cfa63e662e2c Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 10:11:58 -0500 Subject: [PATCH 027/150] fix(tui): prevent duplicate message forks (#38240) Co-authored-by: Aiden Cline --- packages/tui/src/routes/session/dialog-fork.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tui/src/routes/session/dialog-fork.tsx b/packages/tui/src/routes/session/dialog-fork.tsx index 428ef80ecf9b..4a8ac9ed8f31 100644 --- a/packages/tui/src/routes/session/dialog-fork.tsx +++ b/packages/tui/src/routes/session/dialog-fork.tsx @@ -16,7 +16,7 @@ export function DialogFork(props: { sessionID: string; messageID?: string; onMov const client = useClient() const route = useRoute() const toast = useToast() - const [pending, setPending] = createSignal(false) + const [pending, setPending] = createSignal(!!props.messageID) const fork = async (messageID?: string) => { setPending(true) From dba5da7c10db26cb4430123281cbeafe7e4c2f61 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 22 Jul 2026 11:23:50 -0400 Subject: [PATCH 028/150] fix(tui): restore shell mode styling (#38231) --- packages/tui/src/component/prompt/index.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index 7f5e081cff92..ff4b516a889b 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -1300,11 +1300,16 @@ export function Prompt(props: PromptProps) { const highlight = createMemo(() => { if (leader()) return themeV2.border.default - if (store.mode === "shell") return themeV2.background.action.primary.default + if (store.mode === "shell") return themeV2.text.action.primary.selected const agent = local.agent.current() if (!agent) return themeV2.border.default return local.agent.color(agent.id) }) + const agentLabel = createMemo(() => { + if (store.mode === "shell") return "Shell" + const agent = local.agent.current() + return agent ? Locale.titlecase(agent.id) : undefined + }) const showVariant = createMemo(() => { const variants = local.model.variant.list() @@ -1313,7 +1318,7 @@ export function Prompt(props: PromptProps) { return !!current }) - const agentMetaAlpha = createFadeIn(() => !!local.agent.current(), animationsEnabled) + const agentMetaAlpha = createFadeIn(() => store.mode === "shell" || !!local.agent.current(), animationsEnabled) const modelMetaAlpha = createFadeIn(() => !!local.agent.current() && store.mode === "normal", animationsEnabled) const variantMetaAlpha = createFadeIn( () => !!local.agent.current() && store.mode === "normal" && showVariant(), @@ -1460,12 +1465,10 @@ export function Prompt(props: PromptProps) { /> - }> - {(agent) => ( + }> + {(label) => ( <> - - {store.mode === "shell" ? "Shell" : Locale.titlecase(agent().id)} - + {label()} auto From b91dd78ab326e5f4e12de1e05fe0b486a4036fbb Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 11:50:40 -0400 Subject: [PATCH 029/150] test(core): remove source text assertions (#38342) Co-authored-by: Kit Langton --- packages/core/test/tool-edit.test.ts | 25 +------------------------ packages/core/test/tool-shell.test.ts | 19 +------------------ packages/core/test/tool-write.test.ts | 25 +------------------------ 3 files changed, 3 insertions(+), 66 deletions(-) diff --git a/packages/core/test/tool-edit.test.ts b/packages/core/test/tool-edit.test.ts index 1e1d806d9369..aaecf9766fdd 100644 --- a/packages/core/test/tool-edit.test.ts +++ b/packages/core/test/tool-edit.test.ts @@ -1,7 +1,6 @@ import fs from "fs/promises" import path from "path" -import { fileURLToPath } from "url" -import { describe, expect, test } from "bun:test" +import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" @@ -460,25 +459,3 @@ describe("EditTool", () => { ), ) }) - -test("keeps the locked edit schema, semantics docstring, and deferred TODOs visible", async () => { - const source = (await fs.readFile(new URL("../src/tool/edit.ts", import.meta.url), "utf8")).replaceAll("\r\n", "\n") - const definition = await Effect.runPromise( - withTool(path.dirname(fileURLToPath(import.meta.url)), (registry) => toolDefinitions(registry)), - ) - const schema = definition[0]?.inputSchema as { readonly properties?: Record } - - expect(Object.keys(schema.properties ?? {}).sort()).toEqual(["newString", "oldString", "path", "replaceAll"]) - expect(source).toContain( - "absolute external paths retain mutation capability through a separate\n * external_directory approval before edit approval.", - ) - for (const todo of [ - "Port V1 fuzzy correction strategies only after exact-edit behavior is established: line-trimmed matching, block-anchor fallback, indentation correction, and similarity-threshold review.", - "Add formatter integration after V2 formatter runtime exists.", - "Publish watcher/file-edit events after V2 watcher integration exists.", - "Add snapshots / undo after design exists.", - "Add LSP notification and diagnostics after V2 LSP runtime exists.", - ]) { - expect(source).toContain(`TODO: ${todo}`) - } -}) diff --git a/packages/core/test/tool-shell.test.ts b/packages/core/test/tool-shell.test.ts index 8d79cde7a911..039680a8d186 100644 --- a/packages/core/test/tool-shell.test.ts +++ b/packages/core/test/tool-shell.test.ts @@ -1,7 +1,7 @@ import fs from "fs/promises" import { realpathSync } from "node:fs" import path from "path" -import { describe, expect, test } from "bun:test" +import { describe, expect } from "bun:test" import { DateTime, Deferred, Duration, Effect, Fiber, Layer, Scope, Stream } from "effect" import { Money } from "@opencode-ai/schema/money" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" @@ -643,20 +643,3 @@ describe("ShellTool", () => { ), ) }) - -test("keeps locked deferred parity TODOs visible", async () => { - const source = await fs.readFile(new URL("../src/tool/shell.ts", import.meta.url), "utf8") - for (const todo of [ - "Port tree-sitter bash / PowerShell parser-based approval reduction.", - "Port BashArity reusable command-prefix approvals.", - "Replace token-based command-argument external-directory advisories with parser-based detection.", - "Restore PowerShell and cmd-specific invocation/path handling on Windows.", - "Add plugin shell.env environment augmentation once V2 plugin hooks exist.", - "Persist job status and define restart recovery before exposing remote observation.", - "Revisit process-group cleanup and platform coverage with shell-specific tests if current AppProcess semantics do not fully cover it.", - "Revisit binary output handling if stdout/stderr decoding is text-only.", - "Stream full shell output into managed storage while retaining only a bounded in-memory preview.", - ]) { - expect(source).toContain(`TODO: ${todo}`) - } -}) diff --git a/packages/core/test/tool-write.test.ts b/packages/core/test/tool-write.test.ts index 6e4d61c57c2e..67a001e0eb08 100644 --- a/packages/core/test/tool-write.test.ts +++ b/packages/core/test/tool-write.test.ts @@ -1,7 +1,6 @@ import fs from "fs/promises" import path from "path" -import { fileURLToPath } from "url" -import { describe, expect, test } from "bun:test" +import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { FileMutation } from "@opencode-ai/core/file-mutation" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" @@ -363,25 +362,3 @@ describe("WriteTool", () => { ), ) }) - -test("keeps the locked write schema, semantics docstring, and deferred UX TODOs visible", async () => { - const source = (await fs.readFile(new URL("../src/tool/write.ts", import.meta.url), "utf8")).replaceAll("\r\n", "\n") - const definition = await Effect.runPromise( - withTool(path.dirname(fileURLToPath(import.meta.url)), (registry) => toolDefinitions(registry)), - ) - const schema = definition[0]?.inputSchema as { readonly properties?: Record } - - expect(Object.keys(schema.properties ?? {}).sort()).toEqual(["content", "path"]) - expect(source).toContain( - "absolute external paths retain mutation capability through a separate\n * external_directory approval before edit approval.", - ) - for (const todo of [ - "Revisit whether model-facing mutation schemas should prefer absolute `filePath` naming for trained-in compatibility after evaluating model behavior.", - "Add formatter integration after V2 formatter runtime exists.", - "Publish watcher/file-edit events after V2 watcher integration exists.", - "Add snapshots / undo after design exists.", - "Add LSP notification and diagnostics after V2 LSP runtime exists.", - ]) { - expect(source).toContain(`TODO: ${todo}`) - } -}) From 7a1f9764a2ed3fc1779552c50b263d925978b18a Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 22 Jul 2026 12:09:48 -0400 Subject: [PATCH 030/150] feat(core): compact durable tool metadata (#38343) --- packages/cli/test/run/noninteractive.test.ts | 73 ++++++++++++++++++++ packages/core/src/tool/glob.ts | 7 +- packages/core/src/tool/grep.ts | 7 +- packages/core/src/tool/skill.ts | 6 ++ packages/core/src/tool/subagent.ts | 6 ++ packages/core/src/tool/webfetch.ts | 5 ++ packages/core/src/tool/websearch.ts | 5 ++ packages/core/test/tool-search.test.ts | 8 ++- packages/core/test/tool-skill.test.ts | 7 +- packages/core/test/tool-subagent.test.ts | 28 ++++++-- packages/core/test/tool-webfetch.test.ts | 2 +- packages/core/test/tool-websearch.test.ts | 2 +- packages/tui/src/routes/session/index.tsx | 3 +- packages/tui/test/mini/tool.test.ts | 24 +++++++ 14 files changed, 169 insertions(+), 14 deletions(-) diff --git a/packages/cli/test/run/noninteractive.test.ts b/packages/cli/test/run/noninteractive.test.ts index 904ced747434..f0a680061f24 100644 --- a/packages/cli/test/run/noninteractive.test.ts +++ b/packages/cli/test/run/noninteractive.test.ts @@ -157,6 +157,53 @@ function failedTool(inputID: string): V2Event[] { ] } +function successfulGrep(inputID: string): V2Event[] { + const text = "Found 2 matches\n/src/a.ts:\n Line 1: needle\n/src/b.ts:\n Line 2: needle" + return [ + prompted(inputID), + { + id: "evt_grep_input", + created: 1, + type: "session.tool.input.started", + durable: { aggregateID: "ses_1", seq: 1, version: 1 }, + data: { + sessionID: "ses_1", + assistantMessageID: "msg_grep", + callID: "call_grep", + name: "grep", + }, + }, + { + id: "evt_grep_called", + created: 2, + type: "session.tool.called", + durable: { aggregateID: "ses_1", seq: 2, version: 1 }, + data: { + sessionID: "ses_1", + assistantMessageID: "msg_grep", + callID: "call_grep", + input: { pattern: "needle" }, + executed: true, + }, + }, + { + id: "evt_grep_success", + created: 3, + type: "session.tool.success", + durable: { aggregateID: "ses_1", seq: 3, version: 1 }, + data: { + sessionID: "ses_1", + assistantMessageID: "msg_grep", + callID: "call_grep", + structured: { matches: 2 }, + content: [{ type: "text", text }], + executed: false, + }, + }, + settled(), + ] +} + // Runs one non-interactive prompt against a mocked SDK. `turn` produces the // live events the prompt admission triggers, keyed by the generated message ID. async function run(input: { @@ -269,6 +316,32 @@ afterEach(() => { }) describe("runNonInteractivePrompt", () => { + test("keeps formatted tool output and compact structured metadata in JSON", async () => { + const output = await capture({ format: "json", turn: successfulGrep }) + const events = output.stdout + .split("\n") + .filter(Boolean) + .map((line) => JSON.parse(line)) + + expect(events).toHaveLength(1) + expect(events[0]).toMatchObject({ + type: "tool_use", + part: { + tool: "grep", + state: { + status: "completed", + output: expect.stringContaining("Found 2 matches"), + metadata: { + structured: { matches: 2 }, + content: [{ type: "text", text: expect.stringContaining("/src/a.ts") }], + }, + }, + }, + }) + expect(events[0].part.state.metadata.structured).toEqual({ matches: 2 }) + expect(events[0].part.state.metadata.result).toBeUndefined() + }) + test("uses session.wait then reconciles projected output without a terminal event", async () => { const idle = Promise.withResolvers() let done = false diff --git a/packages/core/src/tool/glob.ts b/packages/core/src/tool/glob.ts index e231ec7c0ba5..27a7d26a0103 100644 --- a/packages/core/src/tool/glob.ts +++ b/packages/core/src/tool/glob.ts @@ -8,7 +8,7 @@ import { FileSystem } from "../filesystem" import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "../location" import { Ripgrep } from "../ripgrep" -import { RelativePath } from "../schema" +import { NonNegativeInt, RelativePath } from "../schema" import { PermissionV2 } from "../permission" import { Tool } from "./tool" @@ -25,6 +25,9 @@ export const Input = Schema.Struct({ }) export const Output = Schema.Array(FileSystem.Entry) +const StructuredOutput = Schema.Struct({ + count: NonNegativeInt, +}) type ModelOutput = typeof Output.Encoded /** Format raw search results into the concise line-oriented output models expect. */ @@ -51,6 +54,8 @@ export const Plugin = { "Find files by glob pattern within the active Location. Returns concise relative file resources. Use a relative path to narrow the search and limit to bound the result count.", input: Input, output: Output, + structured: StructuredOutput, + toStructuredOutput: ({ output }) => ({ count: output.length }), toModelOutput: ({ output }) => [ { type: "text", diff --git a/packages/core/src/tool/grep.ts b/packages/core/src/tool/grep.ts index a972c958659f..56cb004ddf8d 100644 --- a/packages/core/src/tool/grep.ts +++ b/packages/core/src/tool/grep.ts @@ -9,7 +9,7 @@ import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "../location" import { PermissionV2 } from "../permission" import { Ripgrep } from "../ripgrep" -import { RelativePath } from "../schema" +import { NonNegativeInt, RelativePath } from "../schema" import { Tool } from "./tool" export const name = "grep" @@ -30,6 +30,9 @@ export const Input = Schema.Struct({ }) export const Output = Schema.Array(FileSystem.Match) +const StructuredOutput = Schema.Struct({ + matches: NonNegativeInt, +}) type ModelOutput = typeof Output.Encoded /** Format raw search matches into the familiar concise model output. */ @@ -65,6 +68,8 @@ export const Plugin = { "Search file contents by regular expression within the active Location or an absolute managed tool-output file. Use a path to narrow the search, include to filter files by glob, and limit to bound the match count. Returns concise file resources, line numbers, and bounded line previews.", input: Input, output: Output, + structured: StructuredOutput, + toStructuredOutput: ({ output }) => ({ matches: output.length }), toModelOutput: ({ output }) => [ { type: "text", diff --git a/packages/core/src/tool/skill.ts b/packages/core/src/tool/skill.ts index 21fdef69c0be..dcf0e8bf70e5 100644 --- a/packages/core/src/tool/skill.ts +++ b/packages/core/src/tool/skill.ts @@ -21,6 +21,10 @@ export const Output = Schema.Struct({ directory: Schema.String, output: Schema.String, }) +const StructuredOutput = Schema.Struct({ + name: Output.fields.name, + directory: Output.fields.directory, +}) export const description = [ "Load a specialized skill when the task at hand matches one of the available skills in the instructions.", @@ -66,6 +70,8 @@ export const Plugin = { description, input: Input, output: Output, + structured: StructuredOutput, + toStructuredOutput: ({ output }) => ({ name: output.name, directory: output.directory }), toModelOutput: ({ output }) => [{ type: "text", text: output.output }], execute: (input, context) => Effect.gen(function* () { diff --git a/packages/core/src/tool/subagent.ts b/packages/core/src/tool/subagent.ts index 347a39988528..1f2f2f6c8bd5 100644 --- a/packages/core/src/tool/subagent.ts +++ b/packages/core/src/tool/subagent.ts @@ -31,6 +31,10 @@ export const Output = Schema.Struct({ status: Schema.Literals(["completed", "running"]), output: Schema.String, }) +const StructuredOutput = Schema.Struct({ + sessionID: Output.fields.sessionID, + status: Output.fields.status, +}) export const description = [ "Spawn a subagent: a child session running a configured agent with fresh context.", @@ -115,6 +119,8 @@ export const Plugin = { description, input: Input, output: Output, + structured: StructuredOutput, + toStructuredOutput: ({ output }) => ({ sessionID: output.sessionID, status: output.status }), toModelOutput: ({ output }) => [{ type: "text", text: output.output }], execute: (input, context) => Effect.gen(function* () { diff --git a/packages/core/src/tool/webfetch.ts b/packages/core/src/tool/webfetch.ts index e7a372e4b44f..0ce325099509 100644 --- a/packages/core/src/tool/webfetch.ts +++ b/packages/core/src/tool/webfetch.ts @@ -37,6 +37,9 @@ const Output = Schema.Struct({ format: Input.fields.format, output: Schema.String, }) +const StructuredOutput = Schema.Struct({ + contentType: Output.fields.contentType, +}) type Format = (typeof Input.Type)["format"] @@ -126,6 +129,8 @@ export const Plugin = { description, input: Input, output: Output, + structured: StructuredOutput, + toStructuredOutput: ({ output }) => ({ contentType: output.contentType }), toModelOutput: ({ output }) => [{ type: "text", text: output.output }], execute: (input, context) => Effect.gen(function* () { diff --git a/packages/core/src/tool/websearch.ts b/packages/core/src/tool/websearch.ts index c74a5d642bd3..49b059c9dffa 100644 --- a/packages/core/src/tool/websearch.ts +++ b/packages/core/src/tool/websearch.ts @@ -190,6 +190,9 @@ const Output = Schema.Struct({ provider: Provider, text: Schema.String, }) +const StructuredOutput = Schema.Struct({ + provider: Output.fields.provider, +}) export const Plugin = { id: "opencode.tool.websearch", @@ -206,6 +209,8 @@ export const Plugin = { description, input: Input, output: Output, + structured: StructuredOutput, + toStructuredOutput: ({ output }) => ({ provider: output.provider }), toModelOutput: ({ output }) => [{ type: "text", text: output.text }], execute: (input, context) => { const provider = selectProvider(context.sessionID, config, config.provider) diff --git a/packages/core/test/tool-search.test.ts b/packages/core/test/tool-search.test.ts index 3152c3f4e1e3..41e25953c418 100644 --- a/packages/core/test/tool-search.test.ts +++ b/packages/core/test/tool-search.test.ts @@ -86,8 +86,12 @@ describe("search tools", () => { const glob = yield* settleTool(registry, call("glob", { pattern: "*" })) const grep = yield* settleTool(registry, call("grep", { pattern: "needle" })) - expect(glob.output?.structured).toHaveLength(FileSystem.DEFAULT_SEARCH_LIMIT) - expect(grep.output?.structured).toHaveLength(FileSystem.DEFAULT_SEARCH_LIMIT) + expect(glob.output?.structured).toEqual({ count: FileSystem.DEFAULT_SEARCH_LIMIT }) + expect(grep.output?.structured).toEqual({ matches: FileSystem.DEFAULT_SEARCH_LIMIT }) + expect(glob.output?.content).toEqual([{ type: "text", text: glob.result.value }]) + expect(grep.output?.content).toEqual([{ type: "text", text: grep.result.value }]) + expect(String(glob.result.value).split("\n")).toHaveLength(FileSystem.DEFAULT_SEARCH_LIMIT) + expect(grep.result.value).toStartWith(`Found ${FileSystem.DEFAULT_SEARCH_LIMIT} matches\n`) }), ) }), diff --git a/packages/core/test/tool-skill.test.ts b/packages/core/test/tool-skill.test.ts index d6745b44d258..72f5565deec4 100644 --- a/packages/core/test/tool-skill.test.ts +++ b/packages/core/test/tool-skill.test.ts @@ -119,9 +119,12 @@ describe("SkillTool", () => { ...toolIdentity, call: { type: "tool-call", id: "call-skill-overflow", name: "skill", input: { id: "effect" } }, }), - ).toMatchObject({ + ).toEqual({ result: { type: "text", value: SkillTool.toModelOutput(info, [reference]) }, - output: { structured: { name: "Effect" } }, + output: { + structured: { name: "Effect", directory }, + content: [{ type: "text", text: SkillTool.toModelOutput(info, [reference]) }], + }, }) expect(assertions).toMatchObject([ { sessionID, action: "skill", resources: ["effect"], save: ["effect"] }, diff --git a/packages/core/test/tool-subagent.test.ts b/packages/core/test/tool-subagent.test.ts index bf17b9092c05..8c70525a9ab0 100644 --- a/packages/core/test/tool-subagent.test.ts +++ b/packages/core/test/tool-subagent.test.ts @@ -35,7 +35,8 @@ const childModel = ModelV2.Ref.make({ id: ModelV2.ID.make("child"), providerID: const parentModel = ModelV2.Ref.make({ id: ModelV2.ID.make("parent"), providerID: ProviderV2.ID.make("test") }) const tokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } } -const outputSessionID = (value: unknown) => Schema.decodeUnknownSync(SubagentTool.Output)(value).sessionID +const outputSessionID = (value: unknown) => + Schema.decodeUnknownSync(Schema.Struct({ sessionID: SessionV2.ID }))(value).sessionID const executionNode = makeGlobalNode({ service: SessionExecution.Service, @@ -229,7 +230,17 @@ describe("SubagentTool", () => { }, }) - expect(settled.output?.structured).toMatchObject({ status: "completed", output: childText }) + expect(settled).toMatchObject({ + result: { type: "text", value: childText }, + output: { + structured: { status: "completed" }, + content: [{ type: "text", text: childText }], + }, + }) + expect(settled.output?.structured).toEqual({ + sessionID: outputSessionID(settled.output?.structured), + status: "completed", + }) expect((yield* sessions.get(outputSessionID(settled.output?.structured))).parentID).toBe(parent.id) }), ), @@ -264,8 +275,15 @@ describe("SubagentTool", () => { }, }) - expect(settled.output?.structured).toMatchObject({ status: "completed", output: childText }) + expect(settled).toMatchObject({ + result: { type: "text", value: childText }, + output: { + structured: { status: "completed" }, + content: [{ type: "text", text: childText }], + }, + }) const child = yield* sessions.get(outputSessionID(settled.output?.structured)) + expect(settled.output?.structured).toEqual({ sessionID: child.id, status: "completed" }) expect(progress[0]?.structured).toEqual({ sessionID: child.id, status: "running" }) expect(child).toMatchObject({ parentID: parent.id, @@ -361,8 +379,10 @@ describe("SubagentTool", () => { const childID = outputSessionID(settled.output?.structured) expect(settled.output?.structured).toMatchObject({ status: "running", - output: expect.stringContaining(`id: ${childID}`), }) + expect(settled.output?.structured).toEqual({ sessionID: childID, status: "running" }) + expect(settled.result).toEqual({ type: "text", value: expect.stringContaining(`id: ${childID}`) }) + expect(settled.output?.content).toEqual([{ type: "text", text: expect.stringContaining(`id: ${childID}`) }]) const admission = Array.from(yield* Fiber.join(admitted))[0] expect(admission?.data.input.data.text).toContain(` { expect(yield* settleTool(registry, call({ url, format: "text", timeout: 4 }))).toEqual({ result: { type: "text", value: "hello" }, output: { - structured: { url, contentType: "text/plain", format: "text", output: "hello" }, + structured: { contentType: "text/plain" }, content: [{ type: "text", text: "hello" }], }, }) diff --git a/packages/core/test/tool-websearch.test.ts b/packages/core/test/tool-websearch.test.ts index f8468285a5fd..37d3960ab7c9 100644 --- a/packages/core/test/tool-websearch.test.ts +++ b/packages/core/test/tool-websearch.test.ts @@ -244,7 +244,7 @@ describe("WebSearchTool registration", () => { expect(settled).toEqual({ result: { type: "text", value: "parallel results" }, output: { - structured: { provider: "parallel", text: "parallel results" }, + structured: { provider: "parallel" }, content: [{ type: "text", text: "parallel results" }], }, }) diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 5a4fd04e828c..ca776bb22815 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -2669,8 +2669,7 @@ function WebFetch(props: ToolProps) { function WebSearch(props: ToolProps) { return ( - {webSearchProviderLabel(props.metadata.provider)} "{stringValue(props.input.query)}"{" "} - ({finiteNumber(props.metadata.numResults)} results) + {webSearchProviderLabel(props.metadata.provider)} "{stringValue(props.input.query)}" ) } diff --git a/packages/tui/test/mini/tool.test.ts b/packages/tui/test/mini/tool.test.ts index 86c40d1492a2..fb005cdf13ca 100644 --- a/packages/tui/test/mini/tool.test.ts +++ b/packages/tui/test/mini/tool.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test" import { normalizeTool, toolInlineInfo, toolOutputText, toolPath, toolScroll } from "../../src/mini/tool" +import { canonicalToolPart } from "./fixture/tool-part" describe("Mini tool presentation", () => { test("uses V2 shell output without the model-facing status", () => { @@ -105,6 +106,29 @@ describe("Mini tool presentation", () => { ).toBe('→ Skill "effect"') }) + test("renders compact search metadata", () => { + expect( + toolInlineInfo( + canonicalToolPart("glob", { + status: "completed", + input: { pattern: "*.ts" }, + structured: { count: 3 }, + content: [], + }), + ).description, + ).toBe("3 matches") + expect( + toolInlineInfo( + canonicalToolPart("grep", { + status: "completed", + input: { pattern: "needle" }, + structured: { matches: 1 }, + content: [], + }), + ).description, + ).toBe("1 match") + }) + test("keeps segment-safe contained tool paths relative", () => { expect(toolPath("..cache/result.txt", { directory: "/work/project" })).toBe("..cache/result.txt") expect(toolPath("../shared/result.txt", { directory: "/work/project" })).toBe("/work/shared/result.txt") From 89e3141079d7573a45f6e81f1ef1da73364135c0 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 22 Jul 2026 12:21:05 -0400 Subject: [PATCH 031/150] fix(core): preserve cache for session generation --- packages/core/src/session/generate-node.ts | 33 +++++++++++++++++---- packages/core/test/session-generate.test.ts | 16 ++++++++-- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/packages/core/src/session/generate-node.ts b/packages/core/src/session/generate-node.ts index 2214b179c9d8..f26e2847c2f1 100644 --- a/packages/core/src/session/generate-node.ts +++ b/packages/core/src/session/generate-node.ts @@ -12,6 +12,7 @@ import { SessionGenerate } from "./generate" import { SessionHistory } from "./history" import { SessionModelHeaders } from "./model-headers" import { SessionRunnerModel } from "./runner/model" +import { ToolRegistry } from "../tool/registry" import PROMPT_DEFAULT from "./runner/prompt/base.txt" import { toLLMMessages } from "./runner/to-llm-message" @@ -23,6 +24,7 @@ export const layer = Layer.effect( const hooks = yield* PluginHooks.Service const llm = yield* LLMClient.Service const models = yield* SessionRunnerModel.Service + const registry = yield* ToolRegistry.Service const app = yield* App.Metadata return SessionGenerate.Service.of({ @@ -34,6 +36,9 @@ export const layer = Layer.effect( const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(selection.session.id) ? selection.session.id.slice(4) : selection.session.id + const executableTools = yield* registry.materialize(selection.agent.info.permissions) + const toolDefinitions = executableTools.definitions + const toolsByName = new Map(toolDefinitions.map((tool) => [tool.name, tool])) const contextEvent = yield* hooks.trigger("session", "context", { sessionID: selection.session.id, agent: selection.agent.id, @@ -46,24 +51,34 @@ export const layer = Layer.effect( ...(history.instructionUpdate ? [Message.system(history.instructionUpdate)] : []), Message.user(input.prompt), ], - tools: {}, + tools: Object.fromEntries( + toolDefinitions.map((tool) => [tool.name, { description: tool.description, input: { ...tool.inputSchema } }]), + ), + }) + const hookedTools = Object.entries(contextEvent.tools).flatMap(([name, tool]) => { + const registered = toolsByName.get(name) + return registered + ? [Object.assign({}, registered, { description: tool.description, inputSchema: tool.input })] + : [] }) yield* Effect.logInfo("sending session generation request", { sessionID: selection.session.id, providerID: model.ref.providerID, modelID: model.ref.id, }) - return (yield* llm.generate( + const response = yield* llm.generate( LLM.request({ model: model.model, http: { headers: SessionModelHeaders.make(selection.session, app) }, providerOptions: { openai: { promptCacheKey } }, system: contextEvent.system, messages: contextEvent.messages, - tools: [], + tools: hookedTools, toolChoice: "none", }), - )).text + ) + yield* Effect.logInfo("session generation usage diagnostic", { usage: response.usage }) + return response.text }), }) }), @@ -72,5 +87,13 @@ export const layer = Layer.effect( export const node = makeLocationNode({ service: SessionGenerate.Service, layer, - deps: [SessionContext.node, Database.node, PluginHooks.node, SessionRunnerModel.node, App.node, llmClient], + deps: [ + SessionContext.node, + Database.node, + PluginHooks.node, + SessionRunnerModel.node, + ToolRegistry.node, + App.node, + llmClient, + ], }) diff --git a/packages/core/test/session-generate.test.ts b/packages/core/test/session-generate.test.ts index 879c4cb2b30c..3f0183167c4b 100644 --- a/packages/core/test/session-generate.test.ts +++ b/packages/core/test/session-generate.test.ts @@ -1,5 +1,5 @@ import { expect } from "bun:test" -import { LLMClient, LLMEvent, LLMResponse, Model, SystemPart, type LLMRequest } from "@opencode-ai/ai" +import { LLMClient, LLMEvent, LLMResponse, Model, SystemPart, ToolDefinition, type LLMRequest } from "@opencode-ai/ai" import { OpenAIChat } from "@opencode-ai/ai/protocols" import { AgentV2 } from "@opencode-ai/core/agent" import { Database } from "@opencode-ai/core/database/database" @@ -38,6 +38,7 @@ import { SessionStore } from "@opencode-ai/core/session/store" import { SkillInstructions } from "@opencode-ai/core/skill/instructions" import { PluginHooks } from "@opencode-ai/core/plugin/hooks" import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" +import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { asc, eq } from "drizzle-orm" import { Effect, Layer, Schema, Stream } from "effect" import { testEffect } from "./lib/effect" @@ -92,6 +93,15 @@ const skills = Layer.mock(SkillInstructions.Service, { load: () => Effect.succee const references = Layer.mock(ReferenceInstructions.Service, { load: () => Effect.succeed(Instructions.empty) }) const mcp = Layer.mock(McpInstructions.Service, { load: () => Effect.succeed(Instructions.empty) }) const plugins = Layer.mock(PluginSupervisor.Service, { flush: Effect.void }) +const tools = Layer.mock(ToolRegistry.Service, { + materialize: () => + Effect.succeed({ + definitions: [ToolDefinition.make({ name: "lookup", description: "Lookup", inputSchema: { type: "object" } })], + settle: () => Effect.die(new Error("unused")), + }), + register: () => Effect.die(new Error("unused")), + registerBatch: () => Effect.die(new Error("unused")), +}) const it = testEffect( AppNodeBuilder.build( @@ -114,6 +124,7 @@ const it = testEffect( [ReferenceInstructions.node, references], [McpInstructions.node, mcp], [PluginSupervisor.node, plugins], + [ToolRegistry.node, tools], [Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })], ], ), @@ -259,6 +270,7 @@ it.effect("generates from fresh settled Session context without durable mutation yield* hooks.register("session", "context", (event) => Effect.sync(() => { event.system = [SystemPart.make("Hooked system"), ...event.system] + if (event.tools.lookup) event.tools.lookup.description = "Hooked lookup" }), ) @@ -287,7 +299,7 @@ it.effect("generates from fresh settled Session context without durable mutation : [], ), ).toEqual(["Settled partial answer"]) - expect(requests[0]?.tools).toEqual([]) + expect(requests[0]?.tools).toMatchObject([{ name: "lookup", description: "Hooked lookup" }]) expect(requests[0]?.toolChoice).toMatchObject({ type: "none" }) expect(yield* durableState(db, sessionID)).toEqual(before) }), From 532292b5f32f4cdd8a020eeb465e0a5ae6d26a50 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:11:51 -0500 Subject: [PATCH 032/150] fix(core): reject malformed patch hunks (#38188) --- packages/core/test/patch.test.ts | 247 ++++++++++++++++++++++++++++++- packages/util/src/patch.ts | 221 ++++++++++++++++++++------- 2 files changed, 409 insertions(+), 59 deletions(-) diff --git a/packages/core/test/patch.test.ts b/packages/core/test/patch.test.ts index d3ff40b78697..18dd3695ce0b 100644 --- a/packages/core/test/patch.test.ts +++ b/packages/core/test/patch.test.ts @@ -22,6 +22,30 @@ describe("Patch", () => { ]) }) + test("parses an empty patch", () => { + expect(parse("*** Begin Patch\n*** End Patch")).toEqual([]) + }) + + test("ignores a Codex environment preamble", () => { + expect( + parse("*** Begin Patch\n*** Environment ID: remote\n*** Add File: file.txt\n+content\n*** End Patch"), + ).toEqual([{ type: "add", path: "file.txt", contents: "content" }]) + }) + + test("parses an update followed by an add", () => { + expect( + parse("*** Begin Patch\n*** Update File: update.txt\n@@\n+line\n*** Add File: add.txt\n+content\n*** End Patch"), + ).toEqual([ + { + type: "update", + path: "update.txt", + movePath: undefined, + chunks: [{ oldLines: [], newLines: ["line"], changeContext: undefined }], + }, + { type: "add", path: "add.txt", contents: "content" }, + ]) + }) + test("parses a file move", () => { expect( parse("*** Begin Patch\n*** Update File: old.txt\n*** Move to: new.txt\n@@\n-old\n+new\n*** End Patch"), @@ -66,6 +90,20 @@ describe("Patch", () => { ]) }) + test("strips quoted heredoc wrappers", () => { + const patch = "*** Begin Patch\n*** Add File: add.txt\n+added\n*** End Patch" + expect(parse(`<<'EOF'\n${patch}\nEOF`)).toEqual([{ type: "add", path: "add.txt", contents: "added" }]) + expect(parse(`<<\"EOF\"\n${patch}\nEOF`)).toEqual([{ type: "add", path: "add.txt", contents: "added" }]) + }) + + test("rejects malformed heredoc wrappers", () => { + const patch = "*** Begin Patch\n*** Add File: add.txt\n+added\n*** End Patch" + expect(() => parse(`<<\"EOF'\n${patch}\nEOF`)).toThrow("The first line of the patch must be '*** Begin Patch'") + expect(() => parse("< { expect(parse("*** Begin Patch\n *** Update File: foo.txt\n@@\n-old\n+new\n*** End Patch")).toEqual([ { @@ -99,6 +137,23 @@ describe("Patch", () => { ]) }) + test("parses relative and absolute hunk paths", () => { + expect( + parse( + "*** Begin Patch\n*** Add File: relative.txt\n+content\n*** Delete File: /tmp/delete.txt\n*** Update File: /tmp/update.txt\n@@\n-old\n+new\n*** End Patch", + ), + ).toEqual([ + { type: "add", path: "relative.txt", contents: "content" }, + { type: "delete", path: "/tmp/delete.txt" }, + { + type: "update", + path: "/tmp/update.txt", + movePath: undefined, + chunks: [{ oldLines: ["old"], newLines: ["new"], changeContext: undefined }], + }, + ]) + }) + test("strips one carriage return from CRLF patch lines", () => { expect(parse("*** Begin Patch\r\n*** Update File: file.txt\r\n@@\r\n-old\r\n+new\r\n*** End Patch\r\n")).toEqual([ { @@ -136,6 +191,42 @@ describe("Patch", () => { ]) }) + test("allows an end-of-file marker before an explicit chunk", () => { + expect( + parse("*** Begin Patch\n*** Update File: file.txt\n*** End of File\n@@\n-old\n+new\n*** End Patch"), + ).toEqual([ + { + type: "update", + path: "file.txt", + movePath: undefined, + chunks: [{ oldLines: ["old"], newLines: ["new"], changeContext: undefined }], + }, + ]) + }) + + test("allows an end-of-file marker before an implicit chunk and move", () => { + expect(parse("*** Begin Patch\n*** Update File: file.txt\n*** End of File\n-old\n+new\n*** End Patch")).toEqual([ + { + type: "update", + path: "file.txt", + movePath: undefined, + chunks: [{ oldLines: ["old"], newLines: ["new"] }], + }, + ]) + expect( + parse( + "*** Begin Patch\n*** Update File: old.txt\n*** End of File\n*** Move to: new.txt\n@@\n-old\n+new\n*** End Patch", + ), + ).toEqual([ + { + type: "update", + path: "old.txt", + movePath: "new.txt", + chunks: [{ oldLines: ["old"], newLines: ["new"], changeContext: undefined }], + }, + ]) + }) + test("derives fuzzy line updates while preserving BOM", () => { const update = Patch.derive("update.txt", [{ oldLines: [" old "], newLines: ["new"] }], "\uFEFFold\n") expect(update).toEqual({ content: "new\n", bom: true }) @@ -236,15 +327,157 @@ describe("Patch", () => { ).toThrow("Failed to find expected lines") }) - test("matches V1 lenient parsing of malformed hunk bodies", () => { - expect(parse("*** Begin Patch\n*** Add File: add.txt\nmissing plus\n*** End Patch")).toEqual([ - { type: "add", path: "add.txt", contents: "" }, + test("parses an update without an explicit first chunk header", () => { + expect(parse("*** Begin Patch\n*** Update File: file.txt\n import foo\n+bar\n*** End Patch")).toEqual([ + { + type: "update", + path: "file.txt", + movePath: undefined, + chunks: [{ oldLines: ["import foo"], newLines: ["import foo", "bar"] }], + }, + ]) + }) + + test("keeps indented update markers as context lines", () => { + expect( + parse( + "*** Begin Patch\n*** Update File: a.txt\n@@\n-old a\n+new a\n *** Update File: b.txt\n@@\n-old b\n+new b\n*** End Patch", + ), + ).toEqual([ + { + type: "update", + path: "a.txt", + movePath: undefined, + chunks: [ + { + oldLines: ["old a", "*** Update File: b.txt"], + newLines: ["new a", "*** Update File: b.txt"], + changeContext: undefined, + }, + { oldLines: ["old b"], newLines: ["new b"], changeContext: undefined }, + ], + }, + ]) + }) + + test("keeps indented move and EOF markers as context lines", () => { + expect( + parse( + "*** Begin Patch\n*** Update File: file.txt\n@@\n before\n *** Move to: moved.txt\n *** End of File\n*** End Patch", + ), + ).toEqual([ + { + type: "update", + path: "file.txt", + movePath: undefined, + chunks: [ + { + oldLines: ["before", "*** Move to: moved.txt", "*** End of File"], + newLines: ["before", "*** Move to: moved.txt", "*** End of File"], + changeContext: undefined, + }, + ], + }, ]) - expect(parse("*** Begin Patch\n*** Update File: update.txt\n*** End Patch")).toEqual([ - { type: "update", path: "update.txt", movePath: undefined, chunks: [] }, + }) + + test("preserves update context indentation", () => { + expect(parse("*** Begin Patch\n*** Update File: file.txt\n@@ section\n-old\n+new\n*** End Patch")).toEqual([ + { + type: "update", + path: "file.txt", + movePath: undefined, + chunks: [{ oldLines: ["old"], newLines: ["new"], changeContext: " section" }], + }, ]) - expect(parse("*** Begin Patch\n*** Delete File: delete.txt\nunexpected body\n*** End Patch")).toEqual([ - { type: "delete", path: "delete.txt" }, + }) + + test("preserves bare empty update lines as context", () => { + expect( + parse("*** Begin Patch\n*** Update File: file.txt\n@@\n context before\n\n context after\n*** End Patch"), + ).toEqual([ + { + type: "update", + path: "file.txt", + movePath: undefined, + chunks: [ + { + oldLines: ["context before", "", "context after"], + newLines: ["context before", "", "context after"], + changeContext: undefined, + }, + ], + }, ]) }) + + test("rejects invalid add and delete lines", () => { + expect(() => parse("*** Begin Patch\n*** Add File: file.txt\nbad\n*** End Patch")).toThrow( + "Invalid hunk at line 3: 'bad' is not a valid hunk header", + ) + expect(() => parse("*** Begin Patch\n*** Delete File: file.txt\nbad\n*** End Patch")).toThrow( + "Invalid hunk at line 3: 'bad' is not a valid hunk header", + ) + }) + + test("rejects an empty update hunk", () => { + expect(() => parse("*** Begin Patch\n*** Update File: file.txt\n*** End Patch")).toThrow( + "Invalid hunk at line 2: Update file hunk for path 'file.txt' is empty", + ) + expect(() => + parse( + "*** Begin Patch\n*** Update File: old.txt\n*** Move to: new.txt\n*** Delete File: other.txt\n*** End Patch", + ), + ).toThrow("Invalid hunk at line 2: Update file hunk for path 'old.txt' is empty") + expect(() => parse("*** Begin Patch\n*** Update File: file.txt\n*** End of File\n*** End Patch")).toThrow( + "Invalid hunk at line 2: Update file hunk for path 'file.txt' is empty", + ) + }) + + test("rejects an empty update chunk", () => { + expect(() => parse("*** Begin Patch\n*** Update File: file.txt\n@@\n*** End Patch")).toThrow( + "Invalid hunk at line 4: Update hunk does not contain any lines", + ) + expect(() => parse("*** Begin Patch\n*** Update File: file.txt\n@@\n*** End of File\n*** End Patch")).toThrow( + "Invalid hunk at line 4: Update hunk does not contain any lines", + ) + expect(() => parse("*** Begin Patch\n*** Update File: file.txt\n@@\n@@\n-old\n+new\n*** End Patch")).toThrow( + "Invalid hunk at line 4: Unexpected line found in update hunk: '@@'", + ) + expect(() => + parse("*** Begin Patch\n*** Update File: file.txt\n@@\n*** Update File: other.txt\n@@\n-old\n+new\n*** End Patch"), + ).toThrow("Invalid hunk at line 4: Unexpected line found in update hunk: '*** Update File: other.txt'") + expect(() => parse("*** Begin Patch\n*** Update File: file.txt\n@@\nbad\n*** End Patch")).toThrow( + "Invalid hunk at line 4: Unexpected line found in update hunk: 'bad'", + ) + }) + + test("rejects an invalid update line", () => { + expect(() => parse("*** Begin Patch\n*** Update File: file.txt\n@@\n-old\nbad\n*** End Patch")).toThrow( + "Invalid hunk at line 5: Expected update hunk to start with a @@ context marker, got: 'bad'", + ) + expect(() => parse("*** Begin Patch\n*** Update File: file.txt\n@@foo\n*** End Patch")).toThrow( + "Invalid hunk at line 3: Unexpected line found in update hunk: '@@foo'", + ) + expect(() => parse("*** Begin Patch\n*** Update File: file.txt\n@@\n-old\n*** Frobnicate File: foo\n*** End Patch")).toThrow( + "Invalid hunk at line 5: Expected update hunk to start with a @@ context marker, got: '*** Frobnicate File: foo'", + ) + }) + + test("rejects invalid and pathless hunk headers", () => { + expect(() => parse("*** Begin Patch\n*** Frobnicate File: foo\n*** End Patch")).toThrow( + "Invalid hunk at line 2: '*** Frobnicate File: foo' is not a valid hunk header", + ) + expect(() => parse("*** Begin Patch\n*** Add File:\n*** End Patch")).toThrow( + "Invalid hunk at line 2: '*** Add File:' is not a valid hunk header", + ) + for (const header of ["*** Add File: ", "*** Delete File: ", "*** Update File: "]) { + expect(() => parse(`*** Begin Patch\n${header}\n*** End Patch`)).toThrow( + `Invalid hunk at line 2: '${header.trim()}' is not a valid hunk header`, + ) + } + expect(() => + parse("*** Begin Patch\n*** Update File: old.txt\n*** Move to: \n@@\n-old\n+new\n*** End Patch"), + ).toThrow("Invalid hunk at line 3: '*** Move to:' is not a valid hunk header") + }) }) diff --git a/packages/util/src/patch.ts b/packages/util/src/patch.ts index d3aeb73fd912..843e14efa14f 100644 --- a/packages/util/src/patch.ts +++ b/packages/util/src/patch.ts @@ -13,8 +13,10 @@ export class BoundaryError extends Schema.TaggedErrorClass()("Pat export class InvalidHunkError extends Schema.TaggedErrorClass()("Patch.InvalidHunkError", { line: Schema.String, lineNumber: Schema.Number, + reason: Schema.optional(Schema.String), }) { override get message() { + if (this.reason) return `Invalid hunk at line ${this.lineNumber}: ${this.reason}` return `Invalid hunk at line ${this.lineNumber}: '${this.line}' is not a valid hunk header. Valid hunk headers: '*** Add File: {path}', '*** Delete File: {path}', '*** Update File: {path}'` } } @@ -57,51 +59,48 @@ export function parse(patchText: string): Result.Result, Par while (index < end) { const line = lines[index]! const header = line.trim() - if (header.startsWith("*** Add File:")) { - const path = header.slice("*** Add File:".length).trim() - if (!path) { - index++ - continue - } + if ( + index === begin + 1 && + header.startsWith("*** Environment ID:") && + header.slice("*** Environment ID:".length).trim() + ) { + index++ + continue + } + if (header.startsWith("*** Add File: ")) { + const path = header.slice("*** Add File: ".length).trim() const parsed = parseAdd(lines, index + 1, end) + if ("error" in parsed) return Result.fail(parsed.error) hunks.push({ type: "add", path, contents: parsed.content }) index = parsed.next continue } - if (header.startsWith("*** Delete File:")) { - const path = header.slice("*** Delete File:".length).trim() - if (!path) { - index++ - continue - } + if (header.startsWith("*** Delete File: ")) { + const path = header.slice("*** Delete File: ".length).trim() hunks.push({ type: "delete", path }) index++ continue } - if (header.startsWith("*** Update File:")) { - const path = header.slice("*** Update File:".length).trim() - if (!path) { - index++ - continue - } + if (header.startsWith("*** Update File: ")) { + const path = header.slice("*** Update File: ".length).trim() let next = index + 1 let movePath: string | undefined - if (lines[next]?.startsWith("*** Move to:")) { - movePath = lines[next]!.slice("*** Move to:".length).trim() + while (lines[next]?.trimEnd() === "*** End of File") next++ + const move = lines[next]?.trimEnd() + if (move === "*** Move to:" || move?.startsWith("*** Move to: ")) { + movePath = move.slice("*** Move to: ".length).trim() + if (!movePath) { + return Result.fail(new InvalidHunkError({ line: lines[next]!.trim(), lineNumber: next + 1 })) + } next++ } - const parsed = parseUpdate(lines, next, end) + const parsed = parseUpdate(lines, next, end, path, index) + if ("error" in parsed) return Result.fail(parsed.error) hunks.push({ type: "update", path, movePath, chunks: parsed.chunks }) index = parsed.next continue } - index++ - } - if (hunks.length === 0) { - const invalid = lines.findIndex((line, index) => index > begin && index < end && line.trim() !== "") - if (invalid !== -1) { - return Result.fail(new InvalidHunkError({ line: lines[invalid]!.trim(), lineNumber: invalid + 1 })) - } + return Result.fail(new InvalidHunkError({ line: header, lineNumber: index + 1 })) } return Result.succeed(hunks) } @@ -123,47 +122,166 @@ export function joinBom(text: string, bom: boolean) { return bom ? `\uFEFF${stripped}` : stripped } -function parseAdd(lines: ReadonlyArray, start: number, end: number) { +function parseAdd( + lines: ReadonlyArray, + start: number, + end: number, +): { content: string; next: number } | { error: InvalidHunkError } { const content: string[] = [] let index = start - while (index < end && !lines[index]!.startsWith("***")) { - if (lines[index]!.startsWith("+")) content.push(lines[index]!.slice(1)) + while (index < end && !isBoundary(lines[index]!.trim())) { + if (!lines[index]!.startsWith("+")) { + return { error: new InvalidHunkError({ line: lines[index]!.trim(), lineNumber: index + 1 }) } + } + content.push(lines[index]!.slice(1)) index++ } return { content: content.join("\n"), next: index } } -function parseUpdate(lines: ReadonlyArray, start: number, end: number) { - const chunks: UpdateFileChunk[] = [] +function parseUpdate( + lines: ReadonlyArray, + start: number, + end: number, + path: string, + hunk: number, +): { chunks: ReadonlyArray; next: number } | { error: InvalidHunkError } { + const chunks: Array<{ + oldLines: string[] + newLines: string[] + changeContext?: string + endOfFile?: boolean + }> = [] let index = start - while (index < end && !lines[index]!.startsWith("***")) { - if (!lines[index]!.startsWith("@@")) { + let afterEndOfFile = false + while (index < end) { + const line = lines[index]! + const updateLine = line.trimEnd() + if (afterEndOfFile) { + if (updateLine === "") { + index++ + continue + } + if (updateLine === "@@" || updateLine.startsWith("@@ ")) afterEndOfFile = false + else if (isBoundary(updateLine)) break + else { + return { + error: new InvalidHunkError({ + line, + lineNumber: index + 1, + reason: `Expected update hunk to start with a @@ context marker, got: '${line}'`, + }), + } + } + } + if (updateLine === "*** End of File") { + const chunk = chunks.at(-1) + if (chunk && chunk.oldLines.length === 0 && chunk.newLines.length === 0) { + return { + error: new InvalidHunkError({ + line: updateLine, + lineNumber: index + 1, + reason: "Update hunk does not contain any lines", + }), + } + } + if (chunk) { + chunk.endOfFile = true + afterEndOfFile = true + } index++ continue } - const changeContext = lines[index]!.slice(2).trim() || undefined - const oldLines: string[] = [] - const newLines: string[] = [] - let endOfFile = false - index++ - while (index < end && !lines[index]!.startsWith("@@") && !lines[index]!.startsWith("***")) { - const line = lines[index]! - if (line.startsWith(" ")) { - oldLines.push(line.slice(1)) - newLines.push(line.slice(1)) - } else if (line.startsWith("-")) oldLines.push(line.slice(1)) - else if (line.startsWith("+")) newLines.push(line.slice(1)) + if (isBoundary(updateLine)) break + if (updateLine === "@@" || updateLine.startsWith("@@ ")) { + const previous = chunks.at(-1) + if (previous && previous.oldLines.length === 0 && previous.newLines.length === 0) { + return { + error: new InvalidHunkError({ + line, + lineNumber: index + 1, + reason: `Unexpected line found in update hunk: '${line}'. Every line should start with ' ' (context line), '+' (added line), or '-' (removed line)`, + }), + } + } + chunks.push({ + oldLines: [], + newLines: [], + changeContext: updateLine === "@@" ? undefined : updateLine.slice("@@ ".length), + }) + index++ + continue + } + if (chunks.length === 0) chunks.push({ oldLines: [], newLines: [] }) + const chunk = chunks.at(-1)! + if (line === "") { + chunk.oldLines.push("") + chunk.newLines.push("") index++ + continue } - if (lines[index]?.trim() === "*** End of File") { - endOfFile = true + if (line.startsWith(" ")) { + chunk.oldLines.push(line.slice(1)) + chunk.newLines.push(line.slice(1)) index++ + continue + } + if (line.startsWith("-")) { + chunk.oldLines.push(line.slice(1)) + index++ + continue + } + if (line.startsWith("+")) { + chunk.newLines.push(line.slice(1)) + index++ + continue + } + const populated = chunk.oldLines.length > 0 || chunk.newLines.length > 0 + return { + error: new InvalidHunkError({ + line, + lineNumber: index + 1, + reason: populated + ? `Expected update hunk to start with a @@ context marker, got: '${line}'` + : `Unexpected line found in update hunk: '${line}'. Every line should start with ' ' (context line), '+' (added line), or '-' (removed line)`, + }), + } + } + if (chunks.length === 0) { + return { + error: new InvalidHunkError({ + line: lines[hunk]!.trim(), + lineNumber: hunk + 1, + reason: `Update file hunk for path '${path}' is empty`, + }), + } + } + const last = chunks.at(-1)! + if (last.oldLines.length === 0 && last.newLines.length === 0) { + const line = lines[index]!.trim() + return { + error: new InvalidHunkError({ + line, + lineNumber: index + 1, + reason: + line === "*** End Patch" + ? "Update hunk does not contain any lines" + : `Unexpected line found in update hunk: '${line}'. Every line should start with ' ' (context line), '+' (added line), or '-' (removed line)`, + }), } - chunks.push({ oldLines, newLines, changeContext, endOfFile: endOfFile || undefined }) } return { chunks, next: index } } +function isBoundary(line: string) { + return ( + line === "*** End Patch" || + line.startsWith("*** Add File: ") || + line.startsWith("*** Delete File: ") || + line.startsWith("*** Update File: ") + ) +} + function computeReplacements(lines: ReadonlyArray, path: string, chunks: ReadonlyArray) { const replacements: Array]> = [] let lineIndex = 0 @@ -231,5 +349,4 @@ const normalize = (value: string) => .replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " ") const splitBom = (text: string) => text.startsWith("\uFEFF") ? { bom: true, text: text.slice(1) } : { bom: false, text } -const stripHeredoc = (input: string) => - input.match(/^(?:cat\s+)?<<['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1\s*$/)?.[2] ?? input +const stripHeredoc = (input: string) => input.match(/^(?:cat\s+)?<<(['"]?)(\w+)\1\s*\n([\s\S]*?)\n\2\s*$/)?.[3] ?? input From 8a36abd32819a86f697ef02f461aafad11595500 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 22 Jul 2026 14:35:28 -0400 Subject: [PATCH 033/150] feat(tui): add developer debug bar (#38359) --- packages/tui/src/app.tsx | 31 +- packages/tui/src/component/devtools-bar.tsx | 533 ++++++++++++++++++ .../tui/src/component/devtools-sidebar.tsx | 65 --- packages/tui/src/component/dialog-config.tsx | 6 +- packages/tui/src/config/index.tsx | 2 +- packages/tui/src/context/theme.tsx | 24 +- packages/tui/src/routes/session/index.tsx | 17 - packages/tui/src/ui/dialog-export-options.tsx | 50 +- packages/tui/test/devtools.test.ts | 19 - 9 files changed, 554 insertions(+), 193 deletions(-) create mode 100644 packages/tui/src/component/devtools-bar.tsx delete mode 100644 packages/tui/src/component/devtools-sidebar.tsx delete mode 100644 packages/tui/test/devtools.test.ts diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 704332977955..5e600b65e845 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -1,4 +1,4 @@ -import { render, TimeToFirstDraw, useRenderer, useTerminalDimensions } from "@opentui/solid" +import { render, useRenderer, useTerminalDimensions } from "@opentui/solid" import { registerOpencodeSpinner } from "./component/register-spinner" import { Deferred, Effect } from "effect" import { Service, type Endpoint } from "@opencode-ai/client/effect/service" @@ -36,6 +36,7 @@ import { TuiPathsProvider, TuiStartupProvider, TuiTerminalEnvironmentProvider, + useTuiApp, useTuiStartup, type TuiApp, } from "./context/runtime" @@ -47,8 +48,7 @@ import { EditorContextProvider } from "./context/editor" import { useEvent } from "./context/event" import { ClientProvider, useClient } from "./context/client" import { StartupLoading } from "./component/startup-loading" -import { DevToolsSidebar } from "./component/devtools-sidebar" -import { DevTools } from "./devtools" +import { DevToolsBar } from "./component/devtools-bar" import { Reconnecting } from "./component/reconnecting" import { DataProvider, useData } from "./context/data" import { LocationProvider, useLocation } from "./context/location" @@ -88,8 +88,6 @@ import { win32DisableProcessedInput, win32FlushInputBuffer } from "./terminal-wi import { destroyRenderer } from "./util/renderer" import { cliErrorMessage, errorFormat } from "./util/error" -const themePerformance = DevTools.register({ id: "theme-performance", title: "Theme performance" }) - registerOpencodeSpinner() const appGlobalBindingCommands = [ @@ -257,12 +255,9 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { const pluginRuntime = createPluginRuntime() yield* Effect.tryPromise(async () => { - const appStarted = performance.now() // Prewarm palette before ThemeProvider mounts so `system` theme avoids a first-paint fallback flash. void renderer.getPalette({ size: 16 }).catch(() => undefined) - const modeStarted = performance.now() const mode = handoff?.mode ?? (await renderer.waitForThemeMode(1000)) ?? "dark" - themePerformance.set("Detect light/dark mode", `${(performance.now() - modeStarted).toFixed(2)} ms`) if (renderer.isDestroyed) return await render(() => { @@ -351,7 +346,6 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { config.data.debug?.devtools ?? false) + const devtools = createMemo(() => config.data.debug?.devtools ?? app.channel === "local") const route = useRoute() const dimensions = useTerminalDimensions() const renderer = useRenderer() @@ -433,11 +428,6 @@ function App(props: { pair?: DialogPairCredentials; started: number }) { const plugins = usePlugin() const clipboard = useClipboard() - createEffect(() => { - if (!themeState.ready) return - themePerformance.set("Total", `${(performance.now() - props.started).toFixed(2)} ms`) - }) - // Toast once when an MCP server enters a failed or needs-auth state so the user knows to act, // without having to open the status panel. Tracking the last alerted status avoids re-toasting // the same problem on every refresh while still re-alerting if the state changes. @@ -1110,9 +1100,6 @@ function App(props: { pair?: DialogPairCredentials; started: number }) { : undefined } > - - - @@ -1141,10 +1128,10 @@ function App(props: { pair?: DialogPairCredentials; started: number }) { - - - + + + diff --git a/packages/tui/src/component/devtools-bar.tsx b/packages/tui/src/component/devtools-bar.tsx new file mode 100644 index 000000000000..fc168260a203 --- /dev/null +++ b/packages/tui/src/component/devtools-bar.tsx @@ -0,0 +1,533 @@ +import { TextAttributes } from "@opentui/core" +import { TimeToFirstDraw, useRenderer, useTerminalDimensions } from "@opentui/solid" +import { open } from "node:fs/promises" +import { tmpdir } from "node:os" +import path from "node:path" +import { monitorEventLoopDelay } from "node:perf_hooks" +import { createMemo, createResource, createSignal, For, onCleanup, onMount, Show, type ParentProps } from "solid-js" +import { useClient } from "../context/client" +import { useConfig } from "../config" +import { useData } from "../context/data" +import { useLocation } from "../context/location" +import { useRoute } from "../context/route" +import { Keymap } from "../context/keymap" +import { useTheme } from "../context/theme" +import { DevTools } from "../devtools" +import { usePlugin } from "../plugin/context" +import { errorMessage } from "../util/error" + +const graphWidth = 23 +const sampleIntervalMilliseconds = 2_000 +const sampleRetentionMilliseconds = 30_000 +const statusWindowMilliseconds = 6_000 +type Panel = "server" | "theme" | "tools" | "ui" +type ProcessSample = Readonly<{ cpu: number; memory: number; delay: number; time: number }> +export type RuntimeStatus = "normal" | "medium" | "high" + +export function DevToolsBar() { + const client = useClient() + const config = useConfig() + const data = useData() + const location = useLocation() + const route = useRoute() + const plugins = usePlugin() + const theme = useTheme() + const keymap = Keymap.use() + const dimensions = useTerminalDimensions() + const { themeV2, mode, supports, setMode } = theme + const elevatedTheme = theme.contextual("elevated").themeV2 + const [panel, setPanel] = createSignal() + const [dumping, setDumping] = createSignal(false) + const [dumpPath, setDumpPath] = createSignal() + const [dumpError, setDumpError] = createSignal() + const [frontendSamples, setFrontendSamples] = createSignal([]) + const connected = createMemo(() => client.connection.status() === "connected") + const serverIndicator = createMemo(() => connectionIndicator(client.connection.status(), client.connection.attempt())) + const themePerformance = createMemo( + () => DevTools.data().find((group) => group.id === "theme-performance")?.entries ?? [], + ) + const groups = createMemo(() => DevTools.data().filter((group) => group.id !== "theme-performance")) + const [server] = createResource(connected, async () => { + const [health, info] = await Promise.all([client.api.health.get(), client.api.server.get()]) + return { + health, + address: info.urls[0] ? new URL(info.urls[0]).host : "Unknown", + } + }) + const toggle = (next: Panel) => setPanel((current) => (current === next ? undefined : next)) + const nextMode = () => (mode() === "dark" ? "light" : "dark") + const canSwitchMode = () => supports(nextMode()) + const runtime = createMemo(() => runtimeStatus(frontendSamples())) + const timing = () => config.data.debug?.timing ?? false + + const offEscape = keymap.intercept( + "key", + ({ event }) => { + if (!panel() || event.name !== "escape") return + event.preventDefault() + event.stopPropagation() + setPanel() + }, + { priority: 10 }, + ) + onCleanup(offEscape) + + onMount(() => { + const eventLoop = monitorEventLoopDelay({ resolution: 20 }) + let frontendCPU = process.cpuUsage() + let frontendTime = performance.now() + let frontendReady = false + eventLoop.enable() + const sample = () => { + const now = performance.now() + const cpu = process.cpuUsage(frontendCPU) + frontendCPU = process.cpuUsage() + setFrontendSamples((samples) => + [ + ...samples, + { + cpu: frontendReady ? cpuPercent(cpu.user + cpu.system, now - frontendTime) : 0, + memory: process.memoryUsage().rss, + delay: eventLoop.percentile(99) / 1_000_000, + time: now, + }, + ].filter((sample) => sample.time >= now - sampleRetentionMilliseconds), + ) + eventLoop.reset() + frontendReady = true + frontendTime = now + } + sample() + const timer = setInterval(sample, sampleIntervalMilliseconds) + onCleanup(() => { + clearInterval(timer) + eventLoop.disable() + }) + }) + + async function dump() { + setDumping(true) + setDumpPath() + setDumpError() + const routeData = route.data + const sessionID = routeData.type === "session" ? routeData.sessionID : undefined + const info = sessionID ? data.session.get(sessionID) : undefined + const sessionLocation = + info?.location ?? + (location.current + ? { directory: location.current.directory, workspaceID: location.current.workspaceID } + : undefined) + const details = server() + const backend = { + connected: connected(), + version: details?.health.version, + pid: details?.health.pid, + error: client.connection.error(), + } + const events = await (sessionID + ? (async () => { + const events: { readonly created: number }[] = [] + for await (const event of client.api.session.log({ sessionID, follow: false })) { + if (event.type !== "log.synced") events.push(event) + } + // Durable events stay in aggregate order even when their wall-clock timestamps differ. + client.connection.internal.history().forEach((event) => { + const index = events.findIndex((item) => item.created > event.created) + if (index === -1) { + events.push(event) + return + } + events.splice(index, 0, event) + }) + return events.slice(-100) + })().catch(() => client.connection.internal.history()) + : Promise.resolve([])) + const file = path.join(tmpdir(), `opencode-debug-${crypto.randomUUID()}.json`) + const output = + JSON.stringify( + { + backend, + session: sessionID + ? { + ...info, + id: sessionID, + projectID: info?.projectID ?? location.current?.project.id, + location: sessionLocation, + status: data.session.status(sessionID), + pending: data.session.pending.list(sessionID), + inputIDs: data.session.input.list(sessionID), + permissions: data.session.permission.list(sessionID) ?? [], + forms: data.session.form.list(sessionID) ?? [], + } + : undefined, + events, + mcp: { + servers: sessionLocation + ? (data.location.mcp.server.list(sessionLocation) ?? []) + : (data.location.mcp.server.list() ?? []), + resources: sessionLocation + ? (data.location.mcp.resource.list(sessionLocation) ?? []) + : (data.location.mcp.resource.list() ?? []), + }, + plugins: { + ready: plugins.ready(), + list: plugins.list().map((plugin) => ({ + name: "id" in plugin ? plugin.id : plugin.target, + ...plugin, + })), + }, + theme: { + name: theme.selected, + mode: theme.mode(), + }, + }, + null, + 2, + ) + "\n" + await open(file, "wx", 0o600) + .then((handle) => handle.writeFile(output).finally(() => handle.close())) + .then( + () => setDumpPath(file), + (error) => setDumpError(errorMessage(error)), + ) + setDumping(false) + } + + return ( + + + setPanel()} + /> + + toggle("server")}> + + {serverIndicator().icon} + + + {" "} + Server + + + + Server + + 0}> + + + {(error) => } + + {(value) => ( + <> + + + + + )} + + + Server details unavailable + + + + + toggle("ui")}> + + {statusIcon(runtime())} + + + {" "} + UI + + + + UI + + sample.delay)} + unit=" ms" + decimals={1} + /> + sample.cpu)} unit="%" /> + sample.memory / 1024 / 1024)} + unit=" MB" + decimals={0} + /> + + + + toggle("theme")}> + Theme + + + Theme + + + {(entry) => } + + setMode(nextMode())} hoverBackground> + Switch to {nextMode()} + + + + + + toggle("tools")}> + Tools + + + Tools + void dump()} disabled={dumping()} hoverBackground> + {dumping() ? "Writing debug snapshot..." : "Write debug snapshot"} + + + {(file) => ( + + {file()} + + )} + + + {(error) => ( + + {error()} + + )} + + + + Render + + + void config.update((draft) => { + draft.debug = { ...draft.debug, timing: !timing() } + }) + } + hoverBackground + > + {timing() ? "[x]" : "[ ]"} Time to first draw + + + + {(group) => ( + + + {group.title} + + {(entry) => } + + )} + + + + + + + + + ) +} + +function BarItem(props: ParentProps<{ active: boolean; onClick: () => void }>) { + const { themeV2 } = useTheme() + const renderer = useRenderer() + return ( + { + if (renderer.getSelection()?.getSelectedText()) return + props.onClick() + }} + > + {props.children} + + ) +} + +function PanelBox(props: ParentProps) { + const { themeV2 } = useTheme().contextual("elevated") + const renderer = useRenderer() + return ( + { + if (renderer.getSelection()?.getSelectedText()) return + event.stopPropagation() + }} + > + {props.children} + + ) +} + +function PanelTitle(props: ParentProps) { + const { themeV2 } = useTheme().contextual("elevated") + return ( + + {props.children} + + ) +} + +function Row(props: { label: string; value: string }) { + const { themeV2 } = useTheme().contextual("elevated") + return ( + + {props.label} + + {props.value} + + ) +} + +function Action(props: ParentProps<{ onClick: () => void; disabled?: boolean; hoverBackground?: boolean }>) { + const { themeV2 } = useTheme().contextual("elevated") + const [hovered, setHovered] = createSignal(false) + return ( + setHovered(true)} + onMouseOut={() => setHovered(false)} + onMouseUp={(event) => { + event.stopPropagation() + if (!props.disabled) props.onClick() + }} + > + {props.children} + + ) +} + +function cpuPercent(microseconds: number, milliseconds: number) { + if (milliseconds <= 0) return 0 + return Math.max(0, microseconds / (milliseconds * 10)) +} + +function ProcessStat(props: { label: string; values: readonly number[]; unit: string; decimals?: number }) { + const { themeV2 } = useTheme().contextual("elevated") + const value = () => { + const value = props.values.at(-1) + if (value === undefined) return "--" + return `${value.toFixed(props.decimals ?? 1)}${props.unit}` + } + return ( + + + {props.label} + + + {brailleGraph(props.values)} + + + {value()} + + + ) +} + +export function runtimeStatus(samples: readonly Readonly<{ delay: number; time: number }>[]): RuntimeStatus { + const latest = samples.at(-1)?.time + if (latest === undefined) return "normal" + const delay = Math.max( + 0, + ...samples.filter((sample) => sample.time > latest - statusWindowMilliseconds).map((sample) => sample.delay), + ) + if (delay >= 100) return "high" + if (delay >= 20) return "medium" + return "normal" +} + +export function statusIcon(status: RuntimeStatus) { + if (status === "high") return "●" + if (status === "medium") return "⦿" + return "○" +} + +export function connectionIndicator(status: "connected" | "connecting" | "reconnecting", attempt: number) { + if (status === "connected") return { state: "connected" as const, icon: "✓" } + if (status === "reconnecting" && attempt >= 3) return { state: "disconnected" as const, icon: "×" } + return { state: "reconnecting" as const, icon: "↻" } +} + +export function brailleGraph(values: readonly number[], width = graphWidth) { + const min = Math.min(...values) + const range = Math.max(...values) - min + const points = [...Array(width * 2 - values.length).fill(values.at(0)), ...values].slice( + -width * 2, + ) + const dots = [ + [6, 2, 1, 0], + [7, 5, 4, 3], + ] + return Array.from({ length: width }, (_, index) => { + const bits = [points[index * 2], points[index * 2 + 1]].reduce((result, value, column) => { + if (value === undefined) return result + const height = 1 + Math.round((range === 0 ? 0 : (value - min) / range) * 3) + return dots[column].slice(0, height).reduce((bits, dot) => bits | (1 << dot), result) + }, 0) + return String.fromCodePoint(0x2800 + bits) + }).join("") +} diff --git a/packages/tui/src/component/devtools-sidebar.tsx b/packages/tui/src/component/devtools-sidebar.tsx deleted file mode 100644 index cca19652329c..000000000000 --- a/packages/tui/src/component/devtools-sidebar.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { TextAttributes } from "@opentui/core" -import { createSignal, For } from "solid-js" -import { useTheme } from "../context/theme" -import { DevTools } from "../devtools" - -export function DevToolsSidebar() { - const { themeV2, mode, supports, setMode } = useTheme().contextual("elevated") - const [modeHovered, setModeHovered] = createSignal(false) - const nextMode = () => (mode() === "dark" ? "light" : "dark") - const canSwitchMode = () => supports(nextMode()) - - return ( - - - - - Theme - - - - Mode - - setModeHovered(canSwitchMode())} - onMouseOut={() => setModeHovered(false)} - onMouseUp={canSwitchMode() ? () => setMode(nextMode()) : undefined} - > - {mode()} - - - - - {(group) => ( - - - - {group.title} - - - - {(entry) => ( - - {entry.key} - - {String(entry.value)} - - )} - - - )} - - - ) -} diff --git a/packages/tui/src/component/dialog-config.tsx b/packages/tui/src/component/dialog-config.tsx index bdacb53b457b..ab84fdf6233f 100644 --- a/packages/tui/src/component/dialog-config.tsx +++ b/packages/tui/src/component/dialog-config.tsx @@ -35,7 +35,7 @@ const settings: Setting[] = [ title: "Animations", category: "Appearance", path: ["animations"], - default: true, + default: false, values: [false, true], labels: ["off", "on"], }, @@ -223,10 +223,10 @@ const settings: Setting[] = [ labels: ["off", "on"], }, { - title: "Timing", + title: "DevTools: Timing", category: "Debug", path: ["debug", "timing"], - default: false, + default: true, values: [false, true], labels: ["off", "on"], }, diff --git a/packages/tui/src/config/index.tsx b/packages/tui/src/config/index.tsx index 6f5b78185e11..3d668183900f 100644 --- a/packages/tui/src/config/index.tsx +++ b/packages/tui/src/config/index.tsx @@ -151,7 +151,7 @@ export const Info = Schema.Struct({ ).annotate({ description: "In-product guidance settings" }), debug: Schema.optional( Schema.Struct({ - devtools: Schema.optional(Schema.Boolean).annotate({ description: "Show the DevTools sidebar" }), + devtools: Schema.optional(Schema.Boolean).annotate({ description: "Show the DevTools debug bar" }), timing: Schema.optional(Schema.Boolean).annotate({ description: "Show time-to-first-draw diagnostics" }), }), ).annotate({ description: "Debugging settings" }), diff --git a/packages/tui/src/context/theme.tsx b/packages/tui/src/context/theme.tsx index a29c25501cf0..88f91cb50de2 100644 --- a/packages/tui/src/context/theme.tsx +++ b/packages/tui/src/context/theme.tsx @@ -162,7 +162,6 @@ const themeContext = createSimpleContext({ let systemThemeMode: "dark" | "light" | undefined let hasResolvedSystemTheme = false function resolveSystemTheme(mode: "dark" | "light" = store.mode) { - const started = performance.now() return renderer .getPalette({ size: 16 }) .then((colors: TerminalColors) => { @@ -186,7 +185,6 @@ const themeContext = createSimpleContext({ setSystemTheme(undefined) if (store.active === "system") setStore("active", "opencode") }) - .finally(() => themePerformance.set("Resolve system palette", duration(performance.now() - started))) } let systemRefreshRunning = false @@ -272,14 +270,10 @@ const themeContext = createSimpleContext({ themeRefreshTimeouts.length = 0 }) + const initStarted = performance.now() const source = createMemo(() => store.themes[store.active] ?? store.themes.opencode) const sourceName = createMemo(() => (store.themes[store.active] ? store.active : "opencode")) - const file = createMemo(() => { - const started = performance.now() - const result = migrateV1(source()) - themePerformance.set("Convert V1 to V2", duration(performance.now() - started)) - return result - }) + const file = createMemo(() => migrateV1(source())) const modes = createMemo(() => themeModes(file())) const mode = () => { const supported = modes() @@ -287,12 +281,9 @@ const themeContext = createSimpleContext({ return supported[0] ?? store.mode } const values = createMemo(() => resolveTheme(source(), mode())) - const valuesV2 = createMemo(() => { - const resolveStarted = performance.now() - const result = resolveThemeFile(file(), mode(), sourceName()) - themePerformance.set("Resolve final theme", duration(performance.now() - resolveStarted)) - return result - }) + const valuesV2 = createMemo(() => resolveThemeFile(file(), mode(), sourceName())) + valuesV2() + themePerformance.set("Init", `${(performance.now() - initStarted).toFixed(2)} ms`) const themeV2 = createComponentTheme(valuesV2, mode) const contextsV2 = { elevated: createComponentTheme(() => { @@ -374,11 +365,6 @@ export function ThemeContextProvider(props: ParentProps<{ context: ContextName } ) } - -function duration(milliseconds: number) { - return `${milliseconds.toFixed(2)} ms` -} - export function createSyntaxStyleMemo(factory: () => SyntaxStyle) { const renderer = useRenderer() const retained = new Set() diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index ca776bb22815..69c9682ec158 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -762,23 +762,6 @@ export function Session() { options.format === "markdown" ? formatSessionTranscript(sessionData, messages(), options.thinking) : await (async () => { - if (options.debug) { - const events: { readonly created: number }[] = [] - for await (const event of client.api.session.log({ sessionID: sessionData.id, follow: false })) { - if (event.type !== "log.synced") events.push(event) - } - // Durable events stay in aggregate order even when their wall-clock timestamps differ. - client.connection.internal.history().forEach((event) => { - const index = events.findIndex((item) => item.created > event.created) - if (index === -1) { - events.push(event) - return - } - events.splice(index, 0, event) - }) - return JSON.stringify({ info: sessionData, events }, null, 2) + EOL - } - const messages: unknown[] = [] let cursor: string | undefined do { diff --git a/packages/tui/src/ui/dialog-export-options.tsx b/packages/tui/src/ui/dialog-export-options.tsx index 104aa04be2d8..13d67d580866 100644 --- a/packages/tui/src/ui/dialog-export-options.tsx +++ b/packages/tui/src/ui/dialog-export-options.tsx @@ -9,11 +9,11 @@ export type ExportFormat = "markdown" | "json" export type DialogExportOptionsProps = { defaultThinking: boolean - onConfirm?: (options: { action: "copy" | "export"; format: ExportFormat; debug: boolean; thinking: boolean }) => void + onConfirm?: (options: { action: "copy" | "export"; format: ExportFormat; thinking: boolean }) => void onCancel?: () => void } -type Active = ExportFormat | "debug" | "thinking" | "copy" | "export" +type Active = ExportFormat | "thinking" | "copy" | "export" export function DialogExportOptions(props: DialogExportOptionsProps) { const dialog = useDialog() @@ -21,7 +21,6 @@ export function DialogExportOptions(props: DialogExportOptionsProps) { const { themeV2: overlayTheme } = useTheme().contextual("overlay") const [store, setStore] = createStore({ format: "markdown" as ExportFormat, - debug: false, thinking: props.defaultThinking, active: "markdown" as Active, }) @@ -30,7 +29,6 @@ export function DialogExportOptions(props: DialogExportOptionsProps) { props.onConfirm?.({ action, format: store.format, - debug: store.debug, thinking: store.thinking, }) @@ -39,7 +37,6 @@ export function DialogExportOptions(props: DialogExportOptionsProps) { setStore("format", store.active) return } - if (store.active === "debug") setStore("debug", !store.debug) if (store.active === "thinking") setStore("thinking", !store.thinking) if (store.active === "copy" || store.active === "export") confirm(store.active) } @@ -55,7 +52,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) { const order: Active[] = store.format === "markdown" ? ["markdown", "json", "thinking", "copy", "export"] - : ["markdown", "json", "debug", "copy", "export"] + : ["markdown", "json", "copy", "export"] setStore("active", order[(order.indexOf(store.active) + 1) % order.length]) }, }, @@ -156,46 +153,6 @@ export function DialogExportOptions(props: DialogExportOptionsProps) { - - { - setStore("active", "debug") - setStore("debug", !store.debug) - }} - > - - {store.debug ? "[x]" : "[ ]"} - - - Events (debug) - - - return new Promise<{ action: "copy" | "export" format: ExportFormat - debug: boolean thinking: boolean } | null>((resolve) => { dialog.replace( diff --git a/packages/tui/test/devtools.test.ts b/packages/tui/test/devtools.test.ts deleted file mode 100644 index 5997c8aa4e97..000000000000 --- a/packages/tui/test/devtools.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { expect, test } from "bun:test" -import { DevTools } from "../src/devtools" - -test("registers and updates grouped DevTools data", () => { - const group = DevTools.register({ id: "test", title: "Test data" }) - - group.set("Duration", "1.00 ms") - group.set("Duration", "2.00 ms") - group.set("Count", 2) - - expect(DevTools.data().find((item) => item.id === "test")).toEqual({ - id: "test", - title: "Test data", - entries: [ - { key: "Duration", value: "2.00 ms" }, - { key: "Count", value: 2 }, - ], - }) -}) From 2271f9b222434972700559cd903bc79c4e08d83f Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:42:29 -0500 Subject: [PATCH 034/150] feat(ai): support PDF inputs (#38253) --- .../ai/src/protocols/anthropic-messages.ts | 65 +++--- packages/ai/src/protocols/bedrock-converse.ts | 3 +- packages/ai/src/protocols/gemini.ts | 33 ++- packages/ai/src/protocols/openai-responses.ts | 81 +++++-- packages/ai/src/protocols/shared.ts | 3 +- .../ai/src/protocols/utils/bedrock-media.ts | 8 +- packages/ai/src/tool-runtime.ts | 25 ++- .../recordings/pdf/anthropic-tool-result.json | 35 +++ .../recordings/pdf/anthropic-user-input.json | 34 +++ .../recordings/pdf/bedrock-tool-result.json | 36 +++ .../recordings/pdf/bedrock-user-input.json | 35 +++ .../recordings/pdf/gemini-tool-result.json | 53 +++++ .../recordings/pdf/gemini-user-input.json | 34 +++ .../recordings/pdf/openai-tool-result.json | 35 +++ .../recordings/pdf/openai-user-input.json | 34 +++ .../recordings/pdf/xai-tool-result.json | 35 +++ .../recordings/pdf/xai-user-input.json | 34 +++ packages/ai/test/lib/tool-runtime.ts | 7 +- .../test/provider/anthropic-messages.test.ts | 14 +- .../ai/test/provider/bedrock-converse.test.ts | 99 ++++++++- packages/ai/test/provider/gemini.test.ts | 75 ++++++- .../ai/test/provider/openai-responses.test.ts | 129 ++++++++++- .../ai/test/provider/pdf.recorded.test.ts | 207 ++++++++++++++++++ packages/ai/test/tool-runtime.test.ts | 45 ++++ 24 files changed, 1063 insertions(+), 96 deletions(-) create mode 100644 packages/ai/test/fixtures/recordings/pdf/anthropic-tool-result.json create mode 100644 packages/ai/test/fixtures/recordings/pdf/anthropic-user-input.json create mode 100644 packages/ai/test/fixtures/recordings/pdf/bedrock-tool-result.json create mode 100644 packages/ai/test/fixtures/recordings/pdf/bedrock-user-input.json create mode 100644 packages/ai/test/fixtures/recordings/pdf/gemini-tool-result.json create mode 100644 packages/ai/test/fixtures/recordings/pdf/gemini-user-input.json create mode 100644 packages/ai/test/fixtures/recordings/pdf/openai-tool-result.json create mode 100644 packages/ai/test/fixtures/recordings/pdf/openai-user-input.json create mode 100644 packages/ai/test/fixtures/recordings/pdf/xai-tool-result.json create mode 100644 packages/ai/test/fixtures/recordings/pdf/xai-user-input.json create mode 100644 packages/ai/test/provider/pdf.recorded.test.ts diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index 26d127eb0e64..f5b9ea67d9f7 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -27,6 +27,7 @@ import { ToolSchemaProjection } from "./utils/tool-schema" import { ToolStream } from "./utils/tool-stream" const ADAPTER = "anthropic-messages" +const MEDIA_MIMES = new Set([...ProviderShared.IMAGE_MIMES, ...ProviderShared.PDF_MIMES]) export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1" export const PATH = "/messages" @@ -56,6 +57,17 @@ const AnthropicImageBlock = Schema.Struct({ }) type AnthropicImageBlock = Schema.Schema.Type +const AnthropicDocumentBlock = Schema.Struct({ + type: Schema.tag("document"), + source: Schema.Struct({ + type: Schema.tag("base64"), + media_type: Schema.Literal("application/pdf"), + data: Schema.String, + }), + cache_control: Schema.optional(AnthropicCacheControl), +}) +type AnthropicDocumentBlock = Schema.Schema.Type + const AnthropicThinkingBlock = Schema.Struct({ type: Schema.tag("thinking"), thinking: Schema.String, @@ -101,13 +113,10 @@ const AnthropicServerToolResultBlock = Schema.Struct({ }) type AnthropicServerToolResultBlock = Schema.Schema.Type -// Anthropic accepts either a plain string or an ordered array of text/image -// blocks inside `tool_result.content`. The array form is required when a tool -// returns image bytes (screenshot, image search, etc.) so they can be passed -// to the model as proper image inputs instead of being JSON-stringified into -// the prompt — which silently inflates context by megabytes and can push the -// conversation over the model's token limit. -const AnthropicToolResultContent = Schema.Union([AnthropicTextBlock, AnthropicImageBlock]) +// Anthropic accepts either a plain string or an ordered array of text, image, and +// document blocks inside `tool_result.content`. The array form keeps media as native +// model input instead of JSON-stringifying base64 into prompt text. +const AnthropicToolResultContent = Schema.Union([AnthropicTextBlock, AnthropicImageBlock, AnthropicDocumentBlock]) const AnthropicToolResultBlock = Schema.Struct({ type: Schema.tag("tool_result"), @@ -117,7 +126,12 @@ const AnthropicToolResultBlock = Schema.Struct({ cache_control: Schema.optional(AnthropicCacheControl), }) -const AnthropicUserBlock = Schema.Union([AnthropicTextBlock, AnthropicImageBlock, AnthropicToolResultBlock]) +const AnthropicUserBlock = Schema.Union([ + AnthropicTextBlock, + AnthropicImageBlock, + AnthropicDocumentBlock, + AnthropicToolResultBlock, +]) type AnthropicUserBlock = Schema.Schema.Type const AnthropicAssistantBlock = Schema.Union([ AnthropicTextBlock, @@ -319,12 +333,17 @@ const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult return { type: wireType, tool_use_id: part.id, content: part.result.value } satisfies AnthropicServerToolResultBlock }) -const lowerImage = Effect.fn("AnthropicMessages.lowerImage")(function* (part: MediaPart) { - const media = yield* ProviderShared.validateMedia( - "Anthropic Messages", - part, - new Set(ProviderShared.IMAGE_MIMES), - ) +const lowerMedia = Effect.fn("AnthropicMessages.lowerMedia")(function* (part: MediaPart) { + const media = yield* ProviderShared.validateMedia("Anthropic Messages", part, MEDIA_MIMES) + if (media.mime === "application/pdf") + return { + type: "document" as const, + source: { + type: "base64" as const, + media_type: "application/pdf" as const, + data: media.base64, + }, + } satisfies AnthropicDocumentBlock return { type: "image" as const, source: { @@ -335,25 +354,13 @@ const lowerImage = Effect.fn("AnthropicMessages.lowerImage")(function* (part: Me } satisfies AnthropicImageBlock }) -// Tool results may carry structured text/images. Keep media as provider-native +// Tool results may carry structured text, images, and documents. Keep media as provider-native // content instead of JSON-stringifying base64 into a prompt string. const lowerToolResultContentItem = Effect.fn("AnthropicMessages.lowerToolResultContentItem")(function* ( item: ToolContent, ) { if (item.type === "text") return { type: "text" as const, text: item.text } satisfies AnthropicTextBlock - const media = yield* ProviderShared.validateToolFile( - "Anthropic Messages", - item, - new Set(ProviderShared.IMAGE_MIMES), - ) - return { - type: "image" as const, - source: { - type: "base64" as const, - media_type: media.mime, - data: media.base64, - }, - } satisfies AnthropicImageBlock + return yield* lowerMedia({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name }) }) const lowerToolResultContent = Effect.fn("AnthropicMessages.lowerToolResultContent")(function* (part: ToolResultPart) { @@ -445,7 +452,7 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* ( continue } if (part.type === "media") { - content.push(yield* lowerImage(part)) + content.push(yield* lowerMedia(part)) continue } return yield* ProviderShared.unsupportedContent("Anthropic Messages", "user", ["text", "media"]) diff --git a/packages/ai/src/protocols/bedrock-converse.ts b/packages/ai/src/protocols/bedrock-converse.ts index b94f23d62f73..801fb8a98a27 100644 --- a/packages/ai/src/protocols/bedrock-converse.ts +++ b/packages/ai/src/protocols/bedrock-converse.ts @@ -52,6 +52,7 @@ const BedrockToolResultContentItem = Schema.Union([ Schema.Struct({ text: Schema.String }), Schema.Struct({ json: Schema.Unknown }), BedrockMedia.ImageBlock, + BedrockMedia.DocumentBlock, ]) const BedrockToolResultBlock = Schema.Struct({ @@ -283,8 +284,6 @@ const lowerToolResultContent = Effect.fn("BedrockConverse.lowerToolResultContent data: item.uri, filename: item.name, }) - if (!("image" in media)) - return yield* ProviderShared.invalidRequest("Bedrock Converse only supports image media in tool results") content.push(media) } return content diff --git a/packages/ai/src/protocols/gemini.ts b/packages/ai/src/protocols/gemini.ts index 82a69b059ef5..6cba2bc7f9b3 100644 --- a/packages/ai/src/protocols/gemini.ts +++ b/packages/ai/src/protocols/gemini.ts @@ -41,9 +41,11 @@ const GeminiInlineDataPart = Schema.Struct({ data: Schema.String, }), }) +type GeminiInlineDataPart = Schema.Schema.Type const GeminiFunctionCallPart = Schema.Struct({ functionCall: Schema.Struct({ + id: Schema.optional(Schema.String), name: Schema.String, args: Schema.Unknown, }), @@ -52,8 +54,10 @@ const GeminiFunctionCallPart = Schema.Struct({ const GeminiFunctionResponsePart = Schema.Struct({ functionResponse: Schema.Struct({ + id: Schema.optional(Schema.String), name: Schema.String, response: Schema.Unknown, + parts: Schema.optional(Schema.Array(GeminiInlineDataPart)), }), }) @@ -197,8 +201,13 @@ const thoughtSignature = (providerMetadata: ProviderMetadata | undefined) => { : undefined } +const functionCallId = (providerMetadata: ProviderMetadata | undefined) => { + const google = providerMetadata?.google + return ProviderShared.isRecord(google) && typeof google.functionCallId === "string" ? google.functionCallId : undefined +} + const lowerToolCall = (part: ToolCallPart) => ({ - functionCall: { name: part.name, args: part.input }, + functionCall: { id: functionCallId(part.providerMetadata), name: part.name, args: part.input }, thoughtSignature: thoughtSignature(part.providerMetadata), }) @@ -255,6 +264,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR if (part.result.type !== "content") { parts.push({ functionResponse: { + id: functionCallId(part.providerMetadata), name: part.name, response: { name: part.name, @@ -266,20 +276,23 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR } const content: ReadonlyArray = part.result.value const text = content.filter((item) => item.type === "text").map((item) => item.text) + const media: GeminiInlineDataPart[] = [] + for (const item of content) { + if (item.type === "text") continue + const value = yield* ProviderShared.validateToolFile("Gemini", item, MEDIA_MIMES) + media.push({ inlineData: { mimeType: value.mime, data: value.base64 } }) + } parts.push({ functionResponse: { + id: functionCallId(part.providerMetadata), name: part.name, response: { name: part.name, content: text.join("\n"), }, + parts: media.length > 0 ? media : undefined, }, }) - for (const item of content) { - if (item.type === "text") continue - const media = yield* ProviderShared.validateToolFile("Gemini", item, MEDIA_MIMES) - parts.push({ inlineData: { mimeType: media.mime, data: media.base64 } }) - } } contents.push({ role: "user", parts }) } @@ -441,6 +454,10 @@ const step = (state: ParserState, event: GeminiEvent) => { if ("functionCall" in part) { const input = part.functionCall.args const id = `tool_${nextToolCallId++}` + const metadata = { + ...(part.functionCall.id === undefined ? {} : { functionCallId: part.functionCall.id }), + ...(part.thoughtSignature === undefined ? {} : { thoughtSignature: part.thoughtSignature }), + } lifecycle = Lifecycle.reasoningEnd( lifecycle, events, @@ -453,9 +470,7 @@ const step = (state: ParserState, event: GeminiEvent) => { id, name: part.functionCall.name, input, - providerMetadata: part.thoughtSignature - ? googleMetadata({ thoughtSignature: part.thoughtSignature }) - : undefined, + providerMetadata: Object.keys(metadata).length > 0 ? googleMetadata(metadata) : undefined, }), ) hasToolCalls = true diff --git a/packages/ai/src/protocols/openai-responses.ts b/packages/ai/src/protocols/openai-responses.ts index f8e11b3cc2a1..87ca8c75e1a4 100644 --- a/packages/ai/src/protocols/openai-responses.ts +++ b/packages/ai/src/protocols/openai-responses.ts @@ -11,6 +11,7 @@ import { type FinishReason, type JsonSchema, type LLMRequest, + type MediaPart, type ProviderMetadata, type ReasoningPart, type TextPart, @@ -28,6 +29,7 @@ import { ToolStream } from "./utils/tool-stream" import { OpenAIImage } from "./utils/openai-image" const ADAPTER = "openai-responses" +const MEDIA_MIMES = new Set([...ProviderShared.IMAGE_MIMES, ...ProviderShared.PDF_MIMES]) export const DEFAULT_BASE_URL = "https://api.openai.com/v1" export const PATH = "/responses" @@ -42,7 +44,17 @@ const OpenAIResponsesInputImage = Schema.Struct({ type: Schema.tag("input_image"), image_url: Schema.String, }) -const OpenAIResponsesInputContent = Schema.Union([OpenAIResponsesInputText, OpenAIResponsesInputImage]) +const OpenAIResponsesInputFile = Schema.Struct({ + type: Schema.tag("input_file"), + filename: Schema.String, + file_data: Schema.String, + mime_type: Schema.optional(Schema.String), +}) +const OpenAIResponsesInputContent = Schema.Union([ + OpenAIResponsesInputText, + OpenAIResponsesInputImage, + OpenAIResponsesInputFile, +]) type OpenAIResponsesInputContent = Schema.Schema.Type const OpenAIResponsesOutputText = Schema.Struct({ @@ -68,9 +80,13 @@ const OpenAIResponsesItemReference = Schema.Struct({ }) // `function_call_output.output` accepts either a plain string or an ordered -// array of content items so tools can return images in addition to text. +// array of content items so tools can return images and files in addition to text. // https://platform.openai.com/docs/api-reference/responses/object -const OpenAIResponsesFunctionCallOutputContent = Schema.Union([OpenAIResponsesInputText, OpenAIResponsesInputImage]) +const OpenAIResponsesFunctionCallOutputContent = Schema.Union([ + OpenAIResponsesInputText, + OpenAIResponsesInputImage, + OpenAIResponsesInputFile, +]) const OpenAIResponsesFunctionCallOutput = Schema.Union([ Schema.String, @@ -343,42 +359,58 @@ const hostedToolItemID = (part: ToolResultPart) => { : undefined } +const lowerMedia = Effect.fn("OpenAIResponses.lowerMedia")(function* (part: MediaPart, provider: string) { + const media = yield* ProviderShared.validateMedia("OpenAI Responses", part, MEDIA_MIMES) + if (media.mime === "application/pdf") { + // xAI models inline bytes and MIME separately; OpenAI uses a data URL in file_data. + if (provider === "xai") + return { + type: "input_file" as const, + filename: part.filename ?? "document.pdf", + file_data: media.base64, + mime_type: media.mime, + } + return { + type: "input_file" as const, + filename: part.filename ?? "document.pdf", + file_data: media.dataUrl, + } + } + return { type: "input_image" as const, image_url: media.dataUrl } +}) + const lowerUserContent = Effect.fn("OpenAIResponses.lowerUserContent")(function* ( part: LLMRequest["messages"][number]["content"][number], + provider: string, ) { if (part.type === "text") return { type: "input_text" as const, text: part.text } - if (part.type === "media") { - const media = yield* ProviderShared.validateMedia( - "OpenAI Responses", - part, - new Set(ProviderShared.IMAGE_MIMES), - ) - return { type: "input_image" as const, image_url: media.dataUrl } - } + if (part.type === "media") return yield* lowerMedia(part, provider) return yield* ProviderShared.unsupportedContent("OpenAI Responses", "user", ["text", "media"]) }) -// Tool results may carry structured text/images. Keep media as provider-native +// Tool results may carry structured text, images, and files. Keep media as provider-native // content instead of JSON-stringifying base64 into a prompt string. const lowerToolResultContentItem = Effect.fn("OpenAIResponses.lowerToolResultContentItem")(function* ( item: ToolContent, + provider: string, ) { if (item.type === "text") return { type: "input_text" as const, text: item.text } - const media = yield* ProviderShared.validateToolFile( - "OpenAI Responses", - item, - new Set(ProviderShared.IMAGE_MIMES), + return yield* lowerMedia( + { type: "media", mediaType: item.mime, data: item.uri, filename: item.name }, + provider, ) - return { type: "input_image" as const, image_url: media.dataUrl } }) -const lowerToolResultOutput = Effect.fn("OpenAIResponses.lowerToolResultOutput")(function* (part: ToolResultPart) { +const lowerToolResultOutput = Effect.fn("OpenAIResponses.lowerToolResultOutput")(function* ( + part: ToolResultPart, + provider: string, +) { // Text/json/error results are encoded as a plain string for backward // compatibility with existing cassettes and provider expectations. if (part.result.type !== "content") return ProviderShared.toolResultText(part) // Preserve the narrowed array element type when compiled through a consumer package. const content: ReadonlyArray = part.result.value - return yield* Effect.forEach(content, lowerToolResultContentItem) + return yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, provider)) }) const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (request: LLMRequest) { @@ -401,7 +433,10 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ } if (message.role === "user") { - input.push({ role: "user", content: yield* Effect.forEach(message.content, lowerUserContent) }) + input.push({ + role: "user", + content: yield* Effect.forEach(message.content, (part) => lowerUserContent(part, request.model.provider)), + }) continue } @@ -460,7 +495,9 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ const content: ReadonlyArray = part.result.value input.push({ role: "user", - content: yield* Effect.forEach(content, lowerToolResultContentItem), + content: yield* Effect.forEach(content, (item) => + lowerToolResultContentItem(item, request.model.provider), + ), }) } if (itemID) hostedToolReferences.add(itemID) @@ -483,7 +520,7 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ input.push({ type: "function_call_output", call_id: part.id, - output: yield* lowerToolResultOutput(part), + output: yield* lowerToolResultOutput(part, request.model.provider), }) } } diff --git a/packages/ai/src/protocols/shared.ts b/packages/ai/src/protocols/shared.ts index 173dc511bb03..478088b08177 100644 --- a/packages/ai/src/protocols/shared.ts +++ b/packages/ai/src/protocols/shared.ts @@ -158,7 +158,8 @@ export const parseToolInput = (route: string, name: string, raw: string) => export const IMAGE_MIMES = ["image/png", "image/jpeg", "image/gif", "image/webp"] as const export const VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"] as const export const AUDIO_MIMES = ["audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", "audio/flac"] as const -export const MEDIA_MIMES = [...IMAGE_MIMES, ...VIDEO_MIMES, ...AUDIO_MIMES] as const +export const PDF_MIMES = ["application/pdf"] as const +export const MEDIA_MIMES = [...IMAGE_MIMES, ...VIDEO_MIMES, ...AUDIO_MIMES, ...PDF_MIMES] as const export const MAX_MEDIA_ENCODED_BYTES = 28 * 1024 * 1024 export const MAX_MEDIA_DECODED_BYTES = 20 * 1024 * 1024 diff --git a/packages/ai/src/protocols/utils/bedrock-media.ts b/packages/ai/src/protocols/utils/bedrock-media.ts index 6fda6c4fbb43..6a12966b7b32 100644 --- a/packages/ai/src/protocols/utils/bedrock-media.ts +++ b/packages/ai/src/protocols/utils/bedrock-media.ts @@ -49,10 +49,10 @@ const DOCUMENT_FORMATS = { "text/markdown": "md", } as const satisfies Record -const documentBlock = (part: MediaPart, format: DocumentFormat, bytes: string): DocumentBlock => ({ +const documentBlock = (name: string, format: DocumentFormat, bytes: string): DocumentBlock => ({ document: { format, - name: part.filename ?? `document.${format}`, + name, source: { bytes }, }, }) @@ -77,12 +77,14 @@ export const lower = Effect.fn("BedrockMedia.lower")(function* (part: MediaPart) return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support image media type ${part.mediaType}`) const documentFormat = DOCUMENT_FORMATS[mime as keyof typeof DOCUMENT_FORMATS] if (documentFormat) { + if (!part.filename) + return yield* ProviderShared.invalidRequest("Bedrock Converse document media requires a filename") const media = yield* ProviderShared.validateMedia( "Bedrock Converse", part, new Set(Object.keys(DOCUMENT_FORMATS)), ) - return documentBlock(part, documentFormat, media.base64) + return documentBlock(part.filename, documentFormat, media.base64) } return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support media type ${part.mediaType}`) }) diff --git a/packages/ai/src/tool-runtime.ts b/packages/ai/src/tool-runtime.ts index d69bbb9d478c..c483950c1252 100644 --- a/packages/ai/src/tool-runtime.ts +++ b/packages/ai/src/tool-runtime.ts @@ -68,10 +68,29 @@ const result = (call: ToolCallPart, value: ToolResultValueType | ToolSettlement, events: settlement.result.type === "error" ? [ - LLMEvent.toolError({ id: call.id, name: call.name, message: String(settlement.result.value), error }), - LLMEvent.toolResult({ id: call.id, name: call.name, result: settlement.result }), + LLMEvent.toolError({ + id: call.id, + name: call.name, + message: String(settlement.result.value), + error, + providerMetadata: call.providerMetadata, + }), + LLMEvent.toolResult({ + id: call.id, + name: call.name, + result: settlement.result, + providerMetadata: call.providerMetadata, + }), ] - : [LLMEvent.toolResult({ id: call.id, name: call.name, result: settlement.result, output: settlement.output })], + : [ + LLMEvent.toolResult({ + id: call.id, + name: call.name, + result: settlement.result, + output: settlement.output, + providerMetadata: call.providerMetadata, + }), + ], } } diff --git a/packages/ai/test/fixtures/recordings/pdf/anthropic-tool-result.json b/packages/ai/test/fixtures/recordings/pdf/anthropic-tool-result.json new file mode 100644 index 000000000000..69281239e5b5 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/pdf/anthropic-tool-result.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:pdf", + "pdf", + "provider:anthropic", + "protocol:anthropic-messages", + "tool", + "tool-result" + ], + "name": "pdf/anthropic-tool-result", + "recordedAt": "2026-07-22T18:15:39.002Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.anthropic.com/v1/messages", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"claude-haiku-4-5-20251001\",\"system\":[{\"type\":\"text\",\"text\":\"Read the PDF returned by the tool and follow the user's response format exactly.\"}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Return only the verification code from the PDF.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"call_pdf_1\",\"name\":\"read_pdf\",\"input\":{}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"call_pdf_1\",\"content\":[{\"type\":\"text\",\"text\":\"PDF read successfully\"},{\"type\":\"document\",\"source\":{\"type\":\"base64\",\"media_type\":\"application/pdf\",\"data\":\"JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiAvQ29udGVudHMgNCAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCA3NSA+PgpzdHJlYW0KQlQKL0YxIDE4IFRmCjcyIDcyMCBUZAooUERGIGNhc3NldHRlIHZlcmlmaWNhdGlvbiBjb2RlOiBPUkNISUQtNzM5MSkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UeXBlMSAvQmFzZUZvbnQgL0hlbHZldGljYSA+PgplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQxIDAwMDAwIG4gCjAwMDAwMDAzNjUgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo0MzUKJSVFT0YK\"}}]}]}],\"tools\":[{\"name\":\"read_pdf\",\"description\":\"Read the attached PDF.\",\"input_schema\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}}],\"stream\":true,\"max_tokens\":40,\"temperature\":0}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_011CdHYzxyRpSVFgwTm6ccUr\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":2229,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":2,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"ORCH\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"ID-7391\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":2229,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":9} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/pdf/anthropic-user-input.json b/packages/ai/test/fixtures/recordings/pdf/anthropic-user-input.json new file mode 100644 index 000000000000..372474e11f0b --- /dev/null +++ b/packages/ai/test/fixtures/recordings/pdf/anthropic-user-input.json @@ -0,0 +1,34 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:pdf", + "pdf", + "provider:anthropic", + "protocol:anthropic-messages", + "user-input" + ], + "name": "pdf/anthropic-user-input", + "recordedAt": "2026-07-22T18:15:37.979Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.anthropic.com/v1/messages", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"claude-haiku-4-5-20251001\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"document\",\"source\":{\"type\":\"base64\",\"media_type\":\"application/pdf\",\"data\":\"JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiAvQ29udGVudHMgNCAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCA3NSA+PgpzdHJlYW0KQlQKL0YxIDE4IFRmCjcyIDcyMCBUZAooUERGIGNhc3NldHRlIHZlcmlmaWNhdGlvbiBjb2RlOiBPUkNISUQtNzM5MSkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UeXBlMSAvQmFzZUZvbnQgL0hlbHZldGljYSA+PgplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQxIDAwMDAwIG4gCjAwMDAwMDAzNjUgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo0MzUKJSVFT0YK\"}},{\"type\":\"text\",\"text\":\"Return only the verification code from the PDF.\"}]}],\"stream\":true,\"max_tokens\":40,\"temperature\":0}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_011CdHYzsayb45rgfamcjFt3\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1602,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":2,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"ORCH\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"ID-7391\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":1602,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":9} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/pdf/bedrock-tool-result.json b/packages/ai/test/fixtures/recordings/pdf/bedrock-tool-result.json new file mode 100644 index 000000000000..3225e019d667 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/pdf/bedrock-tool-result.json @@ -0,0 +1,36 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:pdf", + "pdf", + "provider:amazon-bedrock", + "protocol:bedrock-converse", + "tool", + "tool-result" + ], + "name": "pdf/bedrock-tool-result", + "recordedAt": "2026-07-22T18:15:52.400Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-haiku-4-5-20251001-v1%3A0/converse-stream", + "headers": { + "content-type": "application/json" + }, + "body": "{\"modelId\":\"us.anthropic.claude-haiku-4-5-20251001-v1:0\",\"messages\":[{\"role\":\"user\",\"content\":[{\"text\":\"Return only the verification code from the PDF.\"}]},{\"role\":\"assistant\",\"content\":[{\"toolUse\":{\"toolUseId\":\"call_pdf_1\",\"name\":\"read_pdf\",\"input\":{}}}]},{\"role\":\"user\",\"content\":[{\"toolResult\":{\"toolUseId\":\"call_pdf_1\",\"content\":[{\"text\":\"PDF read successfully\"},{\"document\":{\"format\":\"pdf\",\"name\":\"verification\",\"source\":{\"bytes\":\"JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiAvQ29udGVudHMgNCAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCA3NSA+PgpzdHJlYW0KQlQKL0YxIDE4IFRmCjcyIDcyMCBUZAooUERGIGNhc3NldHRlIHZlcmlmaWNhdGlvbiBjb2RlOiBPUkNISUQtNzM5MSkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UeXBlMSAvQmFzZUZvbnQgL0hlbHZldGljYSA+PgplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQxIDAwMDAwIG4gCjAwMDAwMDAzNjUgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo0MzUKJSVFT0YK\"}}}],\"status\":\"success\"}}]}],\"system\":[{\"text\":\"Read the PDF returned by the tool and follow the user's response format exactly.\"}],\"inferenceConfig\":{\"maxTokens\":40,\"temperature\":0},\"toolConfig\":{\"tools\":[{\"toolSpec\":{\"name\":\"read_pdf\",\"description\":\"Read the attached PDF.\",\"inputSchema\":{\"json\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}}}}]}}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "application/vnd.amazon.eventstream" + }, + "body": "AAAAqgAAAFLa0GiGCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSUyIsInJvbGUiOiJhc3Npc3RhbnQifXIDPnsAAADIAAAAV0lIuCQLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiT1JDSCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUSJ9z2HHHgAAAMUAAABXsdh8lQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiJJRC0ifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PIn2+8d8RAAAAywAAAFcO6ML0CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IjcifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVlcifSIeZ+kAAACzAAAAV8dKafoLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiMzkxIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2dyJ9HAStJQAAAMAAAABWDj/Dcws6ZXZlbnQtdHlwZQcAEGNvbnRlbnRCbG9ja1N0b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU2NyJ9EPTSwQAAALAAAABRaYm2Hws6ZXZlbnQtdHlwZQcAC21lc3NhZ2VTdG9wDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVIiwic3RvcFJlYXNvbiI6ImVuZF90dXJuIn0SuCAcAAAA+AAAAE6MAqhiCzpldmVudC10eXBlBwAIbWV0YWRhdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJtZXRyaWNzIjp7ImxhdGVuY3lNcyI6MzkyMn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFIiwidXNhZ2UiOnsiaW5wdXRUb2tlbnMiOjIyNDEsIm91dHB1dFRva2VucyI6Niwic2VydmVyVG9vbFVzYWdlIjp7fSwidG90YWxUb2tlbnMiOjIyNDd9fcd35Hw=", + "bodyEncoding": "base64" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/pdf/bedrock-user-input.json b/packages/ai/test/fixtures/recordings/pdf/bedrock-user-input.json new file mode 100644 index 000000000000..53f04e5777d2 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/pdf/bedrock-user-input.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:pdf", + "pdf", + "provider:amazon-bedrock", + "protocol:bedrock-converse", + "user-input" + ], + "name": "pdf/bedrock-user-input", + "recordedAt": "2026-07-22T18:15:48.408Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-haiku-4-5-20251001-v1%3A0/converse-stream", + "headers": { + "content-type": "application/json" + }, + "body": "{\"modelId\":\"us.anthropic.claude-haiku-4-5-20251001-v1:0\",\"messages\":[{\"role\":\"user\",\"content\":[{\"document\":{\"format\":\"pdf\",\"name\":\"verification\",\"source\":{\"bytes\":\"JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiAvQ29udGVudHMgNCAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCA3NSA+PgpzdHJlYW0KQlQKL0YxIDE4IFRmCjcyIDcyMCBUZAooUERGIGNhc3NldHRlIHZlcmlmaWNhdGlvbiBjb2RlOiBPUkNISUQtNzM5MSkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UeXBlMSAvQmFzZUZvbnQgL0hlbHZldGljYSA+PgplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQxIDAwMDAwIG4gCjAwMDAwMDAzNjUgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo0MzUKJSVFT0YK\"}}},{\"text\":\"Return only the verification code from the PDF.\"}]}],\"inferenceConfig\":{\"maxTokens\":40,\"temperature\":0}}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "application/vnd.amazon.eventstream" + }, + "body": "AAAAtgAAAFJ/wBIFCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNCIsInJvbGUiOiJhc3Npc3RhbnQifURlAvAAAADGAAAAV/Z4BkULOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiT1JDSCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk8ifU1V/fQAAADWAAAAV5aYkccLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiSUQtIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQ1In1Rr1g8AAAAoAAAAFfgCoSoCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IjcifSwicCI6ImFiY2RlZiJ9UwQMPQAAAM8AAABX+2hkNAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIzOTEifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWSJ9ZmoyCwAAAJAAAABWNiwMuAs6ZXZlbnQtdHlwZQcAEGNvbnRlbnRCbG9ja1N0b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwicCI6ImFiY2RlZmdoaWprbCJ9wtmmXgAAAIgAAABR+NhFWAs6ZXZlbnQtdHlwZQcAC21lc3NhZ2VTdG9wDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmciLCJzdG9wUmVhc29uIjoiZW5kX3R1cm4ifa8D/doAAADvAAAATl7C4/ALOmV2ZW50LXR5cGUHAAhtZXRhZGF0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7Im1ldHJpY3MiOnsibGF0ZW5jeU1zIjo0NTQ1fSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXYiLCJ1c2FnZSI6eyJpbnB1dFRva2VucyI6MTYxNCwib3V0cHV0VG9rZW5zIjo2LCJzZXJ2ZXJUb29sVXNhZ2UiOnt9LCJ0b3RhbFRva2VucyI6MTYyMH19db4j2Q==", + "bodyEncoding": "base64" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/pdf/gemini-tool-result.json b/packages/ai/test/fixtures/recordings/pdf/gemini-tool-result.json new file mode 100644 index 000000000000..1ac7bc5f6ef7 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/pdf/gemini-tool-result.json @@ -0,0 +1,53 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:pdf", + "pdf", + "provider:google", + "protocol:gemini", + "tool", + "tool-result" + ], + "name": "pdf/gemini-tool-result", + "recordedAt": "2026-07-22T18:21:59.606Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:streamGenerateContent?alt=sse", + "headers": { + "content-type": "application/json" + }, + "body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"Use read_pdf with path verification.pdf and return the verification code.\"}]}],\"systemInstruction\":{\"parts\":[{\"text\":\"Call read_pdf exactly once with path verification.pdf, then reply only with the verification code from its PDF.\"}]},\"tools\":[{\"functionDeclarations\":[{\"name\":\"read_pdf\",\"description\":\"Read the attached PDF.\",\"parameters\":{\"required\":[\"path\"],\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}}}}]}],\"generationConfig\":{\"maxOutputTokens\":256,\"temperature\":0}}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream" + }, + "body": "data: {\"candidates\": [{\"content\": {\"parts\": [{\"functionCall\": {\"name\": \"read_pdf\",\"args\": {\"path\": \"verification.pdf\"},\"id\": \"58shgmez\"},\"thoughtSignature\": \"EqkCCqYCARFNMg/JrCTv5i3zYENFBVpZNFL3pbzJmi5Eu387ncF703xFMB4pwyaP7a1gi49EqBhCI2hWOpesU5nZQOLAhGgExKGa2GM+HzpEB5g62r0NFblm/BGkVZaImTuHR7bytfRC5jHQlHKo4OS27OLUVjvkMkBIYsvjhDErY7niERbXJVpyxTVqUf1GgZMSu8kC9/5WDlMs9xVKNT/6KMW4PhhSR9nXg4KZUa+bC03/ydhsWWgBa5aLCgvTq7WPj217xIsmUkSiRedIffPsUSNjYdMHUvWi8bOlvM1veEEP6GIfv5h9gXXzjnHbEHfQxV8PZuBAyY7iM6nqyfkJNdkZ1HdB7DXMBsMsRN6SgrIrFoXX2WaGrkoEI5tdZx1t/gdwF1jEVT6k\"}],\"role\": \"model\"},\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 81,\"candidatesTokenCount\": 18,\"totalTokenCount\": 151,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 81}],\"thoughtsTokenCount\": 52,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"RAphaui3OaSHz7IPy8Kb4Ak\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"\"}],\"role\": \"model\"},\"finishReason\": \"STOP\",\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 81,\"candidatesTokenCount\": 18,\"totalTokenCount\": 151,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 81}],\"thoughtsTokenCount\": 52,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"RAphaui3OaSHz7IPy8Kb4Ak\"}\r\n\r\n" + } + }, + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:streamGenerateContent?alt=sse", + "headers": { + "content-type": "application/json" + }, + "body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"Use read_pdf with path verification.pdf and return the verification code.\"}]},{\"role\":\"model\",\"parts\":[{\"functionCall\":{\"id\":\"58shgmez\",\"name\":\"read_pdf\",\"args\":{\"path\":\"verification.pdf\"}},\"thoughtSignature\":\"EqkCCqYCARFNMg/JrCTv5i3zYENFBVpZNFL3pbzJmi5Eu387ncF703xFMB4pwyaP7a1gi49EqBhCI2hWOpesU5nZQOLAhGgExKGa2GM+HzpEB5g62r0NFblm/BGkVZaImTuHR7bytfRC5jHQlHKo4OS27OLUVjvkMkBIYsvjhDErY7niERbXJVpyxTVqUf1GgZMSu8kC9/5WDlMs9xVKNT/6KMW4PhhSR9nXg4KZUa+bC03/ydhsWWgBa5aLCgvTq7WPj217xIsmUkSiRedIffPsUSNjYdMHUvWi8bOlvM1veEEP6GIfv5h9gXXzjnHbEHfQxV8PZuBAyY7iM6nqyfkJNdkZ1HdB7DXMBsMsRN6SgrIrFoXX2WaGrkoEI5tdZx1t/gdwF1jEVT6k\"}]},{\"role\":\"user\",\"parts\":[{\"functionResponse\":{\"id\":\"58shgmez\",\"name\":\"read_pdf\",\"response\":{\"name\":\"read_pdf\",\"content\":\"PDF read successfully\"},\"parts\":[{\"inlineData\":{\"mimeType\":\"application/pdf\",\"data\":\"JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiAvQ29udGVudHMgNCAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCA3NSA+PgpzdHJlYW0KQlQKL0YxIDE4IFRmCjcyIDcyMCBUZAooUERGIGNhc3NldHRlIHZlcmlmaWNhdGlvbiBjb2RlOiBPUkNISUQtNzM5MSkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UeXBlMSAvQmFzZUZvbnQgL0hlbHZldGljYSA+PgplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQxIDAwMDAwIG4gCjAwMDAwMDAzNjUgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo0MzUKJSVFT0YK\"}}]}}]}],\"systemInstruction\":{\"parts\":[{\"text\":\"Call read_pdf exactly once with path verification.pdf, then reply only with the verification code from its PDF.\"}]},\"tools\":[{\"functionDeclarations\":[{\"name\":\"read_pdf\",\"description\":\"Read the attached PDF.\",\"parameters\":{\"required\":[\"path\"],\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}}}}]}],\"generationConfig\":{\"maxOutputTokens\":256,\"temperature\":0}}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream" + }, + "body": "data: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"ORCHID-7391\"}],\"role\": \"model\"},\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 123,\"candidatesTokenCount\": 8,\"totalTokenCount\": 184,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 123}],\"thoughtsTokenCount\": 53,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"RgphaoL6CMjQz7IPjOnEmQI\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"\",\"thoughtSignature\": \"EqECCp4CARFNMg9obBl8O6iU9lawUIWiE+1vztZm9NtaT9FuyJz343hd9ruz+xPco4Q1DY1GF81ZiSI2ElBkt8Wfwsqtix9LNGSMvbZhhk/ZnB54t05M/Dft1kujcMvEdZUWUI/jWaJ349tO1bKVH9MacG5+gl0n4y8DwyQZSV3xIcet547drSkcA/TM03RB+yj1/dcLHsvUjmv9EnO897vZgO2Dk4tbZ2NyCtOeQ3JKVhUTLg2pjkGk+POCNiOdESWiUzxdQKw9LiV6nnzi071tXNiMeVimq6d7xAzRVNapI2uXynvn9Uk3eyn85purOFa8cKriK9oD6vcyGMqgd9+gu2m3to0IHqd7o+2YSr1m5qV1xT1R2/WRQEtb1b1AuOAU6w==\"}],\"role\": \"model\"},\"finishReason\": \"STOP\",\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 1277,\"candidatesTokenCount\": 8,\"totalTokenCount\": 1338,\"promptTokensDetails\": [{\"modality\": \"IMAGE\",\"tokenCount\": 1102},{\"modality\": \"TEXT\",\"tokenCount\": 175}],\"thoughtsTokenCount\": 53,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"RgphaoL6CMjQz7IPjOnEmQI\"}\r\n\r\n" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/pdf/gemini-user-input.json b/packages/ai/test/fixtures/recordings/pdf/gemini-user-input.json new file mode 100644 index 000000000000..dbe73618a04d --- /dev/null +++ b/packages/ai/test/fixtures/recordings/pdf/gemini-user-input.json @@ -0,0 +1,34 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:pdf", + "pdf", + "provider:google", + "protocol:gemini", + "user-input" + ], + "name": "pdf/gemini-user-input", + "recordedAt": "2026-07-22T18:20:55.140Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:streamGenerateContent?alt=sse", + "headers": { + "content-type": "application/json" + }, + "body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"inlineData\":{\"mimeType\":\"application/pdf\",\"data\":\"JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiAvQ29udGVudHMgNCAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCA3NSA+PgpzdHJlYW0KQlQKL0YxIDE4IFRmCjcyIDcyMCBUZAooUERGIGNhc3NldHRlIHZlcmlmaWNhdGlvbiBjb2RlOiBPUkNISUQtNzM5MSkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UeXBlMSAvQmFzZUZvbnQgL0hlbHZldGljYSA+PgplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQxIDAwMDAwIG4gCjAwMDAwMDAzNjUgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo0MzUKJSVFT0YK\"}},{\"text\":\"Return only the verification code from the PDF.\"}]}],\"generationConfig\":{\"maxOutputTokens\":256,\"temperature\":0}}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream" + }, + "body": "data: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"ORCH\"}],\"role\": \"model\"},\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 10,\"candidatesTokenCount\": 2,\"totalTokenCount\": 127,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 10}],\"thoughtsTokenCount\": 115,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"BQpharW2KaPgz7IP6uSLiAw\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"ID-7391\"}],\"role\": \"model\"},\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 10,\"candidatesTokenCount\": 8,\"totalTokenCount\": 133,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 10}],\"thoughtsTokenCount\": 115,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"BQpharW2KaPgz7IP6uSLiAw\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"\",\"thoughtSignature\": \"EokECoYEARFNMg8L4fpLqaX8tIQZcvw2vLt3WsFjGqpuJGgna0/AGczwuzndRcf3LGIEaliCf4ijVOb1AG4/VPBh1kMzfjeAyHhvWIe4yQVoBwI7BjpFyLie+SnGTXQXKKy5ygRqRLFsV6DcAixNXXBHJw2x/2Nhtriryqs4fhWrL/P7ppHC10sMnTwN6Mw5x20NKwgT+rrw6lvYmQe9rdQsBJ6Zmp0GpPlwZZiAgzvwPfVoNwHSGb54xe/T9wjISjwWNgpedhbsIBDRZFDwruS4x57KBKeMPO69GLfeMP8PJ7rpR0HgT7nRbrl/OdykG/jqSMTvoRSqxawsD+Yr/DukgGatyfB5Ic+X4RhD07URpkGTAu/cakBtzhSmM/hpzKU9m/cId1UCjopLTtonUqSAKkroPdp8kIYw0MI2OZCNVwbDrdClUPmjRKfcTkcC2jNj1rS+WDFbm+mo+SP3rDSvvCdyJuiXHGKiM2EhbYnu42aHVC6w7eAe4Gv3Fq/0faW47r0ihbiAohFB9XUA+fD07g83EjIuc9Q6BRVTTcBfoRkrR/yFZKt3qwPq02W6rPD13/1wAnMtabNcxePMMGk7Dlxwng9yPS0NEge2KD+miOj9SC4aTvOTq2451tfK1x3UZqqb205zGOPbjizhH/CA/PGkG84hdkAG4mrUK0rEHqeWwRXDsxpyfto=\"}],\"role\": \"model\"},\"finishReason\": \"STOP\",\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 530,\"candidatesTokenCount\": 8,\"totalTokenCount\": 653,\"promptTokensDetails\": [{\"modality\": \"IMAGE\",\"tokenCount\": 520},{\"modality\": \"TEXT\",\"tokenCount\": 10}],\"thoughtsTokenCount\": 115,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"BQpharW2KaPgz7IP6uSLiAw\"}\r\n\r\n" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/pdf/openai-tool-result.json b/packages/ai/test/fixtures/recordings/pdf/openai-tool-result.json new file mode 100644 index 000000000000..42363b5ec8b4 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/pdf/openai-tool-result.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:pdf", + "pdf", + "provider:openai", + "protocol:openai-responses", + "tool", + "tool-result" + ], + "name": "pdf/openai-tool-result", + "recordedAt": "2026-07-22T18:15:36.438Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.openai.com/v1/responses", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"gpt-4o-mini\",\"input\":[{\"role\":\"system\",\"content\":\"Read the PDF returned by the tool and follow the user's response format exactly.\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Return only the verification code from the PDF.\"}]},{\"type\":\"function_call\",\"call_id\":\"call_pdf_1\",\"name\":\"read_pdf\",\"arguments\":\"{}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_pdf_1\",\"output\":[{\"type\":\"input_text\",\"text\":\"PDF read successfully\"},{\"type\":\"input_file\",\"filename\":\"verification.pdf\",\"file_data\":\"data:application/pdf;base64,JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiAvQ29udGVudHMgNCAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCA3NSA+PgpzdHJlYW0KQlQKL0YxIDE4IFRmCjcyIDcyMCBUZAooUERGIGNhc3NldHRlIHZlcmlmaWNhdGlvbiBjb2RlOiBPUkNISUQtNzM5MSkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UeXBlMSAvQmFzZUZvbnQgL0hlbHZldGljYSA+PgplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQxIDAwMDAwIG4gCjAwMDAwMDAzNjUgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo0MzUKJSVFT0YK\"}]}],\"tools\":[{\"type\":\"function\",\"name\":\"read_pdf\",\"description\":\"Read the attached PDF.\",\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false},\"strict\":false}],\"store\":false,\"max_output_tokens\":40,\"temperature\":0,\"stream\":true}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_0cd823aeeb11ef0e016a6108c703b88193872d736c48d60e2c\",\"object\":\"response\",\"created_at\":1784744135,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":0.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read the attached PDF.\",\"name\":\"read_pdf\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false},\"strict\":false}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_0cd823aeeb11ef0e016a6108c703b88193872d736c48d60e2c\",\"object\":\"response\",\"created_at\":1784744135,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":0.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read the attached PDF.\",\"name\":\"read_pdf\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false},\"strict\":false}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_0cd823aeeb11ef0e016a6108c83f8081938114e89ac5d99c7e\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_0cd823aeeb11ef0e016a6108c83f8081938114e89ac5d99c7e\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"The\",\"item_id\":\"msg_0cd823aeeb11ef0e016a6108c83f8081938114e89ac5d99c7e\",\"logprobs\":[],\"obfuscation\":\"ztkMbqje3EBwS\",\"output_index\":0,\"sequence_number\":4}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" verification\",\"item_id\":\"msg_0cd823aeeb11ef0e016a6108c83f8081938114e89ac5d99c7e\",\"logprobs\":[],\"obfuscation\":\"mEL\",\"output_index\":0,\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" code\",\"item_id\":\"msg_0cd823aeeb11ef0e016a6108c83f8081938114e89ac5d99c7e\",\"logprobs\":[],\"obfuscation\":\"l7H1kZM2CjW\",\"output_index\":0,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" from\",\"item_id\":\"msg_0cd823aeeb11ef0e016a6108c83f8081938114e89ac5d99c7e\",\"logprobs\":[],\"obfuscation\":\"zoQfdh40bG4\",\"output_index\":0,\"sequence_number\":7}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" the\",\"item_id\":\"msg_0cd823aeeb11ef0e016a6108c83f8081938114e89ac5d99c7e\",\"logprobs\":[],\"obfuscation\":\"5fXB3VBJUbAr\",\"output_index\":0,\"sequence_number\":8}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" PDF\",\"item_id\":\"msg_0cd823aeeb11ef0e016a6108c83f8081938114e89ac5d99c7e\",\"logprobs\":[],\"obfuscation\":\"BpJQrXS6gY3c\",\"output_index\":0,\"sequence_number\":9}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" is\",\"item_id\":\"msg_0cd823aeeb11ef0e016a6108c83f8081938114e89ac5d99c7e\",\"logprobs\":[],\"obfuscation\":\"xmGyHaXtMBy6i\",\"output_index\":0,\"sequence_number\":10}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\":\",\"item_id\":\"msg_0cd823aeeb11ef0e016a6108c83f8081938114e89ac5d99c7e\",\"logprobs\":[],\"obfuscation\":\"oJeWezxDkzq3MRx\",\"output_index\":0,\"sequence_number\":11}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" OR\",\"item_id\":\"msg_0cd823aeeb11ef0e016a6108c83f8081938114e89ac5d99c7e\",\"logprobs\":[],\"obfuscation\":\"LpS12bymKrO3H\",\"output_index\":0,\"sequence_number\":12}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"CH\",\"item_id\":\"msg_0cd823aeeb11ef0e016a6108c83f8081938114e89ac5d99c7e\",\"logprobs\":[],\"obfuscation\":\"EvCpePUNmEXVil\",\"output_index\":0,\"sequence_number\":13}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"ID\",\"item_id\":\"msg_0cd823aeeb11ef0e016a6108c83f8081938114e89ac5d99c7e\",\"logprobs\":[],\"obfuscation\":\"IS5u0zHGO1f3DZ\",\"output_index\":0,\"sequence_number\":14}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"-\",\"item_id\":\"msg_0cd823aeeb11ef0e016a6108c83f8081938114e89ac5d99c7e\",\"logprobs\":[],\"obfuscation\":\"sQxTFLJOfQQxxil\",\"output_index\":0,\"sequence_number\":15}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"739\",\"item_id\":\"msg_0cd823aeeb11ef0e016a6108c83f8081938114e89ac5d99c7e\",\"logprobs\":[],\"obfuscation\":\"iIJRJFEo0kyEU\",\"output_index\":0,\"sequence_number\":16}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"1\",\"item_id\":\"msg_0cd823aeeb11ef0e016a6108c83f8081938114e89ac5d99c7e\",\"logprobs\":[],\"obfuscation\":\"lwMVw41zdSRsVxm\",\"output_index\":0,\"sequence_number\":17}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_0cd823aeeb11ef0e016a6108c83f8081938114e89ac5d99c7e\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":18,\"text\":\"The verification code from the PDF is: ORCHID-7391\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_0cd823aeeb11ef0e016a6108c83f8081938114e89ac5d99c7e\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"The verification code from the PDF is: ORCHID-7391\"},\"sequence_number\":19}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_0cd823aeeb11ef0e016a6108c83f8081938114e89ac5d99c7e\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"The verification code from the PDF is: ORCHID-7391\"}],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":20}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_0cd823aeeb11ef0e016a6108c703b88193872d736c48d60e2c\",\"object\":\"response\",\"created_at\":1784744135,\"status\":\"completed\",\"background\":false,\"completed_at\":1784744136,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[{\"id\":\"msg_0cd823aeeb11ef0e016a6108c83f8081938114e89ac5d99c7e\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"The verification code from the PDF is: ORCHID-7391\"}],\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":0.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Read the attached PDF.\",\"name\":\"read_pdf\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false},\"strict\":false}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":107,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":16,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":123},\"user\":null,\"metadata\":{}},\"sequence_number\":21}\n\n" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/pdf/openai-user-input.json b/packages/ai/test/fixtures/recordings/pdf/openai-user-input.json new file mode 100644 index 000000000000..ab2b4a9dd774 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/pdf/openai-user-input.json @@ -0,0 +1,34 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:pdf", + "pdf", + "provider:openai", + "protocol:openai-responses", + "user-input" + ], + "name": "pdf/openai-user-input", + "recordedAt": "2026-07-22T18:15:34.867Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.openai.com/v1/responses", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"gpt-4o-mini\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_file\",\"filename\":\"verification.pdf\",\"file_data\":\"data:application/pdf;base64,JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiAvQ29udGVudHMgNCAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCA3NSA+PgpzdHJlYW0KQlQKL0YxIDE4IFRmCjcyIDcyMCBUZAooUERGIGNhc3NldHRlIHZlcmlmaWNhdGlvbiBjb2RlOiBPUkNISUQtNzM5MSkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UeXBlMSAvQmFzZUZvbnQgL0hlbHZldGljYSA+PgplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQxIDAwMDAwIG4gCjAwMDAwMDAzNjUgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo0MzUKJSVFT0YK\"},{\"type\":\"input_text\",\"text\":\"Return only the verification code from the PDF.\"}]}],\"store\":false,\"max_output_tokens\":40,\"temperature\":0,\"stream\":true}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_098ccdfefefe7eee016a6108c55ad48194a250d21618f41130\",\"object\":\"response\",\"created_at\":1784744134,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":0.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_098ccdfefefe7eee016a6108c55ad48194a250d21618f41130\",\"object\":\"response\",\"created_at\":1784744134,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":0.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_098ccdfefefe7eee016a6108c6b6e48194bdd91a4de2cb7bbe\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_098ccdfefefe7eee016a6108c6b6e48194bdd91a4de2cb7bbe\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"OR\",\"item_id\":\"msg_098ccdfefefe7eee016a6108c6b6e48194bdd91a4de2cb7bbe\",\"logprobs\":[],\"obfuscation\":\"OT7X1BjsfOw0Y7\",\"output_index\":0,\"sequence_number\":4}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"CH\",\"item_id\":\"msg_098ccdfefefe7eee016a6108c6b6e48194bdd91a4de2cb7bbe\",\"logprobs\":[],\"obfuscation\":\"XpcQS0WDba72lQ\",\"output_index\":0,\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"ID\",\"item_id\":\"msg_098ccdfefefe7eee016a6108c6b6e48194bdd91a4de2cb7bbe\",\"logprobs\":[],\"obfuscation\":\"IfrmlWWbGSpVPM\",\"output_index\":0,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"-\",\"item_id\":\"msg_098ccdfefefe7eee016a6108c6b6e48194bdd91a4de2cb7bbe\",\"logprobs\":[],\"obfuscation\":\"Jpf9w1TEeBMJR6X\",\"output_index\":0,\"sequence_number\":7}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"739\",\"item_id\":\"msg_098ccdfefefe7eee016a6108c6b6e48194bdd91a4de2cb7bbe\",\"logprobs\":[],\"obfuscation\":\"J1beY5zFoug8n\",\"output_index\":0,\"sequence_number\":8}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"1\",\"item_id\":\"msg_098ccdfefefe7eee016a6108c6b6e48194bdd91a4de2cb7bbe\",\"logprobs\":[],\"obfuscation\":\"kG1Isnrwy5QR4QB\",\"output_index\":0,\"sequence_number\":9}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_098ccdfefefe7eee016a6108c6b6e48194bdd91a4de2cb7bbe\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":10,\"text\":\"ORCHID-7391\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_098ccdfefefe7eee016a6108c6b6e48194bdd91a4de2cb7bbe\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"ORCHID-7391\"},\"sequence_number\":11}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_098ccdfefefe7eee016a6108c6b6e48194bdd91a4de2cb7bbe\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"ORCHID-7391\"}],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":12}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_098ccdfefefe7eee016a6108c55ad48194a250d21618f41130\",\"object\":\"response\",\"created_at\":1784744134,\"status\":\"completed\",\"background\":false,\"completed_at\":1784744134,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"moderation\":null,\"output\":[{\"id\":\"msg_098ccdfefefe7eee016a6108c6b6e48194bdd91a4de2cb7bbe\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"ORCHID-7391\"}],\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"context\":null,\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":0.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":44,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":7,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":51},\"user\":null,\"metadata\":{}},\"sequence_number\":13}\n\n" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/pdf/xai-tool-result.json b/packages/ai/test/fixtures/recordings/pdf/xai-tool-result.json new file mode 100644 index 000000000000..71833b93a572 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/pdf/xai-tool-result.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:pdf", + "pdf", + "provider:xai", + "protocol:openai-responses", + "tool", + "tool-result" + ], + "name": "pdf/xai-tool-result", + "recordedAt": "2026-07-22T18:15:43.608Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.x.ai/v1/responses", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"grok-4.5\",\"input\":[{\"role\":\"system\",\"content\":\"Read the PDF returned by the tool and follow the user's response format exactly.\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Return only the verification code from the PDF.\"}]},{\"type\":\"function_call\",\"call_id\":\"call_pdf_1\",\"name\":\"read_pdf\",\"arguments\":\"{}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_pdf_1\",\"output\":[{\"type\":\"input_text\",\"text\":\"PDF read successfully\"},{\"type\":\"input_file\",\"filename\":\"verification.pdf\",\"file_data\":\"JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiAvQ29udGVudHMgNCAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCA3NSA+PgpzdHJlYW0KQlQKL0YxIDE4IFRmCjcyIDcyMCBUZAooUERGIGNhc3NldHRlIHZlcmlmaWNhdGlvbiBjb2RlOiBPUkNISUQtNzM5MSkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UeXBlMSAvQmFzZUZvbnQgL0hlbHZldGljYSA+PgplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQxIDAwMDAwIG4gCjAwMDAwMDAzNjUgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo0MzUKJSVFT0YK\",\"mime_type\":\"application/pdf\"}]}],\"tools\":[{\"type\":\"function\",\"name\":\"read_pdf\",\"description\":\"Read the attached PDF.\",\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false},\"strict\":false}],\"store\":false,\"max_output_tokens\":40,\"temperature\":0,\"stream\":true}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream" + }, + "body": "event: response.created\ndata: {\"sequence_number\":0,\"type\":\"response.created\",\"response\":{\"created_at\":0,\"completed_at\":null,\"id\":\"dab13d84-6060-9eb7-b898-cbe7a3475ad7\",\"max_output_tokens\":40,\"model\":\"grok-4.5\",\"object\":\"response\",\"output\":[],\"parallel_tool_calls\":true,\"previous_response_id\":null,\"reasoning\":{\"effort\":null,\"summary\":null},\"temperature\":0.0,\"text\":{\"format\":{\"type\":\"text\"}},\"tool_choice\":\"auto\",\"tools\":[{\"type\":\"function\",\"description\":\"Read the attached PDF.\",\"name\":\"read_pdf\",\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false},\"strict\":false}],\"top_p\":0.949999988079071,\"usage\":null,\"user\":null,\"incomplete_details\":null,\"status\":\"in_progress\",\"store\":false,\"metadata\":{\"system_fingerprint\":\"fp_a39489019fa99b6e\"},\"background\":false,\"service_tier\":\"default\",\"truncation\":\"disabled\",\"top_logprobs\":0,\"presence_penalty\":0.0,\"frequency_penalty\":0.0,\"prompt_cache_key\":null,\"max_tool_calls\":null,\"safety_identifier\":null,\"error\":null,\"instructions\":null}}\n\nevent: response.in_progress\ndata: {\"sequence_number\":1,\"type\":\"response.in_progress\",\"response\":{\"created_at\":0,\"completed_at\":null,\"id\":\"dab13d84-6060-9eb7-b898-cbe7a3475ad7\",\"max_output_tokens\":40,\"model\":\"grok-4.5\",\"object\":\"response\",\"output\":[],\"parallel_tool_calls\":true,\"previous_response_id\":null,\"reasoning\":{\"effort\":null,\"summary\":null},\"temperature\":0.0,\"text\":{\"format\":{\"type\":\"text\"}},\"tool_choice\":\"auto\",\"tools\":[{\"type\":\"function\",\"description\":\"Read the attached PDF.\",\"name\":\"read_pdf\",\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false},\"strict\":false}],\"top_p\":0.949999988079071,\"usage\":null,\"user\":null,\"incomplete_details\":null,\"status\":\"in_progress\",\"store\":false,\"metadata\":{\"system_fingerprint\":\"fp_a39489019fa99b6e\"},\"background\":false,\"service_tier\":\"default\",\"truncation\":\"disabled\",\"top_logprobs\":0,\"presence_penalty\":0.0,\"frequency_penalty\":0.0,\"prompt_cache_key\":null,\"max_tool_calls\":null,\"safety_identifier\":null,\"error\":null,\"instructions\":null}}\n\nevent: response.output_item.added\ndata: {\"sequence_number\":2,\"type\":\"response.output_item.added\",\"item\":{\"content\":[],\"id\":\"msg_dab13d84-6060-9eb7-b898-cbe7a3475ad7\",\"role\":\"assistant\",\"type\":\"message\",\"status\":\"in_progress\"},\"output_index\":0}\n\nevent: response.content_part.added\ndata: {\"sequence_number\":3,\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_dab13d84-6060-9eb7-b898-cbe7a3475ad7\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"text\":\"\",\"logprobs\":[],\"annotations\":[]}}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":4,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"OR\",\"item_id\":\"msg_dab13d84-6060-9eb7-b898-cbe7a3475ad7\",\"output_index\":0,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":5,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"CH\",\"item_id\":\"msg_dab13d84-6060-9eb7-b898-cbe7a3475ad7\",\"output_index\":0,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":6,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"ID\",\"item_id\":\"msg_dab13d84-6060-9eb7-b898-cbe7a3475ad7\",\"output_index\":0,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":7,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"-\",\"item_id\":\"msg_dab13d84-6060-9eb7-b898-cbe7a3475ad7\",\"output_index\":0,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":8,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"739\",\"item_id\":\"msg_dab13d84-6060-9eb7-b898-cbe7a3475ad7\",\"output_index\":0,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":9,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"1\",\"item_id\":\"msg_dab13d84-6060-9eb7-b898-cbe7a3475ad7\",\"output_index\":0,\"logprobs\":[]}\n\nevent: response.output_text.done\ndata: {\"sequence_number\":10,\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_dab13d84-6060-9eb7-b898-cbe7a3475ad7\",\"output_index\":0,\"text\":\"ORCHID-7391\",\"logprobs\":[]}\n\nevent: response.content_part.done\ndata: {\"sequence_number\":11,\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_dab13d84-6060-9eb7-b898-cbe7a3475ad7\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"text\":\"ORCHID-7391\",\"logprobs\":[],\"annotations\":[]}}\n\nevent: response.output_item.done\ndata: {\"sequence_number\":12,\"type\":\"response.output_item.done\",\"item\":{\"content\":[{\"type\":\"output_text\",\"text\":\"ORCHID-7391\",\"logprobs\":[],\"annotations\":[]}],\"id\":\"msg_dab13d84-6060-9eb7-b898-cbe7a3475ad7\",\"role\":\"assistant\",\"type\":\"message\",\"status\":\"completed\"},\"output_index\":0}\n\nevent: response.completed\ndata: {\"sequence_number\":13,\"type\":\"response.completed\",\"response\":{\"created_at\":0,\"completed_at\":1784744143,\"id\":\"dab13d84-6060-9eb7-b898-cbe7a3475ad7\",\"max_output_tokens\":40,\"model\":\"grok-4.5\",\"object\":\"response\",\"output\":[{\"content\":[{\"type\":\"output_text\",\"text\":\"ORCHID-7391\",\"logprobs\":[],\"annotations\":[]}],\"id\":\"msg_dab13d84-6060-9eb7-b898-cbe7a3475ad7\",\"role\":\"assistant\",\"type\":\"message\",\"status\":\"completed\"}],\"parallel_tool_calls\":true,\"previous_response_id\":null,\"reasoning\":{\"effort\":null,\"summary\":null},\"temperature\":0.0,\"text\":{\"format\":{\"type\":\"text\"}},\"tool_choice\":\"auto\",\"tools\":[{\"type\":\"function\",\"description\":\"Read the attached PDF.\",\"name\":\"read_pdf\",\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false},\"strict\":false}],\"top_p\":0.949999988079071,\"usage\":{\"input_tokens\":1592,\"input_tokens_details\":{\"cached_tokens\":128},\"output_tokens\":10,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":1602,\"num_sources_used\":0,\"num_server_side_tools_used\":0,\"cost_in_usd_ticks\":30264000,\"context_details\":{\"input_tokens\":1592,\"output_tokens\":11}},\"user\":null,\"incomplete_details\":null,\"status\":\"completed\",\"store\":false,\"metadata\":{\"system_fingerprint\":\"fp_a39489019fa99b6e\"},\"background\":false,\"service_tier\":\"default\",\"truncation\":\"disabled\",\"top_logprobs\":0,\"presence_penalty\":0.0,\"frequency_penalty\":0.0,\"prompt_cache_key\":null,\"max_tool_calls\":null,\"safety_identifier\":null,\"error\":null,\"instructions\":null}}\n\n" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/pdf/xai-user-input.json b/packages/ai/test/fixtures/recordings/pdf/xai-user-input.json new file mode 100644 index 000000000000..c3831f6e063e --- /dev/null +++ b/packages/ai/test/fixtures/recordings/pdf/xai-user-input.json @@ -0,0 +1,34 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:pdf", + "pdf", + "provider:xai", + "protocol:openai-responses", + "user-input" + ], + "name": "pdf/xai-user-input", + "recordedAt": "2026-07-22T18:15:42.429Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.x.ai/v1/responses", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"grok-4.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_file\",\"filename\":\"verification.pdf\",\"file_data\":\"JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiAvQ29udGVudHMgNCAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCA3NSA+PgpzdHJlYW0KQlQKL0YxIDE4IFRmCjcyIDcyMCBUZAooUERGIGNhc3NldHRlIHZlcmlmaWNhdGlvbiBjb2RlOiBPUkNISUQtNzM5MSkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UeXBlMSAvQmFzZUZvbnQgL0hlbHZldGljYSA+PgplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQxIDAwMDAwIG4gCjAwMDAwMDAzNjUgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo0MzUKJSVFT0YK\",\"mime_type\":\"application/pdf\"},{\"type\":\"input_text\",\"text\":\"Return only the verification code from the PDF.\"}]}],\"store\":false,\"max_output_tokens\":40,\"temperature\":0,\"stream\":true}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream" + }, + "body": "event: response.created\ndata: {\"sequence_number\":0,\"type\":\"response.created\",\"response\":{\"created_at\":0,\"completed_at\":null,\"id\":\"c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"max_output_tokens\":40,\"model\":\"grok-4.5\",\"object\":\"response\",\"output\":[],\"parallel_tool_calls\":true,\"previous_response_id\":null,\"reasoning\":{\"effort\":null,\"summary\":null},\"temperature\":0.0,\"text\":{\"format\":{\"type\":\"text\"}},\"tool_choice\":\"auto\",\"tools\":[],\"top_p\":0.949999988079071,\"usage\":null,\"user\":null,\"incomplete_details\":null,\"status\":\"in_progress\",\"store\":false,\"metadata\":{\"system_fingerprint\":\"fp_a39489019fa99b6e\"},\"background\":false,\"service_tier\":\"default\",\"truncation\":\"disabled\",\"top_logprobs\":0,\"presence_penalty\":0.0,\"frequency_penalty\":0.0,\"prompt_cache_key\":null,\"max_tool_calls\":null,\"safety_identifier\":null,\"error\":null,\"instructions\":null}}\n\nevent: response.in_progress\ndata: {\"sequence_number\":1,\"type\":\"response.in_progress\",\"response\":{\"created_at\":0,\"completed_at\":null,\"id\":\"c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"max_output_tokens\":40,\"model\":\"grok-4.5\",\"object\":\"response\",\"output\":[],\"parallel_tool_calls\":true,\"previous_response_id\":null,\"reasoning\":{\"effort\":null,\"summary\":null},\"temperature\":0.0,\"text\":{\"format\":{\"type\":\"text\"}},\"tool_choice\":\"auto\",\"tools\":[],\"top_p\":0.949999988079071,\"usage\":null,\"user\":null,\"incomplete_details\":null,\"status\":\"in_progress\",\"store\":false,\"metadata\":{\"system_fingerprint\":\"fp_a39489019fa99b6e\"},\"background\":false,\"service_tier\":\"default\",\"truncation\":\"disabled\",\"top_logprobs\":0,\"presence_penalty\":0.0,\"frequency_penalty\":0.0,\"prompt_cache_key\":null,\"max_tool_calls\":null,\"safety_identifier\":null,\"error\":null,\"instructions\":null}}\n\nevent: response.output_item.added\ndata: {\"sequence_number\":2,\"type\":\"response.output_item.added\",\"item\":{\"id\":\"rs_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"summary\":[],\"type\":\"reasoning\",\"status\":\"in_progress\"},\"output_index\":0}\n\nevent: response.reasoning_summary_part.added\ndata: {\"sequence_number\":3,\"type\":\"response.reasoning_summary_part.added\",\"item_id\":\"rs_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":0,\"part\":{\"text\":\"\",\"type\":\"summary_text\"},\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":4,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\"The\",\"item_id\":\"rs_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":5,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" user\",\"item_id\":\"rs_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":6,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" wants\",\"item_id\":\"rs_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":7,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" me\",\"item_id\":\"rs_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":8,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" to\",\"item_id\":\"rs_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":9,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" return\",\"item_id\":\"rs_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":10,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" only\",\"item_id\":\"rs_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":11,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" the\",\"item_id\":\"rs_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":12,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" verification\",\"item_id\":\"rs_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":13,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" code\",\"item_id\":\"rs_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":14,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" from\",\"item_id\":\"rs_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":15,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" the\",\"item_id\":\"rs_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":16,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\" PDF\",\"item_id\":\"rs_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.delta\ndata: {\"sequence_number\":17,\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\".\\n\",\"item_id\":\"rs_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":0,\"summary_index\":0}\n\nevent: response.reasoning_summary_text.done\ndata: {\"sequence_number\":18,\"type\":\"response.reasoning_summary_text.done\",\"item_id\":\"rs_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":0,\"summary_index\":0,\"text\":\"The user wants me to return only the verification code from the PDF.\\n\"}\n\nevent: response.reasoning_summary_part.done\ndata: {\"sequence_number\":19,\"type\":\"response.reasoning_summary_part.done\",\"item_id\":\"rs_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":0,\"part\":{\"text\":\"The user wants me to return only the verification code from the PDF.\\n\",\"type\":\"summary_text\"},\"summary_index\":0}\n\nevent: response.output_item.done\ndata: {\"sequence_number\":20,\"type\":\"response.output_item.done\",\"item\":{\"id\":\"rs_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"summary\":[{\"text\":\"The user wants me to return only the verification code from the PDF.\\n\",\"type\":\"summary_text\"}],\"type\":\"reasoning\",\"status\":\"completed\"},\"output_index\":0}\n\nevent: response.output_item.added\ndata: {\"sequence_number\":21,\"type\":\"response.output_item.added\",\"item\":{\"content\":[],\"id\":\"msg_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"role\":\"assistant\",\"type\":\"message\",\"status\":\"in_progress\"},\"output_index\":1}\n\nevent: response.content_part.added\ndata: {\"sequence_number\":22,\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"text\":\"\",\"logprobs\":[],\"annotations\":[]}}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":23,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"OR\",\"item_id\":\"msg_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":24,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"CH\",\"item_id\":\"msg_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":25,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"ID\",\"item_id\":\"msg_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":26,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"-\",\"item_id\":\"msg_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":27,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"739\",\"item_id\":\"msg_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.delta\ndata: {\"sequence_number\":28,\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"1\",\"item_id\":\"msg_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":1,\"logprobs\":[]}\n\nevent: response.output_text.done\ndata: {\"sequence_number\":29,\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":1,\"text\":\"ORCHID-7391\",\"logprobs\":[]}\n\nevent: response.content_part.done\ndata: {\"sequence_number\":30,\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"text\":\"ORCHID-7391\",\"logprobs\":[],\"annotations\":[]}}\n\nevent: response.output_item.done\ndata: {\"sequence_number\":31,\"type\":\"response.output_item.done\",\"item\":{\"content\":[{\"type\":\"output_text\",\"text\":\"ORCHID-7391\",\"logprobs\":[],\"annotations\":[]}],\"id\":\"msg_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"role\":\"assistant\",\"type\":\"message\",\"status\":\"completed\"},\"output_index\":1}\n\nevent: response.completed\ndata: {\"sequence_number\":32,\"type\":\"response.completed\",\"response\":{\"created_at\":0,\"completed_at\":1784744142,\"id\":\"c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"max_output_tokens\":40,\"model\":\"grok-4.5\",\"object\":\"response\",\"output\":[{\"id\":\"rs_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"summary\":[{\"text\":\"The user wants me to return only the verification code from the PDF.\\n\",\"type\":\"summary_text\"}],\"type\":\"reasoning\",\"status\":\"completed\"},{\"content\":[{\"type\":\"output_text\",\"text\":\"ORCHID-7391\",\"logprobs\":[],\"annotations\":[]}],\"id\":\"msg_c69d4e2b-243a-909a-84b6-ee953321ef6c\",\"role\":\"assistant\",\"type\":\"message\",\"status\":\"completed\"}],\"parallel_tool_calls\":true,\"previous_response_id\":null,\"reasoning\":{\"effort\":null,\"summary\":null},\"temperature\":0.0,\"text\":{\"format\":{\"type\":\"text\"}},\"tool_choice\":\"auto\",\"tools\":[],\"top_p\":0.949999988079071,\"usage\":{\"input_tokens\":1522,\"input_tokens_details\":{\"cached_tokens\":128},\"output_tokens\":119,\"output_tokens_details\":{\"reasoning_tokens\":109},\"total_tokens\":1641,\"num_sources_used\":0,\"num_server_side_tools_used\":0,\"cost_in_usd_ticks\":35404000,\"context_details\":{\"input_tokens\":1522,\"output_tokens\":119}},\"user\":null,\"incomplete_details\":null,\"status\":\"completed\",\"store\":false,\"metadata\":{\"system_fingerprint\":\"fp_a39489019fa99b6e\"},\"background\":false,\"service_tier\":\"default\",\"truncation\":\"disabled\",\"top_logprobs\":0,\"presence_penalty\":0.0,\"frequency_penalty\":0.0,\"prompt_cache_key\":null,\"max_tool_calls\":null,\"safety_identifier\":null,\"error\":null,\"instructions\":null}}\n\n" + } + } + ] +} diff --git a/packages/ai/test/lib/tool-runtime.ts b/packages/ai/test/lib/tool-runtime.ts index 28ebc47c712b..55fce8e0010b 100644 --- a/packages/ai/test/lib/tool-runtime.ts +++ b/packages/ai/test/lib/tool-runtime.ts @@ -59,7 +59,12 @@ export const runTools = (options: RunOptions) => ...request.messages, Message.assistant(state.assistantContent), ...dispatched.map(([call, dispatched]) => - Message.tool({ id: call.id, name: call.name, result: dispatched.result }), + Message.tool({ + id: call.id, + name: call.name, + result: dispatched.result, + providerMetadata: call.providerMetadata, + }), ), ], }) diff --git a/packages/ai/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts index 69a27e6228db..30b3c1b6b861 100644 --- a/packages/ai/test/provider/anthropic-messages.test.ts +++ b/packages/ai/test/provider/anthropic-messages.test.ts @@ -235,9 +235,9 @@ describe("Anthropic Messages route", () => { }), ) - // Regression: screenshot/read tool results must stay structured so base64 - // image data is not JSON-stringified into `tool_result.content`. - it.effect("lowers image tool-result content as structured image blocks", () => + // Regression: read tool results must stay structured so base64 media data is + // not JSON-stringified into `tool_result.content`. + it.effect("lowers media tool-result content as structured blocks", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( LLM.request({ @@ -253,6 +253,7 @@ describe("Anthropic Messages route", () => { result: [ { type: "text", text: "Image read successfully" }, { type: "file", uri: "data:image/png;base64,AAECAw==", mime: "image/png" }, + { type: "file", uri: "data:application/pdf;base64,JVBERi0xLjQ=", mime: "application/pdf" }, ], }), ], @@ -263,6 +264,7 @@ describe("Anthropic Messages route", () => { expect(expectToolResult(prepared.body).content).toEqual([ { type: "text", text: "Image read successfully" }, { type: "image", source: { type: "base64", media_type: "image/png", data: "AAECAw==" } }, + { type: "document", source: { type: "base64", media_type: "application/pdf", data: "JVBERi0xLjQ=" } }, ]) }), ) @@ -292,7 +294,7 @@ describe("Anthropic Messages route", () => { }), ) - it.effect("rejects non-image media in tool-result content with a clear error", () => + it.effect("rejects unsupported media in tool-result content with a clear error", () => Effect.gen(function* () { const error = yield* LLMClient.prepare( LLM.request({ @@ -756,7 +758,7 @@ describe("Anthropic Messages route", () => { }), ) - it.effect("continues a conversation with user image content", () => + it.effect("continues a conversation with user media content", () => Effect.gen(function* () { const response = yield* LLMClient.generate( LLM.request({ @@ -766,6 +768,7 @@ describe("Anthropic Messages route", () => { Message.user([ { type: "text", text: "What is in this image?" }, { type: "media", mediaType: "image/png", data: "AAECAw==" }, + { type: "media", mediaType: "application/pdf", data: "JVBERi0xLjQ=", filename: "report.pdf" }, ]), ], }), @@ -781,6 +784,7 @@ describe("Anthropic Messages route", () => { content: [ { type: "text", text: "What is in this image?" }, { type: "image", source: { type: "base64", media_type: "image/png", data: "AAECAw==" } }, + { type: "document", source: { type: "base64", media_type: "application/pdf", data: "JVBERi0xLjQ=" } }, ], }, ], diff --git a/packages/ai/test/provider/bedrock-converse.test.ts b/packages/ai/test/provider/bedrock-converse.test.ts index a550d1eb90e8..59ca67333bf5 100644 --- a/packages/ai/test/provider/bedrock-converse.test.ts +++ b/packages/ai/test/provider/bedrock-converse.test.ts @@ -549,10 +549,12 @@ describe("Bedrock Converse route", () => { LLM.request({ id: "req_doc", model, + cache: "none", messages: [ Message.user([ + { type: "text", text: "Summarize these documents." }, { type: "media", mediaType: "application/pdf", data: "UERGREFUQQ==", filename: "report.pdf" }, - { type: "media", mediaType: "text/csv", data: "Q1NWREFUQQ==" }, + { type: "media", mediaType: "text/csv", data: "Q1NWREFUQQ==", filename: "data.csv" }, ]), ], }), @@ -563,10 +565,9 @@ describe("Bedrock Converse route", () => { { role: "user", content: [ - // Filename round-trips when supplied. + { text: "Summarize these documents." }, { document: { format: "pdf", name: "report.pdf", source: { bytes: "UERGREFUQQ==" } } }, - // Falls back to a stable placeholder when filename is missing. - { document: { format: "csv", name: "document.csv", source: { bytes: "Q1NWREFUQQ==" } } }, + { document: { format: "csv", name: "data.csv", source: { bytes: "Q1NWREFUQQ==" } } }, ], }, ], @@ -574,6 +575,96 @@ describe("Bedrock Converse route", () => { }), ) + it.effect("requires names for document media", () => + Effect.gen(function* () { + const error = yield* LLMClient.prepare( + LLM.request({ + model, + messages: [Message.user({ type: "media", mediaType: "application/pdf", data: "UERGREFUQQ==" })], + }), + ).pipe(Effect.flip) + + expect(error.message).toContain("document media requires a filename") + }), + ) + + it.effect("passes named document-only messages through for provider validation", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model, + cache: "none", + messages: [ + Message.user({ + type: "media", + mediaType: "application/pdf", + data: "UERGREFUQQ==", + filename: "report.pdf", + }), + ], + }), + ) + + expect(prepared.body.messages).toEqual([ + { + role: "user", + content: [{ document: { format: "pdf", name: "report.pdf", source: { bytes: "UERGREFUQQ==" } } }], + }, + ]) + }), + ) + + it.effect("lowers document media in tool results", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model, + messages: [ + Message.assistant([ToolCallPart.make({ id: "call_1", name: "read", input: { path: "report.pdf" } })]), + Message.tool({ + id: "call_1", + name: "read", + result: { + type: "content", + value: [ + { type: "text", text: "Read successfully" }, + { + type: "file", + uri: "data:application/pdf;base64,UERGREFUQQ==", + mime: "application/pdf", + name: "report", + }, + ], + }, + }), + ], + }), + ) + + expect(prepared.body.messages).toEqual([ + { + role: "assistant", + content: [{ toolUse: { toolUseId: "call_1", name: "read", input: { path: "report.pdf" } } }], + }, + { + role: "user", + content: [ + { + toolResult: { + toolUseId: "call_1", + status: "success", + content: [ + { text: "Read successfully" }, + { document: { format: "pdf", name: "report", source: { bytes: "UERGREFUQQ==" } } }, + ], + }, + }, + ], + }, + ]) + }), + ) + it.effect("rejects unsupported image media types", () => Effect.gen(function* () { const error = yield* LLMClient.prepare( diff --git a/packages/ai/test/provider/gemini.test.ts b/packages/ai/test/provider/gemini.test.ts index 1dc253c0ea88..5195d372c901 100644 --- a/packages/ai/test/provider/gemini.test.ts +++ b/packages/ai/test/provider/gemini.test.ts @@ -70,6 +70,7 @@ describe("Gemini route", () => { Message.user([ { type: "text", text: "What is in this image?" }, { type: "media", mediaType: "image/png", data: "AAECAw==" }, + { type: "media", mediaType: "application/pdf", data: "JVBERi0xLjQ=" }, ]), Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: { query: "weather" } })]), Message.tool({ id: "call_1", name: "lookup", result: { forecast: "sunny" } }), @@ -81,7 +82,11 @@ describe("Gemini route", () => { contents: [ { role: "user", - parts: [{ text: "What is in this image?" }, { inlineData: { mimeType: "image/png", data: "AAECAw==" } }], + parts: [ + { text: "What is in this image?" }, + { inlineData: { mimeType: "image/png", data: "AAECAw==" } }, + { inlineData: { mimeType: "application/pdf", data: "JVBERi0xLjQ=" } }, + ], }, { role: "model", @@ -90,7 +95,12 @@ describe("Gemini route", () => { { role: "user", parts: [ - { functionResponse: { name: "lookup", response: { name: "lookup", content: '{"forecast":"sunny"}' } } }, + { + functionResponse: { + name: "lookup", + response: { name: "lookup", content: '{"forecast":"sunny"}' }, + }, + }, ], }, ], @@ -110,7 +120,7 @@ describe("Gemini route", () => { }), ) - it.effect("continues image tool results as inline vision input without base64 text", () => + it.effect("continues media tool results as inline model input without base64 text", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( LLM.request({ @@ -125,6 +135,7 @@ describe("Gemini route", () => { value: [ { type: "text", text: "Image read successfully" }, { type: "file", uri: "data:image/png;base64,AAECAw==", mime: "image/png", name: "pixel.png" }, + { type: "file", uri: "data:application/pdf;base64,JVBERi0xLjQ=", mime: "application/pdf" }, ], }, }), @@ -141,9 +152,12 @@ describe("Gemini route", () => { functionResponse: { name: "read", response: { name: "read", content: "Image read successfully" }, + parts: [ + { inlineData: { mimeType: "image/png", data: "AAECAw==" } }, + { inlineData: { mimeType: "application/pdf", data: "JVBERi0xLjQ=" } }, + ], }, }, - { inlineData: { mimeType: "image/png", data: "AAECAw==" } }, ], }, ]) @@ -174,8 +188,13 @@ describe("Gemini route", () => { { role: "user", parts: [ - { functionResponse: { name: "read", response: { name: "read", content: "" } } }, - { inlineData: { mimeType: "image/jpeg", data: "/9j/" } }, + { + functionResponse: { + name: "read", + response: { name: "read", content: "" }, + parts: [{ inlineData: { mimeType: "image/jpeg", data: "/9j/" } }], + }, + }, ], }, ]) @@ -372,7 +391,10 @@ describe("Gemini route", () => { parts: [ { text: "thinking", thought: true }, { text: "", thought: true, thoughtSignature: "thought_sig" }, - { functionCall: { name: "lookup", args: { query: "weather" } }, thoughtSignature: "tool_sig" }, + { + functionCall: { id: "provider_call", name: "lookup", args: { query: "weather" } }, + thoughtSignature: "tool_sig", + }, ], }, finishReason: "STOP", @@ -398,7 +420,10 @@ describe("Gemini route", () => { id: "reasoning-0", providerMetadata: { google: { thoughtSignature: "thought_sig" } }, }) - expect(toolCall).toMatchObject({ providerMetadata: { google: { thoughtSignature: "tool_sig" } } }) + expect(toolCall).toMatchObject({ + id: "tool_0", + providerMetadata: { google: { functionCallId: "provider_call", thoughtSignature: "tool_sig" } }, + }) expect(response.events.findIndex((event) => event.type === "reasoning-end")).toBeLessThan( response.events.findIndex((event) => event.type === "tool-call"), ) @@ -416,6 +441,13 @@ describe("Gemini route", () => { providerMetadata: toolCall?.providerMetadata, }), ]), + Message.tool({ + id: "tool_0", + name: "lookup", + result: "done", + resultType: "text", + providerMetadata: toolCall?.providerMetadata, + }), ], }), ) @@ -424,7 +456,22 @@ describe("Gemini route", () => { role: "model", parts: [ { text: "thinking", thought: true, thoughtSignature: "thought_sig" }, - { functionCall: { name: "lookup", args: { query: "weather" } }, thoughtSignature: "tool_sig" }, + { + functionCall: { id: "provider_call", name: "lookup", args: { query: "weather" } }, + thoughtSignature: "tool_sig", + }, + ], + }, + { + role: "user", + parts: [ + { + functionResponse: { + id: "provider_call", + name: "lookup", + response: { name: "lookup", content: "done" }, + }, + }, ], }, ]) @@ -498,7 +545,7 @@ describe("Gemini route", () => { content: { role: "model", parts: [ - { functionCall: { name: "lookup", args: { query: "weather" } } }, + { functionCall: { id: "tool_0", name: "lookup", args: { query: "weather" } } }, { functionCall: { name: "lookup", args: { query: "news" } } }, ], }, @@ -513,7 +560,13 @@ describe("Gemini route", () => { ).pipe(Effect.provide(fixedResponse(body))) expect(response.toolCalls).toEqual([ - { type: "tool-call", id: "tool_0", name: "lookup", input: { query: "weather" } }, + { + type: "tool-call", + id: "tool_0", + name: "lookup", + input: { query: "weather" }, + providerMetadata: { google: { functionCallId: "tool_0" } }, + }, { type: "tool-call", id: "tool_1", name: "lookup", input: { query: "news" } }, ]) expect(response.events.at(-1)).toMatchObject({ type: "finish", reason: "tool-calls" }) diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index e345cd59b1f5..11466a6896f9 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -5,6 +5,7 @@ import { LLM, LLMError, LLMEvent, Message, Model, ToolCallPart, ToolResultPart, import { Auth, LLMClient, RequestExecutor, WebSocketExecutor } from "../../src/route" import * as Azure from "../../src/providers/azure" import * as OpenAI from "../../src/providers/openai" +import * as XAI from "../../src/providers/xai" import * as OpenAIResponses from "../../src/protocols/openai-responses" import * as ProviderShared from "../../src/protocols/shared" import { continuationRequest, nativeOpenAIResponsesContinuation } from "../continuation-scenarios" @@ -16,6 +17,8 @@ const model = OpenAIResponses.route .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) .model({ id: "gpt-4.1-mini" }) +const xaiModel = XAI.configure({ apiKey: "test", baseURL: "https://api.x.ai/v1" }).responses("grok-4.5") + const request = LLM.request({ id: "req_1", model, @@ -524,7 +527,77 @@ describe("OpenAI Responses route", () => { }), ) - it.effect("rejects non-image media in tool-result content with a clear error", () => + it.effect("lowers PDF tool-result content as structured input_file array", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + id: "req_tool_result_pdf", + model, + messages: [ + Message.assistant([ToolCallPart.make({ id: "call_1", name: "read", input: {} })]), + Message.tool({ + id: "call_1", + name: "read", + resultType: "content", + result: [ + { + type: "file", + uri: "data:application/pdf;base64,JVBERi0xLjQ=", + mime: "application/pdf", + name: "report.pdf", + }, + ], + }), + ], + }), + ) + + expect(expectToolOutput(prepared.body).output).toEqual([ + { + type: "input_file", + filename: "report.pdf", + file_data: "data:application/pdf;base64,JVBERi0xLjQ=", + }, + ]) + }), + ) + + it.effect("uses xAI inline file encoding for PDF tool results", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model: xaiModel, + messages: [ + Message.assistant([ToolCallPart.make({ id: "call_1", name: "read", input: {} })]), + Message.tool({ + id: "call_1", + name: "read", + resultType: "content", + result: [ + { + type: "file", + uri: "data:application/pdf;base64,JVBERi0xLjQ=", + mime: "application/pdf", + name: "report.pdf", + }, + ], + }), + ], + }), + ) + + expect(expectToolOutput(prepared.body).output).toEqual([ + { + type: "input_file", + filename: "report.pdf", + file_data: "JVBERi0xLjQ=", + mime_type: "application/pdf", + }, + ]) + }), + ) + + it.effect("rejects unsupported media in tool-result content with a clear error", () => Effect.gen(function* () { const error = yield* LLMClient.prepare( LLM.request({ @@ -1526,20 +1599,64 @@ describe("OpenAI Responses route", () => { }), ) - it.effect("lowers user image content", () => + it.effect("lowers user image and PDF content", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( LLM.request({ id: "req_media", model, - messages: [Message.user({ type: "media", mediaType: "image/png", data: "AAECAw==" })], + messages: [ + Message.user([ + { type: "media", mediaType: "image/png", data: "AAECAw==" }, + { type: "media", mediaType: "application/pdf", data: "JVBERi0xLjQ=", filename: "report.pdf" }, + ]), + ], + }), + ) + + expect(prepared.body.input).toEqual([ + { + role: "user", + content: [ + { type: "input_image", image_url: "data:image/png;base64,AAECAw==" }, + { + type: "input_file", + filename: "report.pdf", + file_data: "data:application/pdf;base64,JVBERi0xLjQ=", + }, + ], + }, + ]) + }), + ) + + it.effect("uses xAI inline file encoding for user PDFs", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model: xaiModel, + messages: [ + Message.user({ + type: "media", + mediaType: "application/pdf", + data: "data:application/pdf;base64,JVBERi0xLjQ=", + filename: "report.pdf", + }), + ], }), ) expect(prepared.body.input).toEqual([ { role: "user", - content: [{ type: "input_image", image_url: "data:image/png;base64,AAECAw==" }], + content: [ + { + type: "input_file", + filename: "report.pdf", + file_data: "JVBERi0xLjQ=", + mime_type: "application/pdf", + }, + ], }, ]) }), @@ -1551,11 +1668,11 @@ describe("OpenAI Responses route", () => { LLM.request({ id: "req_media", model, - messages: [Message.user({ type: "media", mediaType: "application/pdf", data: "AAECAw==" })], + messages: [Message.user({ type: "media", mediaType: "application/x-tar", data: "AAECAw==" })], }), ).pipe(Effect.flip) - expect(error.message).toContain("OpenAI Responses does not support media type application/pdf") + expect(error.message).toContain("OpenAI Responses does not support media type application/x-tar") }), ) diff --git a/packages/ai/test/provider/pdf.recorded.test.ts b/packages/ai/test/provider/pdf.recorded.test.ts new file mode 100644 index 000000000000..c3827a651c1f --- /dev/null +++ b/packages/ai/test/provider/pdf.recorded.test.ts @@ -0,0 +1,207 @@ +import { describe, expect } from "bun:test" +import { Effect, Schema, Stream } from "effect" +import { LLM, LLMResponse, Message, ToolDefinition, type Model } from "../../src" +import { AmazonBedrock, Anthropic, Google, OpenAI, XAI } from "../../src/providers" +import { LLMClient } from "../../src/route" +import { Tool } from "../../src/tool" +import { runTools } from "../lib/tool-runtime" +import { recordedTests } from "../recorded-test" + +const CODE = "ORCHID-7391" +const PDF = + "JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiAvQ29udGVudHMgNCAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCA3NSA+PgpzdHJlYW0KQlQKL0YxIDE4IFRmCjcyIDcyMCBUZAooUERGIGNhc3NldHRlIHZlcmlmaWNhdGlvbiBjb2RlOiBPUkNISUQtNzM5MSkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UeXBlMSAvQmFzZUZvbnQgL0hlbHZldGljYSA+PgplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQxIDAwMDAwIG4gCjAwMDAwMDAzNjUgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo0MzUKJSVFT0YK" + +const openai = OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY ?? "fixture" }) +const anthropic = Anthropic.configure({ apiKey: process.env.ANTHROPIC_API_KEY ?? "fixture" }) +const google = Google.configure({ apiKey: process.env.GOOGLE_API_KEY ?? "fixture" }) +const xai = XAI.configure({ apiKey: process.env.XAI_API_KEY ?? "fixture" }) +const bedrock = AmazonBedrock.configure({ + apiKey: process.env.AWS_BEDROCK_API_KEY ?? "fixture", + region: process.env.AWS_REGION ?? "us-east-1", +}) + +const targets: ReadonlyArray<{ + readonly id: string + readonly name: string + readonly provider: string + readonly protocol: string + readonly requires: string + readonly filename: string + readonly maxTokens: number + readonly model: Model +}> = [ + { + id: "openai", + name: "OpenAI Responses gpt-4o-mini", + provider: "openai", + protocol: "openai-responses", + requires: "OPENAI_API_KEY", + filename: "verification.pdf", + maxTokens: 40, + model: openai.responses("gpt-4o-mini"), + }, + { + id: "anthropic", + name: "Anthropic Haiku 4.5", + provider: "anthropic", + protocol: "anthropic-messages", + requires: "ANTHROPIC_API_KEY", + filename: "verification.pdf", + maxTokens: 40, + model: anthropic.model("claude-haiku-4-5-20251001"), + }, + { + id: "gemini", + name: "Gemini 3.5 Flash", + provider: "google", + protocol: "gemini", + requires: "GOOGLE_API_KEY", + filename: "verification.pdf", + maxTokens: 256, + model: google.model("gemini-3.5-flash"), + }, + { + id: "xai", + name: "xAI Grok 4.5", + provider: "xai", + protocol: "openai-responses", + requires: "XAI_API_KEY", + filename: "verification.pdf", + maxTokens: 40, + model: xai.responses("grok-4.5"), + }, + { + id: "bedrock", + name: "Bedrock Claude Haiku 4.5", + provider: "amazon-bedrock", + protocol: "bedrock-converse", + requires: "AWS_BEDROCK_API_KEY", + filename: "verification", + maxTokens: 40, + model: bedrock.model("us.anthropic.claude-haiku-4-5-20251001-v1:0"), + }, +] + +const recorded = recordedTests({ prefix: "pdf", tags: ["pdf"] }) +const prompt = "Return only the verification code from the PDF." +const readPdf = ToolDefinition.make({ + name: "read_pdf", + description: "Read the attached PDF.", + inputSchema: { type: "object", properties: {}, additionalProperties: false }, +}) +const readPdfRuntime = Tool.make({ + description: readPdf.description, + parameters: Schema.Struct({ path: Schema.String }), + success: Schema.String, + execute: () => Effect.succeed("PDF read successfully"), + toModelOutput: () => [ + { type: "text", text: "PDF read successfully" }, + { + type: "file", + uri: `data:application/pdf;base64,${PDF}`, + mime: "application/pdf", + name: "verification.pdf", + }, + ], +}) + +const expectCode = (response: LLMResponse) => { + expect(response.finishReason).toBe("stop") + expect(response.text.toUpperCase()).toContain(CODE) +} + +describe("PDF recorded", () => { + for (const target of targets) { + recorded.effect.with( + `reads a user PDF with ${target.name}`, + { + id: `${target.id}-user-input`, + provider: target.provider, + protocol: target.protocol, + requires: [target.requires], + tags: ["user-input"], + }, + Effect.gen(function* () { + expectCode( + yield* LLMClient.generate( + LLM.request({ + id: `recorded_pdf_${target.id}_user_input`, + model: target.model, + cache: "none", + generation: { maxTokens: target.maxTokens, temperature: 0 }, + messages: [ + Message.user([ + { type: "media", mediaType: "application/pdf", data: PDF, filename: target.filename }, + { type: "text", text: prompt }, + ]), + ], + }), + ), + ) + }), + ) + + recorded.effect.with( + `reads a PDF tool result with ${target.name}`, + { + id: `${target.id}-tool-result`, + provider: target.provider, + protocol: target.protocol, + requires: [target.requires], + tags: ["tool", "tool-result"], + }, + Effect.gen(function* () { + if (target.id === "gemini") { + const events = Array.from( + yield* runTools({ + request: LLM.request({ + id: "recorded_pdf_gemini_tool_result", + model: target.model, + system: + "Call read_pdf exactly once with path verification.pdf, then reply only with the verification code from its PDF.", + prompt: "Use read_pdf with path verification.pdf and return the verification code.", + cache: "none", + generation: { maxTokens: target.maxTokens, temperature: 0 }, + }), + tools: { read_pdf: readPdfRuntime }, + }).pipe(Stream.runCollect), + ) + expect(events.at(-1)).toMatchObject({ type: "finish", reason: "stop" }) + expect(LLMResponse.text({ events }).toUpperCase()).toContain(CODE) + return + } + + expectCode( + yield* LLMClient.generate( + LLM.request({ + id: `recorded_pdf_${target.id}_tool_result`, + model: target.model, + system: "Read the PDF returned by the tool and follow the user's response format exactly.", + cache: "none", + generation: { maxTokens: target.maxTokens, temperature: 0 }, + messages: [ + Message.user(prompt), + Message.assistant([{ type: "tool-call", id: "call_pdf_1", name: readPdf.name, input: {} }]), + Message.tool({ + id: "call_pdf_1", + name: readPdf.name, + resultType: "content", + result: [ + { type: "text", text: "PDF read successfully" }, + { + type: "file", + uri: `data:application/pdf;base64,${PDF}`, + mime: "application/pdf", + name: target.filename, + }, + ], + }), + ], + tools: [readPdf], + }), + ), + ) + }), + ) + } +}) diff --git a/packages/ai/test/tool-runtime.test.ts b/packages/ai/test/tool-runtime.test.ts index c03a18bd8aa2..e6e97887193a 100644 --- a/packages/ai/test/tool-runtime.test.ts +++ b/packages/ai/test/tool-runtime.test.ts @@ -183,6 +183,51 @@ describe("LLMClient tools", () => { }), ) + it.effect("preserves provider metadata on dispatched tool results", () => + Effect.gen(function* () { + const tool = Tool.make({ + description: "Return text.", + parameters: Schema.Struct({}), + success: Schema.String, + execute: () => Effect.succeed("hello"), + }) + const providerMetadata = { google: { functionCallId: "provider_call" } } + const dispatched = yield* ToolRuntime.dispatch( + { tool }, + LLMEvent.toolCall({ id: "call_1", name: "tool", input: {}, providerMetadata }), + ) + + expect(dispatched.events).toEqual([ + LLMEvent.toolResult({ + id: "call_1", + name: "tool", + result: { type: "text", value: "hello" }, + output: { structured: "hello", content: [{ type: "text", text: "hello" }] }, + providerMetadata, + }), + ]) + + const failed = yield* ToolRuntime.dispatch( + {}, + LLMEvent.toolCall({ id: "call_2", name: "missing", input: {}, providerMetadata }), + ) + expect(failed.events).toEqual([ + LLMEvent.toolError({ + id: "call_2", + name: "missing", + message: "Unknown tool: missing", + providerMetadata, + }), + LLMEvent.toolResult({ + id: "call_2", + name: "missing", + result: { type: "error", value: "Unknown tool: missing" }, + providerMetadata, + }), + ]) + }), + ) + it.effect("uses the narrow default projection for encoded typed success", () => Effect.gen(function* () { const text = Tool.make({ From 88f572cfce310680affceeef4d8bfd075f2e4c66 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 22 Jul 2026 16:19:00 -0400 Subject: [PATCH 035/150] refactor(tui): migrate selection views to V2 theme (#38001) --- .../tui/src/component/dialog-session-list.tsx | 13 ++-- packages/tui/src/component/dialog-stash.tsx | 5 +- .../tui/src/component/prompt/autocomplete.tsx | 24 ++++-- packages/tui/src/ui/dialog-select.tsx | 78 ++++++++++++------- 4 files changed, 77 insertions(+), 43 deletions(-) diff --git a/packages/tui/src/component/dialog-session-list.tsx b/packages/tui/src/component/dialog-session-list.tsx index 04783790e46d..b1067fa85986 100644 --- a/packages/tui/src/component/dialog-session-list.tsx +++ b/packages/tui/src/component/dialog-session-list.tsx @@ -20,7 +20,7 @@ export function DialogSessionList() { const dialog = useDialog() const route = useRoute() const data = useData() - const { theme } = useTheme() + const { themeV2, mode } = useTheme().contextual("elevated") const client = useClient() const local = useLocal() const toast = useToast() @@ -109,12 +109,13 @@ export function DialogSessionList() { value: session.id, category, footer, - bg: deleting ? theme.error : undefined, + bg: deleting ? themeV2.background.action.destructive.focused : undefined, + fg: deleting ? themeV2.text.action.destructive.focused : undefined, gutter: data.session.family(session.id).some((id) => data.session.status(id) === "running") ? () => : slot === undefined ? undefined - : () => {slot}, + : () => {slot}, } } @@ -142,12 +143,14 @@ export function DialogSessionList() { }} emptyView={ - No sessions available + No sessions available } noMatchView={ - {searchState().message} + + {searchState().message} + } onMove={() => setToDelete(undefined)} diff --git a/packages/tui/src/component/dialog-stash.tsx b/packages/tui/src/component/dialog-stash.tsx index cefe315ee3a4..80aa75250cb1 100644 --- a/packages/tui/src/component/dialog-stash.tsx +++ b/packages/tui/src/component/dialog-stash.tsx @@ -29,7 +29,7 @@ function getStashPreview(input: string, maxLength: number = 50): string { export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) { const dialog = useDialog() const stash = usePromptStash() - const { theme } = useTheme() + const { themeV2 } = useTheme().contextual("elevated") const shortcuts = Keymap.useShortcuts() const [toDelete, setToDelete] = createSignal() @@ -45,7 +45,8 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) { title: isDeleting ? `Press ${shortcuts.get("stash.delete")} again to confirm` : getStashPreview(entry.prompt.text), - bg: isDeleting ? theme.error : undefined, + bg: isDeleting ? themeV2.background.action.destructive.focused : undefined, + fg: isDeleting ? themeV2.text.action.destructive.focused : undefined, value: index, description: getRelativeTime(entry.timestamp), footer: lineCount > 1 ? `~${lineCount} lines` : undefined, diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx index 5dd80ee39676..921d096819c9 100644 --- a/packages/tui/src/component/prompt/autocomplete.tsx +++ b/packages/tui/src/component/prompt/autocomplete.tsx @@ -12,7 +12,7 @@ import { getScrollAcceleration } from "../../util/scroll" import { useTuiPaths } from "../../context/runtime" import { useConfig } from "../../config" import { useLocation } from "../../context/location" -import { useTheme, selectedForeground } from "../../context/theme" +import { useTheme } from "../../context/theme" import { SplitBorder } from "../../ui/border" import { useTerminalDimensions } from "@opentui/solid" import { Locale } from "../../util/locale" @@ -57,7 +57,7 @@ export function Autocomplete(props: { const data = useData() const keymap = Keymap.use() const keymapCommands = Keymap.useCommands() - const { theme } = useTheme() + const { themeV2 } = useTheme().contextual("overlay") const dimensions = useTerminalDimensions() const frecency = useFrecency() const config = useConfig().data @@ -698,11 +698,11 @@ export function Autocomplete(props: { width={position().width} zIndex={100} {...SplitBorder} - borderColor={theme.border} + borderColor={themeV2.border.default} > (scroll = r)} - backgroundColor={theme.backgroundMenu} + backgroundColor={themeV2.background.default} height={height()} scrollbarOptions={{ visible: false }} scrollAcceleration={scrollAcceleration()} @@ -711,7 +711,9 @@ export function Autocomplete(props: { each={options()} fallback={ - {emptyMessage()} + + {emptyMessage()} + } > @@ -719,7 +721,7 @@ export function Autocomplete(props: { { setStore("input", "mouse") @@ -734,11 +736,17 @@ export function Autocomplete(props: { }} onMouseUp={() => select()} > - + {option().display} - + {" " + option().description?.trimStart()} diff --git a/packages/tui/src/ui/dialog-select.tsx b/packages/tui/src/ui/dialog-select.tsx index 257de75b179d..88248c92d34c 100644 --- a/packages/tui/src/ui/dialog-select.tsx +++ b/packages/tui/src/ui/dialog-select.tsx @@ -1,6 +1,6 @@ import { InputRenderable, RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core" import { Keymap, type KeymapCommand } from "../context/keymap" -import { useTheme, selectedForeground } from "../context/theme" +import { useTheme } from "../context/theme" import { entries, filter, flatMap, groupBy, pipe } from "remeda" import { batch, createEffect, createMemo, createSignal, For, Show, type JSX, on, onCleanup } from "solid-js" import { createStore } from "solid-js/store" @@ -74,6 +74,7 @@ export interface DialogSelectOption { categoryView?: JSX.Element disabled?: boolean bg?: RGBA + fg?: RGBA gutter?: () => JSX.Element margin?: JSX.Element onSelect?: (ctx: DialogContext) => void @@ -91,7 +92,7 @@ export function DialogSelect(props: DialogSelectProps) { type VisibleAction = (Action & { label: string }) | FooterHint const dialog = useDialog() - const { theme } = useTheme() + const { themeV2, mode } = useTheme().contextual("elevated") const config = useConfig().data const scrollAcceleration = createMemo(() => getScrollAcceleration(config)) @@ -517,29 +518,44 @@ export function DialogSelect(props: DialogSelectProps) { if (!isActionItem(action.item)) return ( - + {action.item.title}{" "} - {action.item.label} + {action.item.label} ) const item = action.item const active = createMemo(() => isActionFocused(item)) const disabled = createMemo(() => isActionDisabled(item)) - const fg = selectedForeground(theme) return ( trigger(item)} > {item.title} - {item.label} + + {" " + item.label} + ) } @@ -549,11 +565,11 @@ export function DialogSelect(props: DialogSelectProps) { {props.titleView ?? ( - + {props.title} )} - dialog.clear()}> + dialog.clear()}> esc @@ -567,9 +583,9 @@ export function DialogSelect(props: DialogSelectProps) { props.onFilter?.(e) }) }} - focusedBackgroundColor={theme.backgroundPanel} - cursorColor={theme.primary} - focusedTextColor={theme.textMuted} + focusedBackgroundColor={themeV2.background.formfield.focused} + cursorColor={themeV2.text.formfield.focused} + focusedTextColor={themeV2.text.formfield.focused} ref={(r) => { input = r input.traits = { status: "FILTER" } @@ -580,7 +596,7 @@ export function DialogSelect(props: DialogSelectProps) { }, 1) }} placeholder={props.placeholder ?? "Search"} - placeholderColor={theme.textMuted} + placeholderColor={themeV2.text.subdued} /> @@ -594,14 +610,14 @@ export function DialogSelect(props: DialogSelectProps) { fallback={ props.emptyView ?? ( - No items available + No items available ) } > {props.noMatchView ?? ( - No results found + No results found )} @@ -623,7 +639,10 @@ export function DialogSelect(props: DialogSelectProps) { + {category} } @@ -672,8 +691,8 @@ export function DialogSelect(props: DialogSelectProps) { backgroundColor={ active() ? actionFocused() - ? theme.backgroundElement - : (option.bg ?? theme.primary) + ? themeV2.background.surface.overlay + : (option.bg ?? themeV2.background.action.primary.focused) : RGBA.fromInts(0, 0, 0, 0) } > @@ -692,6 +711,7 @@ export function DialogSelect(props: DialogSelectProps) { active={active()} current={current()} muted={actionFocused()} + activeColor={option.fg} gutter={option.gutter} /> @@ -699,7 +719,7 @@ export function DialogSelect(props: DialogSelectProps) { {(detail) => ( {option.detailsWrap @@ -745,15 +765,15 @@ function Option(props: { titleWidth?: number truncateTitle?: boolean | "left" gutter?: () => JSX.Element + activeColor?: RGBA onMouseOver?: () => void }) { - const { theme } = useTheme() - const fg = selectedForeground(theme) + const { themeV2 } = useTheme().contextual("elevated") const text = createMemo(() => { - if (props.active && !props.muted) return fg - if (props.muted && (props.active || props.current)) return theme.textMuted - if (props.current) return theme.primary - return theme.text + if (props.active && !props.muted) return props.activeColor ?? themeV2.text.action.primary.focused + if (props.muted && (props.active || props.current)) return themeV2.text.subdued + if (props.current) return themeV2.text.formfield.selected + return themeV2.text.default }) return ( @@ -783,12 +803,14 @@ function Option(props: { ? Locale.truncateLeft(props.title, props.titleWidth ?? 61) : Locale.truncate(props.title, props.titleWidth ?? 61))} - {props.description} + + {" " + props.description} + - {props.footer} + {props.footer} From 5913c1db0bf7a5e7475b946f7681d7c3a77bcf2f Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:33:14 -0500 Subject: [PATCH 036/150] chore: merge dev into v2 (#38377) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: opencode-agent[bot] Co-authored-by: Frank Co-authored-by: Aiden Cline Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Dax Raad Co-authored-by: Dax Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Nabs Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Brendan Allan Co-authored-by: Victor Navarro Co-authored-by: Vladimir Glafirov Co-authored-by: AidenGeunGeun Co-authored-by: Mark Co-authored-by: Aiden Cline Co-authored-by: opencode Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com> Co-authored-by: Jay Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: BB84 <110078428+BB-84C@users.noreply.github.com> Co-authored-by: Dustin Deus Co-authored-by: Jack Co-authored-by: Sebastian Co-authored-by: Jérôme Benoit Co-authored-by: Test User Co-authored-by: Simon Klee Co-authored-by: Rahul A Mistry <149420892+ProdigyRahul@users.noreply.github.com> Co-authored-by: Qiping Li Co-authored-by: liqiping Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Matthias Reso <13337103+mreso@users.noreply.github.com> Co-authored-by: tobwen <1864057+tobwen@users.noreply.github.com> Co-authored-by: Daniel Polito Co-authored-by: opencode --- .../session-timeline-lifecycle-state.spec.ts | 17 +++++++++++++++++ .../src/pages/layout/project-avatar-state.ts | 12 +++++++----- packages/console/app/src/i18n/ar.ts | 8 ++++---- packages/console/app/src/i18n/br.ts | 8 ++++---- packages/console/app/src/i18n/da.ts | 8 ++++---- packages/console/app/src/i18n/de.ts | 8 ++++---- packages/console/app/src/i18n/en.ts | 8 ++++---- packages/console/app/src/i18n/es.ts | 8 ++++---- packages/console/app/src/i18n/fr.ts | 8 ++++---- packages/console/app/src/i18n/it.ts | 8 ++++---- packages/console/app/src/i18n/ja.ts | 8 ++++---- packages/console/app/src/i18n/ko.ts | 8 ++++---- packages/console/app/src/i18n/no.ts | 8 ++++---- packages/console/app/src/i18n/pl.ts | 8 ++++---- packages/console/app/src/i18n/ru.ts | 8 ++++---- packages/console/app/src/i18n/th.ts | 8 ++++---- packages/console/app/src/i18n/tr.ts | 8 ++++---- packages/console/app/src/i18n/uk.ts | 8 ++++---- packages/console/app/src/i18n/zh.ts | 8 ++++---- packages/console/app/src/i18n/zht.ts | 8 ++++---- packages/console/app/src/routes/go/index.tsx | 2 ++ .../routes/workspace/[id]/go/lite-section.tsx | 1 + .../session-ui/src/components/basic-tool.tsx | 5 +++-- .../session-ui/src/components/message-part.tsx | 3 ++- packages/web/src/content/docs/ar/go.mdx | 7 ++++++- packages/web/src/content/docs/bs/go.mdx | 7 ++++++- packages/web/src/content/docs/da/go.mdx | 7 ++++++- packages/web/src/content/docs/de/go.mdx | 7 ++++++- packages/web/src/content/docs/es/go.mdx | 7 ++++++- packages/web/src/content/docs/fr/go.mdx | 7 ++++++- packages/web/src/content/docs/go.mdx | 7 ++++++- packages/web/src/content/docs/it/go.mdx | 7 ++++++- packages/web/src/content/docs/ja/go.mdx | 7 ++++++- packages/web/src/content/docs/ko/go.mdx | 7 ++++++- packages/web/src/content/docs/nb/go.mdx | 7 ++++++- packages/web/src/content/docs/pl/go.mdx | 7 ++++++- packages/web/src/content/docs/pt-br/go.mdx | 7 ++++++- packages/web/src/content/docs/ru/go.mdx | 7 ++++++- packages/web/src/content/docs/th/go.mdx | 7 ++++++- packages/web/src/content/docs/tr/go.mdx | 7 ++++++- packages/web/src/content/docs/zh-cn/go.mdx | 7 ++++++- packages/web/src/content/docs/zh-tw/go.mdx | 7 ++++++- 42 files changed, 212 insertions(+), 98 deletions(-) diff --git a/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts b/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts index 3e2b171bca0a..b303071c87f7 100644 --- a/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts +++ b/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts @@ -32,6 +32,23 @@ for (const expanded of [false, true]) { }) } +test("shows and expands a running shell command without shimmering it", async ({ page }) => { + const id = "prt_shell_running_command" + const command = "sleep 10 && echo done" + await setupTimeline(page, { + messages: [userMessage(), assistantMessage([shell(id, "running", "still running", command)], { completed: false })], + settings: { shellToolPartsExpanded: false }, + }) + + const tool = page.locator(`[data-timeline-part-id="${id}"]`) + await expect(tool.locator('[data-component="text-shimmer"]')).toHaveAttribute("data-active", "true") + await expect(tool.locator('[data-component="shell-submessage"]')).toHaveText(command) + await expect(tool.locator('[data-component="shell-submessage"] [data-component="text-shimmer"]')).toHaveCount(0) + await tool.locator('[data-slot="collapsible-trigger"]').click() + await expect(tool.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true") + await expect(tool.locator('[data-slot="bash-pre"]')).toContainText("still running") +}) + test("transitions thinking and hidden reasoning through busy to idle", async ({ page }) => { const reasoningID = "prt_reasoning_hidden" const assistant = assistantMessage([reasoningPart(reasoningID, "## Inspecting stability")], { completed: false }) diff --git a/packages/app/src/pages/layout/project-avatar-state.ts b/packages/app/src/pages/layout/project-avatar-state.ts index 8e5dc38d671d..236f6bd40531 100644 --- a/packages/app/src/pages/layout/project-avatar-state.ts +++ b/packages/app/src/pages/layout/project-avatar-state.ts @@ -13,7 +13,6 @@ export function useSessionTabAvatarState( const global = useGlobal() const notification = useNotification() const permission = usePermission() - const permissionState = createMemo(() => permission.ensureServerState(server())) const connection = createMemo(() => global.servers.list().find((item) => ServerConnection.key(item) === server())) const sync = createMemo(() => { const conn = connection() @@ -22,9 +21,10 @@ export function useSessionTabAvatarState( const hasPermissions = createMemo(() => { const serverSync = sync() if (!serverSync) return false + const permissionState = permission.ensureServerState(server()) const [store] = serverSync.child(directory(), { bootstrap: false }) return !!sessionPermissionRequest(store.session, serverSync.session.data.permission, sessionId(), (item) => { - return !permissionState().autoResponds(item, directory()) + return !permissionState.autoResponds(item, directory()) }) }) const hasQuestions = createMemo(() => { @@ -34,9 +34,11 @@ export function useSessionTabAvatarState( return !!sessionQuestionRequest(store.session, serverSync.session.data.question, sessionId()) }) const needsAttention = createMemo(() => hasPermissions() || hasQuestions()) - const unread = createMemo( - () => needsAttention() || notification.ensureServerState(server()).session.unseenCount(sessionId()) > 0, - ) + const notificationState = createMemo(() => { + if (!connection()) return + return notification.ensureServerState(server()) + }) + const unread = createMemo(() => needsAttention() || (notificationState()?.session.unseenCount(sessionId()) ?? 0) > 0) const loading = createMemo(() => { const serverSync = sync() if (!serverSync) return false diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index 991f7fb2d33f..082e211e0bea 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -254,7 +254,7 @@ export const dict = { "go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع", "go.banner.text": "يحصل Kimi K3 على حدود استخدام مضاعفة لفترة محدودة", "go.meta.description": - "يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash.", + "يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash وHy3.", "go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع", "go.hero.body": "يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.", @@ -302,7 +302,7 @@ export const dict = { "go.problem.item2": "حدود سخية ووصول موثوق", "go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين", "go.problem.item4": - "يتضمن Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash", + "يتضمن Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash وHy3", "go.how.title": "كيف يعمل Go", "go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.", "go.how.step1.title": "أنشئ حسابًا", @@ -326,7 +326,7 @@ export const dict = { "go.faq.a2": "يتضمن Go النماذج المدرجة أدناه، مع حدود سخية وإتاحة موثوقة.", "go.faq.q3": "هل Go هو نفسه Zen؟", "go.faq.a3": - "لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash.", + "لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash وHy3.", "go.faq.q4": "كم تكلفة Go؟", "go.faq.a4.p1.beforePricing": "تكلفة Go", "go.faq.a4.p1.pricingLink": "$5 للشهر الأول", @@ -349,7 +349,7 @@ export const dict = { "go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟", "go.faq.a9": - "تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).", + "تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash وHy3 مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).", "zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.", "zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم", diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index 9bef420e85c8..69979b0a4419 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -258,7 +258,7 @@ export const dict = { "go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos", "go.banner.text": "Kimi K3 tem limites de uso 2x maiores por tempo limitado", "go.meta.description": - "O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.", + "O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash e Hy3.", "go.hero.title": "Modelos de codificação de baixo custo para todos", "go.hero.body": "O Go traz a codificação com agentes para programadores em todo o mundo. Oferecendo limites generosos e acesso confiável aos modelos de código aberto mais capazes, para que você possa construir com agentes poderosos sem se preocupar com custos ou disponibilidade.", @@ -307,7 +307,7 @@ export const dict = { "go.problem.item2": "Limites generosos e acesso confiável", "go.problem.item3": "Feito para o maior número possível de programadores", "go.problem.item4": - "Inclui Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash", + "Inclui Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash e Hy3", "go.how.title": "Como o Go funciona", "go.how.body": "O Go começa em $5 no primeiro mês, depois $10/mês. Você pode usá-lo com o OpenCode ou qualquer agente.", @@ -333,7 +333,7 @@ export const dict = { "go.faq.a2": "O Go inclui os modelos listados abaixo, com limites generosos e acesso confiável.", "go.faq.q3": "O Go é o mesmo que o Zen?", "go.faq.a3": - "Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.", + "Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash e Hy3.", "go.faq.q4": "Quanto custa o Go?", "go.faq.a4.p1.beforePricing": "O Go custa", "go.faq.a4.p1.pricingLink": "$5 no primeiro mês", @@ -357,7 +357,7 @@ export const dict = { "go.faq.q9": "Qual a diferença entre os modelos gratuitos e o Go?", "go.faq.a9": - "Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).", + "Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash e Hy3 com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).", "zen.api.error.rateLimitExceeded": "Limite de taxa excedido. Por favor, tente novamente mais tarde.", "zen.api.error.modelNotSupported": "Modelo {{model}} não suportado", diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index e7c8c8feaf83..43d8d51abb63 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -256,7 +256,7 @@ export const dict = { "go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle", "go.banner.text": "Kimi K3 får fordoblet brugsgrænse i en begrænset periode", "go.meta.description": - "Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.", + "Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash og Hy3.", "go.hero.title": "Kodningsmodeller til lav pris for alle", "go.hero.body": "Go bringer agentisk kodning til programmører over hele verden. Med generøse grænser og pålidelig adgang til de mest kapable open source-modeller, så du kan bygge med kraftfulde agenter uden at bekymre dig om omkostninger eller tilgængelighed.", @@ -304,7 +304,7 @@ export const dict = { "go.problem.item2": "Generøse grænser og pålidelig adgang", "go.problem.item3": "Bygget til så mange programmører som muligt", "go.problem.item4": - "Inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash", + "Inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash og Hy3", "go.how.title": "Hvordan Go virker", "go.how.body": "Go starter ved $5 for den første måned, derefter $10/måned. Du kan bruge det med OpenCode eller enhver agent.", @@ -330,7 +330,7 @@ export const dict = { "go.faq.a2": "Go inkluderer modellerne nedenfor med generøse grænser og pålidelig adgang.", "go.faq.q3": "Er Go det samme som Zen?", "go.faq.a3": - "Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.", + "Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash og Hy3.", "go.faq.q4": "Hvad koster Go?", "go.faq.a4.p1.beforePricing": "Go koster", "go.faq.a4.p1.pricingLink": "$5 første måned", @@ -353,7 +353,7 @@ export const dict = { "go.faq.q9": "Hvad er forskellen på gratis modeller og Go?", "go.faq.a9": - "Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).", + "Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash og Hy3 med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).", "zen.api.error.rateLimitExceeded": "Hastighedsgrænse overskredet. Prøv venligst igen senere.", "zen.api.error.modelNotSupported": "Model {{model}} understøttes ikke", diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index 465b24568f67..99446d92b084 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -258,7 +258,7 @@ export const dict = { "go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle", "go.banner.text": "Kimi K3 erhält für begrenzte Zeit 2x Nutzungslimits", "go.meta.description": - "Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro und DeepSeek V4 Flash.", + "Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash und Hy3.", "go.hero.title": "Kostengünstige Coding-Modelle für alle", "go.hero.body": "Go bringt Agentic Coding zu Programmierern auf der ganzen Welt. Mit großzügigen Limits und zuverlässigem Zugang zu den leistungsfähigsten Open-Source-Modellen, damit du mit leistungsstarken Agenten entwickeln kannst, ohne dir Gedanken über Kosten oder Verfügbarkeit zu machen.", @@ -306,7 +306,7 @@ export const dict = { "go.problem.item2": "Großzügige Limits und zuverlässiger Zugang", "go.problem.item3": "Für so viele Programmierer wie möglich gebaut", "go.problem.item4": - "Beinhaltet Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro und DeepSeek V4 Flash", + "Beinhaltet Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash und Hy3", "go.how.title": "Wie Go funktioniert", "go.how.body": "Go beginnt bei $5 für den ersten Monat, danach $10/Monat. Du kannst es mit OpenCode oder jedem Agenten nutzen.", @@ -332,7 +332,7 @@ export const dict = { "go.faq.a2": "Go umfasst die unten aufgeführten Modelle mit großzügigen Limits und zuverlässigem Zugriff.", "go.faq.q3": "Ist Go dasselbe wie Zen?", "go.faq.a3": - "Nein. Zen ist Pay-as-you-go, während Go bei $5 für den ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro und DeepSeek V4 Flash.", + "Nein. Zen ist Pay-as-you-go, während Go bei $5 für den ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash und Hy3.", "go.faq.q4": "Wie viel kostet Go?", "go.faq.a4.p1.beforePricing": "Go kostet", "go.faq.a4.p1.pricingLink": "$5 im ersten Monat", @@ -356,7 +356,7 @@ export const dict = { "go.faq.q9": "Was ist der Unterschied zwischen kostenlosen Modellen und Go?", "go.faq.a9": - "Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro und DeepSeek V4 Flash mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).", + "Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash und Hy3 mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).", "zen.api.error.rateLimitExceeded": "Ratenlimit überschritten. Bitte versuche es später erneut.", "zen.api.error.modelNotSupported": "Modell {{model}} wird nicht unterstützt", diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index 7d0531e6f058..690658c657a8 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -255,7 +255,7 @@ export const dict = { "go.title": "OpenCode Go | Low cost coding models for everyone", "go.banner.text": "Kimi K3 gets 2× usage limits for a limited time", "go.meta.description": - "Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, and DeepSeek V4 Flash.", + "Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, and Hy3.", "go.hero.title": "Low cost coding models for everyone", "go.hero.body": "Go brings agentic coding to programmers around the world. Offering generous limits and reliable access to the most capable open-source models, so you can build with powerful agents without worrying about cost or availability.", @@ -302,7 +302,7 @@ export const dict = { "go.problem.item2": "Generous limits and reliable access", "go.problem.item3": "Built for as many programmers as possible", "go.problem.item4": - "Includes Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, and DeepSeek V4 Flash", + "Includes Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, and Hy3", "go.how.title": "How Go works", "go.how.body": "Go starts at $5 for your first month, then $10/month. You can use it with OpenCode or any agent.", "go.how.step1.title": "Create an account", @@ -327,7 +327,7 @@ export const dict = { "go.faq.a2": "Go includes the models listed below, with generous limits and reliable access.", "go.faq.q3": "Is Go the same as Zen?", "go.faq.a3": - "No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to open-source models Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, and DeepSeek V4 Flash.", + "No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to open-source models Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, and Hy3.", "go.faq.q4": "How much does Go cost?", "go.faq.a4.p1.beforePricing": "Go costs", "go.faq.a4.p1.pricingLink": "$5 first month", @@ -351,7 +351,7 @@ export const dict = { "go.faq.q9": "What is the difference between free models and Go?", "go.faq.a9": - "Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go includes Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, and DeepSeek V4 Flash with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).", + "Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go includes Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, and Hy3 with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).", "zen.api.error.rateLimitExceeded": "Rate limit exceeded. Please try again later.", "zen.api.error.modelNotSupported": "Model {{model}} is not supported", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index 08d30aef68e2..bb1a44138f79 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -259,7 +259,7 @@ export const dict = { "go.title": "OpenCode Go | Modelos de programación de bajo coste para todos", "go.banner.text": "Kimi K3 tiene límites de uso 2x mayores por tiempo limitado", "go.meta.description": - "Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro y DeepSeek V4 Flash.", + "Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash y Hy3.", "go.hero.title": "Modelos de programación de bajo coste para todos", "go.hero.body": "Go lleva la programación agéntica a programadores de todo el mundo. Ofrece límites generosos y acceso fiable a los modelos de código abierto más capaces, para que puedas crear con agentes potentes sin preocuparte por el coste o la disponibilidad.", @@ -308,7 +308,7 @@ export const dict = { "go.problem.item2": "Límites generosos y acceso fiable", "go.problem.item3": "Creado para tantos programadores como sea posible", "go.problem.item4": - "Incluye Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro y DeepSeek V4 Flash", + "Incluye Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash y Hy3", "go.how.title": "Cómo funciona Go", "go.how.body": "Go comienza en $5 el primer mes, luego 10 $/mes. Puedes usarlo con OpenCode o cualquier agente.", "go.how.step1.title": "Crear una cuenta", @@ -333,7 +333,7 @@ export const dict = { "go.faq.a2": "Go incluye los modelos que se indican abajo, con límites generosos y acceso confiable.", "go.faq.q3": "¿Es Go lo mismo que Zen?", "go.faq.a3": - "No. Zen es pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a los modelos de código abierto Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro y DeepSeek V4 Flash.", + "No. Zen es pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a los modelos de código abierto Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash y Hy3.", "go.faq.q4": "¿Cuánto cuesta Go?", "go.faq.a4.p1.beforePricing": "Go cuesta", "go.faq.a4.p1.pricingLink": "$5 el primer mes", @@ -357,7 +357,7 @@ export const dict = { "go.faq.q9": "¿Cuál es la diferencia entre los modelos gratuitos y Go?", "go.faq.a9": - "Los modelos gratuitos incluyen Big Pickle más modelos promocionales disponibles en el momento, con una cuota de 200 solicitudes/día. Go incluye Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro y DeepSeek V4 Flash con cuotas de solicitud más altas aplicadas a través de ventanas móviles (5 horas, semanal y mensual), aproximadamente equivalente a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (los recuentos reales de solicitudes varían según el modelo y el uso).", + "Los modelos gratuitos incluyen Big Pickle más modelos promocionales disponibles en el momento, con una cuota de 200 solicitudes/día. Go incluye Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash y Hy3 con cuotas de solicitud más altas aplicadas a través de ventanas móviles (5 horas, semanal y mensual), aproximadamente equivalente a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (los recuentos reales de solicitudes varían según el modelo y el uso).", "zen.api.error.rateLimitExceeded": "Límite de tasa excedido. Por favor, inténtalo de nuevo más tarde.", "zen.api.error.modelNotSupported": "Modelo {{model}} no soportado", diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index a0b844419589..4d0ff288d696 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -260,7 +260,7 @@ export const dict = { "go.title": "OpenCode Go | Modèles de code à faible coût pour tous", "go.banner.text": "Kimi K3 bénéficie de limites d’utilisation 2x supérieures pour une durée limitée", "go.meta.description": - "Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro et DeepSeek V4 Flash.", + "Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash et Hy3.", "go.hero.title": "Modèles de code à faible coût pour tous", "go.hero.body": "Go apporte le codage agentique aux programmeurs du monde entier. Offrant des limites généreuses et un accès fiable aux modèles open source les plus capables, pour que vous puissiez construire avec des agents puissants sans vous soucier du coût ou de la disponibilité.", @@ -308,7 +308,7 @@ export const dict = { "go.problem.item2": "Limites généreuses et accès fiable", "go.problem.item3": "Conçu pour autant de programmeurs que possible", "go.problem.item4": - "Inclut Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro et DeepSeek V4 Flash", + "Inclut Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash et Hy3", "go.how.title": "Comment fonctionne Go", "go.how.body": "Go commence à $5 pour le premier mois, puis 10 $/mois. Vous pouvez l'utiliser avec OpenCode ou n'importe quel agent.", @@ -334,7 +334,7 @@ export const dict = { "go.faq.a2": "Go inclut les modèles ci-dessous, avec des limites généreuses et un accès fiable.", "go.faq.q3": "Est-ce que Go est la même chose que Zen ?", "go.faq.a3": - "Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable aux modèles open source Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro et DeepSeek V4 Flash.", + "Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable aux modèles open source Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash et Hy3.", "go.faq.q4": "Combien coûte Go ?", "go.faq.a4.p1.beforePricing": "Go coûte", "go.faq.a4.p1.pricingLink": "$5 le premier mois", @@ -357,7 +357,7 @@ export const dict = { "Oui, vous pouvez utiliser Go avec n'importe quel agent. Suivez les instructions de configuration dans votre agent de code préféré.", "go.faq.q9": "Quelle est la différence entre les modèles gratuits et Go ?", "go.faq.a9": - "Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro et DeepSeek V4 Flash avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).", + "Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash et Hy3 avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).", "zen.api.error.rateLimitExceeded": "Limite de débit dépassée. Veuillez réessayer plus tard.", "zen.api.error.modelNotSupported": "Modèle {{model}} non pris en charge", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index a5e37dfc60b9..effeb1fdb42d 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -256,7 +256,7 @@ export const dict = { "go.title": "OpenCode Go | Modelli di coding a basso costo per tutti", "go.banner.text": "Kimi K3 offre limiti di utilizzo 2x superiori per un periodo limitato", "go.meta.description": - "Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.", + "Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash e Hy3.", "go.hero.title": "Modelli di coding a basso costo per tutti", "go.hero.body": "Go porta il coding agentico ai programmatori di tutto il mondo. Offrendo limiti generosi e un accesso affidabile ai modelli open source più capaci, in modo da poter costruire con agenti potenti senza preoccuparsi dei costi o della disponibilità.", @@ -304,7 +304,7 @@ export const dict = { "go.problem.item2": "Limiti generosi e accesso affidabile", "go.problem.item3": "Costruito per il maggior numero possibile di programmatori", "go.problem.item4": - "Include Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash", + "Include Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash e Hy3", "go.how.title": "Come funziona Go", "go.how.body": "Go inizia a $5 per il primo mese, poi $10/mese. Puoi usarlo con OpenCode o qualsiasi agente.", "go.how.step1.title": "Crea un account", @@ -329,7 +329,7 @@ export const dict = { "go.faq.a2": "Go include i modelli elencati di seguito, con limiti generosi e accesso affidabile.", "go.faq.q3": "Go è lo stesso di Zen?", "go.faq.a3": - "No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e accesso affidabile ai modelli open source Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.", + "No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e accesso affidabile ai modelli open source Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash e Hy3.", "go.faq.q4": "Quanto costa Go?", "go.faq.a4.p1.beforePricing": "Go costa", "go.faq.a4.p1.pricingLink": "$5 il primo mese", @@ -353,7 +353,7 @@ export const dict = { "go.faq.q9": "Qual è la differenza tra i modelli gratuiti e Go?", "go.faq.a9": - "I modelli gratuiti includono Big Pickle più modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go include Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).", + "I modelli gratuiti includono Big Pickle più modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go include Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash e Hy3 con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).", "zen.api.error.rateLimitExceeded": "Limite di richieste superato. Riprova più tardi.", "zen.api.error.modelNotSupported": "Modello {{model}} non supportato", diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index aca480b71978..6dfd750c6add 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -255,7 +255,7 @@ export const dict = { "go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル", "go.banner.text": "Kimi K3の利用上限が期間限定で2倍に", "go.meta.description": - "Goは最初の月$5、その後$10/月で、Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashに対して5時間のゆとりあるリクエスト上限があります。", + "Goは最初の月$5、その後$10/月で、Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash、Hy3に対して5時間のゆとりあるリクエスト上限があります。", "go.hero.title": "すべての人のための低価格なコーディングモデル", "go.hero.body": "Goは、世界中のプログラマーにエージェント型コーディングをもたらします。最も高性能なオープンソースモデルへの十分な制限と安定したアクセスを提供し、コストや可用性を気にすることなく強力なエージェントで構築できます。", @@ -304,7 +304,7 @@ export const dict = { "go.problem.item2": "十分な制限と安定したアクセス", "go.problem.item3": "できるだけ多くのプログラマーのために構築", "go.problem.item4": - "Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashを含む", + "Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash、Hy3を含む", "go.how.title": "Goの仕組み", "go.how.body": "Goは最初の月$5、その後$10/月で始まります。OpenCodeまたは任意のエージェントで使えます。", "go.how.step1.title": "アカウントを作成", @@ -329,7 +329,7 @@ export const dict = { "go.faq.a2": "Go には、十分な利用上限と安定したアクセスを備えた、以下のモデルが含まれます。", "go.faq.q3": "GoはZenと同じですか?", "go.faq.a3": - "いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashのオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。", + "いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash、Hy3のオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。", "go.faq.q4": "Goの料金は?", "go.faq.a4.p1.beforePricing": "Goは", "go.faq.a4.p1.pricingLink": "最初の月$5", @@ -353,7 +353,7 @@ export const dict = { "go.faq.q9": "無料モデルとGoの違いは何ですか?", "go.faq.a9": - "無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGrok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashが含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。", + "無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGrok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash、Hy3が含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。", "zen.api.error.rateLimitExceeded": "レート制限を超えました。後でもう一度お試しください。", "zen.api.error.modelNotSupported": "モデル {{model}} はサポートされていません", diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index f1e2235d7eb3..a24e988d71e8 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -252,7 +252,7 @@ export const dict = { "go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델", "go.banner.text": "Kimi K3 사용 한도가 한시적으로 2배 확대됩니다", "go.meta.description": - "Go는 첫 달 $5, 이후 $10/월로 시작하며, Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash에 대해 넉넉한 5시간 요청 한도를 제공합니다.", + "Go는 첫 달 $5, 이후 $10/월로 시작하며, Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, Hy3에 대해 넉넉한 5시간 요청 한도를 제공합니다.", "go.hero.title": "모두를 위한 저비용 코딩 모델", "go.hero.body": "Go는 전 세계 프로그래머들에게 에이전트 코딩을 제공합니다. 가장 유능한 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공하므로, 비용이나 가용성 걱정 없이 강력한 에이전트로 빌드할 수 있습니다.", @@ -301,7 +301,7 @@ export const dict = { "go.problem.item2": "넉넉한 한도와 안정적인 액세스", "go.problem.item3": "가능한 한 많은 프로그래머를 위해 제작됨", "go.problem.item4": - "Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash 포함", + "Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, Hy3 포함", "go.how.title": "Go 작동 방식", "go.how.body": "Go는 첫 달 $5, 이후 $10/월로 시작합니다. OpenCode 또는 어떤 에이전트와도 함께 사용할 수 있습니다.", "go.how.step1.title": "계정 생성", @@ -325,7 +325,7 @@ export const dict = { "go.faq.a2": "Go에는 넉넉한 한도와 안정적인 액세스를 제공하는 아래 모델이 포함됩니다.", "go.faq.q3": "Go는 Zen과 같은가요?", "go.faq.a3": - "아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.", + "아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, Hy3 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.", "go.faq.q4": "Go 비용은 얼마인가요?", "go.faq.a4.p1.beforePricing": "Go 비용은", "go.faq.a4.p1.pricingLink": "첫 달 $5", @@ -348,7 +348,7 @@ export const dict = { "go.faq.q9": "무료 모델과 Go의 차이점은 무엇인가요?", "go.faq.a9": - "무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).", + "무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, Hy3를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).", "zen.api.error.rateLimitExceeded": "속도 제한을 초과했습니다. 나중에 다시 시도해 주세요.", "zen.api.error.modelNotSupported": "{{model}} 모델은 지원되지 않습니다", diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index bec0e0ce5ef2..b5ceff412c66 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -256,7 +256,7 @@ export const dict = { "go.title": "OpenCode Go | Rimelige kodemodeller for alle", "go.banner.text": "Kimi K3 får 2x bruksgrense i en begrenset periode", "go.meta.description": - "Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.", + "Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash og Hy3.", "go.hero.title": "Rimelige kodemodeller for alle", "go.hero.body": "Go bringer agent-koding til programmerere over hele verden. Med rause grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene, kan du bygge med kraftige agenter uten å bekymre deg for kostnader eller tilgjengelighet.", @@ -304,7 +304,7 @@ export const dict = { "go.problem.item2": "Rause grenser og pålitelig tilgang", "go.problem.item3": "Bygget for så mange programmerere som mulig", "go.problem.item4": - "Inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash", + "Inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash og Hy3", "go.how.title": "Hvordan Go fungerer", "go.how.body": "Go starter på $5 for den første måneden, deretter $10/måned. Du kan bruke det med OpenCode eller hvilken som helst agent.", @@ -330,7 +330,7 @@ export const dict = { "go.faq.a2": "Go inkluderer modellene nedenfor, med høye grenser og pålitelig tilgang.", "go.faq.q3": "Er Go det samme som Zen?", "go.faq.a3": - "Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til åpen kildekode-modellene Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.", + "Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til åpen kildekode-modellene Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash og Hy3.", "go.faq.q4": "Hva koster Go?", "go.faq.a4.p1.beforePricing": "Go koster", "go.faq.a4.p1.pricingLink": "$5 første måned", @@ -354,7 +354,7 @@ export const dict = { "go.faq.q9": "Hva er forskjellen mellom gratis modeller og Go?", "go.faq.a9": - "Gratis modeller inkluderer Big Pickle pluss kampanjemodeller tilgjengelig på det tidspunktet, med en kvote på 200 forespørsler/dag. Go inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash med høyere kvoter håndhevet over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).", + "Gratis modeller inkluderer Big Pickle pluss kampanjemodeller tilgjengelig på det tidspunktet, med en kvote på 200 forespørsler/dag. Go inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash og Hy3 med høyere kvoter håndhevet over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).", "zen.api.error.rateLimitExceeded": "Rate limit overskredet. Vennligst prøv igjen senere.", "zen.api.error.modelNotSupported": "Modell {{model}} støttes ikke", diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index 8be53855f375..3199606a8b32 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -257,7 +257,7 @@ export const dict = { "go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego", "go.banner.text": "Kimi K3 oferuje 2x wyższe limity użycia przez ograniczony czas", "go.meta.description": - "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro i DeepSeek V4 Flash.", + "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash i Hy3.", "go.hero.title": "Niskokosztowe modele do kodowania dla każdego", "go.hero.body": "Go udostępnia programowanie z agentami programistom na całym świecie. Oferuje hojne limity i niezawodny dostęp do najzdolniejszych modeli open source, dzięki czemu możesz budować za pomocą potężnych agentów, nie martwiąc się o koszty czy dostępność.", @@ -305,7 +305,7 @@ export const dict = { "go.problem.item2": "Hojne limity i niezawodny dostęp", "go.problem.item3": "Stworzony dla jak największej liczby programistów", "go.problem.item4": - "Zawiera Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro i DeepSeek V4 Flash", + "Zawiera Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash i Hy3", "go.how.title": "Jak działa Go", "go.how.body": "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc. Możesz go używać z OpenCode lub dowolnym agentem.", @@ -331,7 +331,7 @@ export const dict = { "go.faq.a2": "Go obejmuje poniższe modele z wysokimi limitami i niezawodnym dostępem.", "go.faq.q3": "Czy Go to to samo co Zen?", "go.faq.a3": - "Nie. Zen to model płatności za użycie, podczas gdy Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi limitami i niezawodnym dostępem do modeli open source Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro i DeepSeek V4 Flash.", + "Nie. Zen to model płatności za użycie, podczas gdy Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi limitami i niezawodnym dostępem do modeli open source Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash i Hy3.", "go.faq.q4": "Ile kosztuje Go?", "go.faq.a4.p1.beforePricing": "Go kosztuje", "go.faq.a4.p1.pricingLink": "$5 za pierwszy miesiąc", @@ -355,7 +355,7 @@ export const dict = { "go.faq.q9": "Jaka jest różnica między darmowymi modelami a Go?", "go.faq.a9": - "Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go zawiera Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro i DeepSeek V4 Flash z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), w przybliżeniu równoważnymi $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).", + "Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go zawiera Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash i Hy3 z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), w przybliżeniu równoważnymi $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).", "zen.api.error.rateLimitExceeded": "Przekroczono limit zapytań. Spróbuj ponownie później.", "zen.api.error.modelNotSupported": "Model {{model}} nie jest obsługiwany", diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index abe56bbb0337..821ed70e98b2 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -260,7 +260,7 @@ export const dict = { "go.title": "OpenCode Go | Недорогие модели для кодинга для всех", "go.banner.text": "Kimi K3 получает 2x лимиты использования на ограниченное время", "go.meta.description": - "Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash.", + "Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash и Hy3.", "go.hero.title": "Недорогие модели для кодинга для всех", "go.hero.body": "Go открывает доступ к агентам-программистам разработчикам по всему миру. Предлагая щедрые лимиты и надежный доступ к наиболее способным моделям с открытым исходным кодом, вы можете создавать проекты с мощными агентами, не беспокоясь о затратах или доступности.", @@ -309,7 +309,7 @@ export const dict = { "go.problem.item2": "Щедрые лимиты и надежный доступ", "go.problem.item3": "Создан для максимального числа программистов", "go.problem.item4": - "Включает Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash", + "Включает Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash и Hy3", "go.how.title": "Как работает Go", "go.how.body": "Go начинается с $5 за первый месяц, затем $10/месяц. Вы можете использовать его с OpenCode или любым агентом.", @@ -335,7 +335,7 @@ export const dict = { "go.faq.a2": "Go включает перечисленные ниже модели с щедрыми лимитами и надежным доступом.", "go.faq.q3": "Go — это то же самое, что и Zen?", "go.faq.a3": - "Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash.", + "Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash и Hy3.", "go.faq.q4": "Сколько стоит Go?", "go.faq.a4.p1.beforePricing": "Go стоит", "go.faq.a4.p1.pricingLink": "$5 за первый месяц", @@ -359,7 +359,7 @@ export const dict = { "go.faq.q9": "В чем разница между бесплатными моделями и Go?", "go.faq.a9": - "Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).", + "Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash и Hy3 с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).", "zen.api.error.rateLimitExceeded": "Превышен лимит запросов. Пожалуйста, попробуйте позже.", "zen.api.error.modelNotSupported": "Модель {{model}} не поддерживается", diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index a6069a1bed4d..2a68e94c0a31 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -255,7 +255,7 @@ export const dict = { "go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน", "go.banner.text": "Kimi K3 เพิ่มโควตาการใช้งานเป็น 2 เท่าในช่วงเวลาจำกัด", "go.meta.description": - "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash", + "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash และ Hy3", "go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน", "go.hero.body": "Go นำการเขียนโค้ดแบบเอเจนต์มาสู่นักเขียนโปรแกรมทั่วโลก เสนอขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดได้อย่างน่าเชื่อถือ เพื่อให้คุณสามารถสร้างสรรค์ด้วยเอเจนต์ที่ทรงพลังโดยไม่ต้องกังวลเรื่องค่าใช้จ่ายหรือความพร้อมใช้งาน", @@ -302,7 +302,7 @@ export const dict = { "go.problem.item2": "ขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้", "go.problem.item3": "สร้างขึ้นเพื่อโปรแกรมเมอร์จำนวนมากที่สุดเท่าที่จะเป็นไปได้", "go.problem.item4": - "รวมถึง Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash", + "รวมถึง Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash และ Hy3", "go.how.title": "Go ทำงานอย่างไร", "go.how.body": "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้", "go.how.step1.title": "สร้างบัญชี", @@ -327,7 +327,7 @@ export const dict = { "go.faq.a2": "Go รวมโมเดลด้านล่างนี้ พร้อมขีดจำกัดที่มากและการเข้าถึงที่เชื่อถือได้", "go.faq.q3": "Go เหมือนกับ Zen หรือไม่?", "go.faq.a3": - "ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash อย่างเชื่อถือได้", + "ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash และ Hy3 อย่างเชื่อถือได้", "go.faq.q4": "Go ราคาเท่าไหร่?", "go.faq.a4.p1.beforePricing": "Go ราคา", "go.faq.a4.p1.pricingLink": "$5 เดือนแรก", @@ -350,7 +350,7 @@ export const dict = { "go.faq.q9": "ความแตกต่างระหว่างโมเดลฟรีและ Go คืออะไร?", "go.faq.a9": - "โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)", + "โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash และ Hy3 ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)", "zen.api.error.rateLimitExceeded": "เกินขีดจำกัดอัตราการใช้งาน กรุณาลองใหม่ในภายหลัง", "zen.api.error.modelNotSupported": "ไม่รองรับโมเดล {{model}}", diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index 7d8bc49f5066..9bdcfeaeb446 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -258,7 +258,7 @@ export const dict = { "go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri", "go.banner.text": "Kimi K3 sınırlı bir süre için 2x kullanım limiti sunuyor", "go.meta.description": - "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro ve DeepSeek V4 Flash için cömert 5 saatlik istek limitleri sunar.", + "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash ve Hy3 için cömert 5 saatlik istek limitleri sunar.", "go.hero.title": "Herkes için düşük maliyetli kodlama modelleri", "go.hero.body": "Go, dünya çapındaki programcılara ajan tabanlı kodlama getiriyor. En yetenekli açık kaynaklı modellere cömert limitler ve güvenilir erişim sunarak, maliyet veya erişilebilirlik konusunda endişelenmeden güçlü ajanlarla geliştirme yapmanızı sağlar.", @@ -307,7 +307,7 @@ export const dict = { "go.problem.item2": "Cömert limitler ve güvenilir erişim", "go.problem.item3": "Mümkün olduğunca çok programcı için geliştirildi", "go.problem.item4": - "Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro ve DeepSeek V4 Flash içerir", + "Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash ve Hy3 içerir", "go.how.title": "Go nasıl çalışır?", "go.how.body": "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar. OpenCode veya herhangi bir ajanla kullanabilirsiniz.", @@ -333,7 +333,7 @@ export const dict = { "go.faq.a2": "Go, aşağıda listelenen modelleri cömert limitler ve güvenilir erişimle sunar.", "go.faq.q3": "Go, Zen ile aynı mı?", "go.faq.a3": - "Hayır. Zen kullandıkça öde modelidir, Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro ve DeepSeek V4 Flash açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.", + "Hayır. Zen kullandıkça öde modelidir, Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash ve Hy3 açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.", "go.faq.q4": "Go ne kadar?", "go.faq.a4.p1.beforePricing": "Go'nun maliyeti", "go.faq.a4.p1.pricingLink": "İlk ay $5", @@ -357,7 +357,7 @@ export const dict = { "go.faq.q9": "Ücretsiz modeller ve Go arasındaki fark nedir?", "go.faq.a9": - "Ücretsiz modeller, günlük 200 istek kotası ile Big Pickle ve o sırada mevcut olan promosyonel modelleri içerir. Go ise Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro ve DeepSeek V4 Flash modellerini; yuvarlanan pencereler (5 saatlik, haftalık ve aylık) üzerinden uygulanan daha yüksek istek kotalarıyla içerir. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).", + "Ücretsiz modeller, günlük 200 istek kotası ile Big Pickle ve o sırada mevcut olan promosyonel modelleri içerir. Go ise Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash ve Hy3 modellerini; yuvarlanan pencereler (5 saatlik, haftalık ve aylık) üzerinden uygulanan daha yüksek istek kotalarıyla içerir. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).", "zen.api.error.rateLimitExceeded": "İstek limiti aşıldı. Lütfen daha sonra tekrar deneyin.", "zen.api.error.modelNotSupported": "{{model}} modeli desteklenmiyor", diff --git a/packages/console/app/src/i18n/uk.ts b/packages/console/app/src/i18n/uk.ts index ee2405b65f2e..1dbb0be8afb2 100644 --- a/packages/console/app/src/i18n/uk.ts +++ b/packages/console/app/src/i18n/uk.ts @@ -257,7 +257,7 @@ export const dict = { "go.title": "OpenCode Go | Недорогі моделі кодування для всіх", "go.banner.text": "Kimi K3 отримує 2x ліміти використання протягом обмеженого часу", "go.meta.description": - "Go починається від $5 за перший місяць, потім $10/місяць, зі щедрими 5-годинними лімітами запитів для Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash.", + "Go починається від $5 за перший місяць, потім $10/місяць, зі щедрими 5-годинними лімітами запитів для Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash та Hy3.", "go.hero.title": "Недорогі моделі кодування для всіх", "go.hero.body": "Go надає агентне програмування програмістам у всьому світі, пропонуючи щедрі ліміти та надійний доступ до найкращих моделей з відкритим кодом.", @@ -305,7 +305,7 @@ export const dict = { "go.problem.item2": "Щедрі ліміти та надійний доступ", "go.problem.item3": "Створено для якомога більшої кількості програмістів", "go.problem.item4": - "Включає Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash", + "Включає Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash та Hy3", "go.how.title": "Як працює Go", "go.how.body": "Go починається від $5 за перший місяць, потім $10/місяць. Використовуйте з OpenCode або будь-яким агентом.", @@ -331,7 +331,7 @@ export const dict = { "go.faq.a2": "Go включає моделі, перелічені нижче, із щедрими лімітами та надійним доступом.", "go.faq.q3": "Чи Go те саме, що Zen?", "go.faq.a3": - "Ні. Zen — це плата за використання, тоді як Go починається від $5 за перший місяць, потім $10/місяць, із щедрими лімітами та надійним доступом до моделей з відкритим кодом Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash.", + "Ні. Zen — це плата за використання, тоді як Go починається від $5 за перший місяць, потім $10/місяць, із щедрими лімітами та надійним доступом до моделей з відкритим кодом Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash та Hy3.", "go.faq.q4": "Скільки коштує Go?", "go.faq.a4.p1.beforePricing": "Go коштує", "go.faq.a4.p1.pricingLink": "$5 за перший місяць", @@ -354,7 +354,7 @@ export const dict = { "go.faq.q9": "Яка різниця між безкоштовними моделями та Go?", "go.faq.a9": - "Безкоштовні моделі включають Big Pickle та акційні моделі з лімітом 200 запитів/день. Go включає Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash із вищими лімітами.", + "Безкоштовні моделі включають Big Pickle та акційні моделі з лімітом 200 запитів/день. Go включає Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash та Hy3 із вищими лімітами.", "zen.api.error.rateLimitExceeded": "Перевищено ліміт запитів. Спробуйте пізніше.", "zen.api.error.modelNotSupported": "Модель {{model}} не підтримується", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index cc8b6326f7ea..47e5ee8361c7 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -246,7 +246,7 @@ export const dict = { "go.title": "OpenCode Go | 人人可用的低成本编程模型", "go.banner.text": "Kimi K3 限时享受 2 倍使用额度", "go.meta.description": - "Go 首月 $5,之后 $10/月,提供对 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小时充裕请求额度。", + "Go 首月 $5,之后 $10/月,提供对 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash 和 Hy3 的 5 小时充裕请求额度。", "go.hero.title": "人人可用的低成本编程模型", "go.hero.body": "Go 将代理编程带给全世界的程序员。提供充裕的限额和对最强大的开源模型的可靠访问,让您可以利用强大的代理进行构建,而无需担心成本或可用性。", @@ -293,7 +293,7 @@ export const dict = { "go.problem.item2": "充裕的限额和可靠的访问", "go.problem.item3": "为尽可能多的程序员打造", "go.problem.item4": - "包含 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code、Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash", + "包含 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code、Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash 和 Hy3", "go.how.title": "Go 如何工作", "go.how.body": "Go 起价为首月 $5,之后 $10/月。您可以将其与 OpenCode 或任何代理搭配使用。", "go.how.step1.title": "创建账户", @@ -315,7 +315,7 @@ export const dict = { "go.faq.a2": "Go 包含下方列出的模型,提供充足的限额和可靠的访问。", "go.faq.q3": "Go 和 Zen 一样吗?", "go.faq.a3": - "不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的额度,并可可靠地访问 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等开源模型。", + "不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的额度,并可可靠地访问 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash 和 Hy3 等开源模型。", "go.faq.q4": "Go 多少钱?", "go.faq.a4.p1.beforePricing": "Go 费用为", "go.faq.a4.p1.pricingLink": "首月 $5", @@ -337,7 +337,7 @@ export const dict = { "go.faq.q9": "免费模型和 Go 之间的区别是什么?", "go.faq.a9": - "免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code、Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。", + "免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code、Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash 和 Hy3,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。", "zen.api.error.rateLimitExceeded": "超出速率限制。请稍后重试。", "zen.api.error.modelNotSupported": "不支持模型 {{model}}", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index 8612bb8dacd1..77c0e8e918ec 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -246,7 +246,7 @@ export const dict = { "go.title": "OpenCode Go | 低成本全民編碼模型", "go.banner.text": "Kimi K3 限時享有 2 倍使用額度", "go.meta.description": - "Go 首月 $5,之後 $10/月,提供對 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小時充裕請求額度。", + "Go 首月 $5,之後 $10/月,提供對 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash 和 Hy3 的 5 小時充裕請求額度。", "go.hero.title": "低成本全民編碼模型", "go.hero.body": "Go 將代理編碼帶給全世界的程式設計師。提供寬裕的限額以及對最強大開源模型的穩定存取,讓你可以使用強大的代理進行構建,而無需擔心成本或可用性。", @@ -293,7 +293,7 @@ export const dict = { "go.problem.item2": "寬裕的限額與穩定存取", "go.problem.item3": "專為盡可能多的程式設計師打造", "go.problem.item4": - "包含 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 與 DeepSeek V4 Flash", + "包含 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash 與 Hy3", "go.how.title": "Go 如何運作", "go.how.body": "Go 起價為首月 $5,之後 $10/月。您可以將其與 OpenCode 或任何代理搭配使用。", "go.how.step1.title": "建立帳號", @@ -315,7 +315,7 @@ export const dict = { "go.faq.a2": "Go 包含下方列出的模型,提供充足的額度與穩定的存取。", "go.faq.q3": "Go 與 Zen 一樣嗎?", "go.faq.a3": - "不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的額度,並可可靠地存取 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等開源模型。", + "不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的額度,並可可靠地存取 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash 和 Hy3 等開源模型。", "go.faq.q4": "Go 費用是多少?", "go.faq.a4.p1.beforePricing": "Go 費用為", "go.faq.a4.p1.pricingLink": "首月 $5", @@ -337,7 +337,7 @@ export const dict = { "go.faq.q9": "免費模型與 Go 有什麼區別?", "go.faq.a9": - "免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 與 DeepSeek V4 Flash,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。", + "免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash 與 Hy3,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。", "zen.api.error.rateLimitExceeded": "超出頻率限制。請稍後再試。", "zen.api.error.modelNotSupported": "不支援模型 {{model}}", diff --git a/packages/console/app/src/routes/go/index.tsx b/packages/console/app/src/routes/go/index.tsx index 9dedcdcbb4e1..2742f49ef4a0 100644 --- a/packages/console/app/src/routes/go/index.tsx +++ b/packages/console/app/src/routes/go/index.tsx @@ -38,6 +38,7 @@ const models = [ "MiniMax M2.7", "DeepSeek V4 Pro", "DeepSeek V4 Flash", + "Hy3", ] function LimitsGraph(props: { href: string }) { @@ -72,6 +73,7 @@ function LimitsGraph(props: { href: string }) { { id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", req: 3250, d: "240ms" }, { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", req: 3450, d: "270ms" }, { id: "qwen3.7-plus", name: "Qwen3.7 Plus", req: 4300, d: "300ms" }, + { id: "hy3", name: "Hy3", req: 4300, d: "320ms" }, { id: "mimo-v2.5", name: "MiMo-V2.5", req: 30100, d: "340ms" }, { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 31650, d: "340ms" }, ] diff --git a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx index 6704f926089e..88141656f31e 100644 --- a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx @@ -321,6 +321,7 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
  • DeepSeek V4 Flash
  • MiMo-V2.5
  • MiMo-V2.5-Pro
  • +
  • Hy3
  • {i18n.t("workspace.lite.promo.footer")}

    diff --git a/packages/session-ui/src/components/basic-tool.tsx b/packages/session-ui/src/components/basic-tool.tsx index a3ce5c13b5de..2b73db24ed12 100644 --- a/packages/session-ui/src/components/basic-tool.tsx +++ b/packages/session-ui/src/components/basic-tool.tsx @@ -32,6 +32,7 @@ export interface BasicToolProps { open?: boolean onOpenChange?: (open: boolean) => void forceOpen?: boolean + allowOpenWhilePending?: boolean defer?: boolean locked?: boolean animated?: boolean @@ -176,7 +177,7 @@ export function BasicTool(props: BasicToolProps) { }) const handleOpenChange = (value: boolean) => { - if (pending()) return + if (pending() && !props.allowOpenWhilePending) return if (props.locked && !value) return setOpen(value) } @@ -247,7 +248,7 @@ export function BasicTool(props: BasicToolProps) {
    - + diff --git a/packages/session-ui/src/components/message-part.tsx b/packages/session-ui/src/components/message-part.tsx index 9f7130916511..77d9a8c56a13 100644 --- a/packages/session-ui/src/components/message-part.tsx +++ b/packages/session-ui/src/components/message-part.tsx @@ -2123,13 +2123,14 @@ ToolRegistry.register({ (
    - +
    diff --git a/packages/web/src/content/docs/ar/go.mdx b/packages/web/src/content/docs/ar/go.mdx index 0597f3adf180..5698d4272444 100644 --- a/packages/web/src/content/docs/ar/go.mdx +++ b/packages/web/src/content/docs/ar/go.mdx @@ -64,6 +64,7 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** قد تتغير قائمة النماذج مع استمرارنا في اختبار نماذج جديدة وإضافتها. @@ -87,7 +88,7 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -98,6 +99,7 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | تستند التقديرات إلى أنماط الطلبات المرصودة: @@ -112,6 +114,7 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال - Qwen3.7 Max — ‏420 input، و66,000 cached، و200 output tokens لكل طلب - Qwen3.7 Plus — ‏500 input، و57,000 cached، و190 output tokens لكل طلب - Qwen3.6 Plus — ‏500 input، و57,000 cached، و190 output tokens لكل طلب +- Hy3 — ‏830 input، و71,500 cached، و295 output tokens لكل طلب - MiMo-V2.5 — ‏830 input، و71,500 cached، و295 output tokens لكل طلب - MiMo-V2.5-Pro — ‏790 input، و86,000 cached، و305 output tokens لكل طلب @@ -137,6 +140,7 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | يمكنك تتبّع استخدامك الحالي في **
    console**. @@ -188,6 +192,7 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | يستخدم [model id](/docs/config/#models) في إعدادات OpenCode لديك التنسيق `opencode-go/`. على سبيل المثال، بالنسبة إلى Kimi K3، ستستخدم `opencode-go/kimi-k3` في إعداداتك. diff --git a/packages/web/src/content/docs/bs/go.mdx b/packages/web/src/content/docs/bs/go.mdx index 26b5575a0134..c9ea860d3530 100644 --- a/packages/web/src/content/docs/bs/go.mdx +++ b/packages/web/src/content/docs/bs/go.mdx @@ -74,6 +74,7 @@ Trenutna lista modela uključuje: - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** Lista modela se može mijenjati dok testiramo i dodajemo nove. @@ -97,7 +98,7 @@ Tabela ispod pruža procijenjeni broj zahtjeva na osnovu tipičnih obrazaca kori | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -108,6 +109,7 @@ Tabela ispod pruža procijenjeni broj zahtjeva na osnovu tipičnih obrazaca kori | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | Procjene se zasnivaju na zapaženim obrascima zahtjeva: @@ -122,6 +124,7 @@ Procjene se zasnivaju na zapaženim obrascima zahtjeva: - Qwen3.7 Max — 420 ulaznih, 66,000 keširanih, 200 izlaznih tokena po zahtjevu - Qwen3.7 Plus — 500 ulaznih, 57,000 keširanih, 190 izlaznih tokena po zahtjevu - Qwen3.6 Plus — 500 ulaznih, 57,000 keširanih, 190 izlaznih tokena po zahtjevu +- Hy3 — 830 ulaznih, 71,500 keširanih, 295 izlaznih tokena po zahtjevu - MiMo-V2.5 — 830 ulaznih, 71,500 keširanih, 295 izlaznih tokena po zahtjevu - MiMo-V2.5-Pro — 790 ulaznih, 86,000 keširanih, 305 izlaznih tokena po zahtjevu @@ -147,6 +150,7 @@ Procjene se također zasnivaju na sljedećim cijenama po 1M tokena i mjesečnoj | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Svoju trenutnu potrošnju možete pratiti u **konzoli**. @@ -200,6 +204,7 @@ Također možete pristupiti Go modelima putem sljedećih API endpointa. | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | [Model id](/docs/config/#models) u vašoj OpenCode konfiguraciji koristi format `opencode-go/`. Na primjer, za Kimi K3, koristili biste diff --git a/packages/web/src/content/docs/da/go.mdx b/packages/web/src/content/docs/da/go.mdx index 280891deeed5..4256f4afad04 100644 --- a/packages/web/src/content/docs/da/go.mdx +++ b/packages/web/src/content/docs/da/go.mdx @@ -74,6 +74,7 @@ Den nuværende liste over modeller inkluderer: - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** Listen over modeller kan ændre sig, efterhånden som vi tester og tilføjer nye. @@ -97,7 +98,7 @@ Tabellen nedenfor giver et estimeret antal anmodninger baseret på typiske Go-fo | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -108,6 +109,7 @@ Tabellen nedenfor giver et estimeret antal anmodninger baseret på typiske Go-fo | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | Estimaterne er baseret på observerede anmodningsmønstre: @@ -122,6 +124,7 @@ Estimaterne er baseret på observerede anmodningsmønstre: - Qwen3.7 Max — 420 input, 66.000 cachelagrede, 200 output-tokens pr. anmodning - Qwen3.7 Plus — 500 input, 57.000 cachelagrede, 190 output-tokens pr. anmodning - Qwen3.6 Plus — 500 input, 57.000 cachelagrede, 190 output-tokens pr. anmodning +- Hy3 — 830 input, 71.500 cachelagrede, 295 output-tokens pr. anmodning - MiMo-V2.5 — 830 input, 71.500 cachelagrede, 295 output-tokens pr. anmodning - MiMo-V2.5-Pro — 790 input, 86.000 cachelagrede, 305 output-tokens pr. anmodning @@ -147,6 +150,7 @@ Estimaterne er også baseret på følgende priser pr. 1M tokens og det månedlig | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Du kan spore dit nuværende forbrug i **konsollen**. @@ -200,6 +204,7 @@ Du kan også få adgang til Go-modeller gennem følgende API-endpoints. | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | Dit [model id](/docs/config/#models) i din OpenCode config bruger formatet `opencode-go/`. For eksempel for Kimi K3, vil du diff --git a/packages/web/src/content/docs/de/go.mdx b/packages/web/src/content/docs/de/go.mdx index 2bb3f0b7429e..3de5f5f786f8 100644 --- a/packages/web/src/content/docs/de/go.mdx +++ b/packages/web/src/content/docs/de/go.mdx @@ -66,6 +66,7 @@ Die aktuelle Liste der Modelle umfasst: - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** Die Liste der Modelle kann sich ändern, während wir neue testen und hinzufügen. @@ -89,7 +90,7 @@ Die folgende Tabelle zeigt eine geschätzte Anzahl von Anfragen basierend auf ty | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -100,6 +101,7 @@ Die folgende Tabelle zeigt eine geschätzte Anzahl von Anfragen basierend auf ty | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | Die Schätzungen basieren auf beobachteten Anfragemustern: @@ -114,6 +116,7 @@ Die Schätzungen basieren auf beobachteten Anfragemustern: - Qwen3.7 Max — 420 Input-, 66.000 Cached-, 200 Output-Tokens pro Anfrage - Qwen3.7 Plus — 500 Input-, 57.000 Cached-, 190 Output-Tokens pro Anfrage - Qwen3.6 Plus — 500 Input-, 57.000 Cached-, 190 Output-Tokens pro Anfrage +- Hy3 — 830 Input-, 71.500 Cached-, 295 Output-Tokens pro Anfrage - MiMo-V2.5 — 830 Input-, 71.500 Cached-, 295 Output-Tokens pro Anfrage - MiMo-V2.5-Pro — 790 Input-, 86.000 Cached-, 305 Output-Tokens pro Anfrage @@ -139,6 +142,7 @@ Die Schätzungen basieren außerdem auf den folgenden Preisen pro 1M Tokens und | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Du kannst deine aktuelle Nutzung in der **Console** verfolgen. @@ -190,6 +194,7 @@ Du kannst auf die Go-Modelle auch über die folgenden API-Endpunkte zugreifen. | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | Die [Modell-ID](/docs/config/#models) in deiner OpenCode Config verwendet das Format `opencode-go/`. Für Kimi K3 würdest du beispielsweise `opencode-go/kimi-k3` in deiner Config verwenden. diff --git a/packages/web/src/content/docs/es/go.mdx b/packages/web/src/content/docs/es/go.mdx index 4aa1ca46e3f3..de0280336216 100644 --- a/packages/web/src/content/docs/es/go.mdx +++ b/packages/web/src/content/docs/es/go.mdx @@ -74,6 +74,7 @@ La lista actual de modelos incluye: - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** La lista de modelos puede cambiar a medida que probamos y agregamos otros nuevos. @@ -97,7 +98,7 @@ La siguiente tabla proporciona una cantidad estimada de peticiones basada en los | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -108,6 +109,7 @@ La siguiente tabla proporciona una cantidad estimada de peticiones basada en los | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | Las estimaciones se basan en los patrones de peticiones observados: @@ -122,6 +124,7 @@ Las estimaciones se basan en los patrones de peticiones observados: - Qwen3.7 Max — 420 tokens de entrada, 66,000 en caché, 200 tokens de salida por petición - Qwen3.7 Plus — 500 tokens de entrada, 57,000 en caché, 190 tokens de salida por petición - Qwen3.6 Plus — 500 tokens de entrada, 57,000 en caché, 190 tokens de salida por petición +- Hy3 — 830 tokens de entrada, 71,500 en caché, 295 tokens de salida por petición - MiMo-V2.5 — 830 tokens de entrada, 71,500 en caché, 295 tokens de salida por petición - MiMo-V2.5-Pro — 790 tokens de entrada, 86,000 en caché, 305 tokens de salida por petición @@ -147,6 +150,7 @@ Las estimaciones también se basan en los siguientes precios por 1M tokens y en | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Puedes realizar un seguimiento de tu uso actual en la **consola**. @@ -200,6 +204,7 @@ También puedes acceder a los modelos de Go a través de los siguientes endpoint | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | El [ID del modelo](/docs/config/#models) en tu configuración de OpenCode usa el formato `opencode-go/`. Por ejemplo, para Kimi K3, usarías diff --git a/packages/web/src/content/docs/fr/go.mdx b/packages/web/src/content/docs/fr/go.mdx index 95849616b0b8..fe1139d389f2 100644 --- a/packages/web/src/content/docs/fr/go.mdx +++ b/packages/web/src/content/docs/fr/go.mdx @@ -64,6 +64,7 @@ La liste actuelle des modèles comprend : - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** La liste des modèles peut changer au fur et à mesure que nous en testons et en ajoutons de nouveaux. @@ -87,7 +88,7 @@ Le tableau ci-dessous fournit une estimation du nombre de requêtes basée sur d | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -98,6 +99,7 @@ Le tableau ci-dessous fournit une estimation du nombre de requêtes basée sur d | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | Les estimations sont basées sur les schémas de requêtes observés : @@ -112,6 +114,7 @@ Les estimations sont basées sur les schémas de requêtes observés : - Qwen3.7 Max — 420 tokens en entrée, 66,000 en cache, 200 tokens en sortie par requête - Qwen3.7 Plus — 500 tokens en entrée, 57,000 en cache, 190 tokens en sortie par requête - Qwen3.6 Plus — 500 tokens en entrée, 57,000 en cache, 190 tokens en sortie par requête +- Hy3 — 830 tokens en entrée, 71,500 en cache, 295 tokens en sortie par requête - MiMo-V2.5 — 830 tokens en entrée, 71,500 en cache, 295 tokens en sortie par requête - MiMo-V2.5-Pro — 790 tokens en entrée, 86,000 en cache, 305 tokens en sortie par requête @@ -137,6 +140,7 @@ Les estimations sont également basées sur les prix suivants par 1M tokens et s | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Vous pouvez suivre votre utilisation actuelle dans la **console**. @@ -188,6 +192,7 @@ Vous pouvez également accéder aux modèles Go via les points de terminaison d' | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | L'[ID de modèle](/docs/config/#models) dans votre configuration OpenCode utilise le format `opencode-go/`. Par exemple, pour Kimi K3, vous utiliseriez `opencode-go/kimi-k3` dans votre configuration. diff --git a/packages/web/src/content/docs/go.mdx b/packages/web/src/content/docs/go.mdx index 8c46464086f7..bbd4225b9fa9 100644 --- a/packages/web/src/content/docs/go.mdx +++ b/packages/web/src/content/docs/go.mdx @@ -74,6 +74,7 @@ The current list of models includes: - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** The list of models may change as we test and add new ones. @@ -97,7 +98,7 @@ The table below provides an estimated request count based on typical Go usage pa | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -108,6 +109,7 @@ The table below provides an estimated request count based on typical Go usage pa | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | The estimates are based on observed request patterns: @@ -124,6 +126,7 @@ The estimates are based on observed request patterns: - Qwen3.7 Max — 420 input, 66,000 cached, 200 output tokens per request - Qwen3.7 Plus — 500 input, 57,000 cached, 190 output tokens per request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request +- Hy3 — 830 input, 71,500 cached, 295 output tokens per request The estimates are also based on the following prices per 1M tokens and the monthly usage included with each model: @@ -147,6 +150,7 @@ The estimates are also based on the following prices per 1M tokens and the month | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | You can track your current usage in the **console**. @@ -200,6 +204,7 @@ You can also access Go models through the following API endpoints. | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | The [model id](/docs/config/#models) in your OpenCode config uses the format `opencode-go/`. For example, for Kimi K3, you would diff --git a/packages/web/src/content/docs/it/go.mdx b/packages/web/src/content/docs/it/go.mdx index 65369e1e868a..26c459f45698 100644 --- a/packages/web/src/content/docs/it/go.mdx +++ b/packages/web/src/content/docs/it/go.mdx @@ -72,6 +72,7 @@ L'elenco attuale dei modelli include: - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** L'elenco dei modelli potrebbe cambiare man mano che ne testiamo e aggiungiamo di nuovi. @@ -95,7 +96,7 @@ La tabella seguente fornisce una stima del conteggio delle richieste in base a p | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -106,6 +107,7 @@ La tabella seguente fornisce una stima del conteggio delle richieste in base a p | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | Le stime si basano sui pattern di richieste osservati: @@ -120,6 +122,7 @@ Le stime si basano sui pattern di richieste osservati: - Qwen3.7 Max — 420 di input, 66.000 in cache, 200 token di output per richiesta - Qwen3.7 Plus — 500 di input, 57.000 in cache, 190 token di output per richiesta - Qwen3.6 Plus — 500 di input, 57.000 in cache, 190 token di output per richiesta +- Hy3 — 830 di input, 71.500 in cache, 295 token di output per richiesta - MiMo-V2.5 — 830 di input, 71.500 in cache, 295 token di output per richiesta - MiMo-V2.5-Pro — 790 di input, 86.000 in cache, 305 token di output per richiesta @@ -145,6 +148,7 @@ Le stime si basano anche sui seguenti prezzi per 1M token e sull'utilizzo mensil | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Puoi monitorare il tuo utilizzo attuale nella **console**. @@ -198,6 +202,7 @@ Puoi anche accedere ai modelli Go tramite i seguenti endpoint API. | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | Il [model id](/docs/config/#models) nella tua OpenCode config utilizza il formato `opencode-go/`. Ad esempio, per Kimi K3, useresti diff --git a/packages/web/src/content/docs/ja/go.mdx b/packages/web/src/content/docs/ja/go.mdx index 8bb9139e83ae..f2e95659a6f5 100644 --- a/packages/web/src/content/docs/ja/go.mdx +++ b/packages/web/src/content/docs/ja/go.mdx @@ -64,6 +64,7 @@ OpenCode Goをサブスクライブできるのは、1つのワークスペー - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** 新しいモデルをテストして追加するにつれて、モデルのリストは変更される場合があります。 @@ -87,7 +88,7 @@ OpenCode Goには以下の制限が含まれています: | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -98,6 +99,7 @@ OpenCode Goには以下の制限が含まれています: | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | 推定値は、観測されたリクエストパターンに基づいています: @@ -112,6 +114,7 @@ OpenCode Goには以下の制限が含まれています: - Qwen3.7 Max — リクエストあたり 入力 420トークン、キャッシュ 66,000トークン、出力 200トークン - Qwen3.7 Plus — リクエストあたり 入力 500トークン、キャッシュ 57,000トークン、出力 190トークン - Qwen3.6 Plus — リクエストあたり 入力 500トークン、キャッシュ 57,000トークン、出力 190トークン +- Hy3 — リクエストあたり 入力 830トークン、キャッシュ 71,500トークン、出力 295トークン - MiMo-V2.5 — リクエストあたり 入力 830トークン、キャッシュ 71,500トークン、出力 295トークン - MiMo-V2.5-Pro — リクエストあたり 入力 790トークン、キャッシュ 86,000トークン、出力 305トークン @@ -137,6 +140,7 @@ OpenCode Goには以下の制限が含まれています: | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | 現在の利用状況は**コンソール**で追跡できます。 @@ -188,6 +192,7 @@ Goでは月額$10を支払い、その6倍の利用枠を提供することを | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | OpenCode設定の[model id](/docs/config/#models)は、`opencode-go/`という形式を使用します。たとえば、Kimi K3の場合は、設定で`opencode-go/kimi-k3`を使用します。 diff --git a/packages/web/src/content/docs/ko/go.mdx b/packages/web/src/content/docs/ko/go.mdx index 02f88d2828eb..d03198ce4f1c 100644 --- a/packages/web/src/content/docs/ko/go.mdx +++ b/packages/web/src/content/docs/ko/go.mdx @@ -64,6 +64,7 @@ workspace당 한 명의 멤버만 OpenCode Go를 구독할 수 있습니다. - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** 새로운 모델을 테스트하고 추가함에 따라 이 목록은 변경될 수 있습니다. @@ -87,7 +88,7 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다. | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -98,6 +99,7 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다. | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | 이 예상치는 관찰된 요청 패턴을 기준으로 합니다. @@ -112,6 +114,7 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다. - Qwen3.7 Max — 요청당 입력 420, 캐시 66,000, 출력 토큰 200 - Qwen3.7 Plus — 요청당 입력 500, 캐시 57,000, 출력 토큰 190 - Qwen3.6 Plus — 요청당 입력 500, 캐시 57,000, 출력 토큰 190 +- Hy3 — 요청당 입력 830, 캐시 71,500, 출력 토큰 295 - MiMo-V2.5 — 요청당 입력 830, 캐시 71,500, 출력 토큰 295 - MiMo-V2.5-Pro — 요청당 입력 790, 캐시 86,000, 출력 토큰 305 @@ -137,6 +140,7 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다. | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | 현재 사용량은 **console**에서 확인할 수 있습니다. @@ -188,6 +192,7 @@ Go에서는 월 $10를 지불하며, 저희는 그 6배의 사용량을 제공 | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | OpenCode config의 [model id](/docs/config/#models)는 `opencode-go/` 형식을 사용합니다. 예를 들어 Kimi K3의 경우 config에서 `opencode-go/kimi-k3`를 사용하면 됩니다. diff --git a/packages/web/src/content/docs/nb/go.mdx b/packages/web/src/content/docs/nb/go.mdx index 60be1cc7bbba..c63d007a80ab 100644 --- a/packages/web/src/content/docs/nb/go.mdx +++ b/packages/web/src/content/docs/nb/go.mdx @@ -74,6 +74,7 @@ Den nåværende listen over modeller inkluderer: - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** Listen over modeller kan endres etter hvert som vi tester og legger til nye. @@ -97,7 +98,7 @@ Tabellen nedenfor gir et estimert antall forespørsler basert på typiske bruksm | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -108,6 +109,7 @@ Tabellen nedenfor gir et estimert antall forespørsler basert på typiske bruksm | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | Estimatene er basert på observerte forespørselsmønstre: @@ -122,6 +124,7 @@ Estimatene er basert på observerte forespørselsmønstre: - Qwen3.7 Max — 420 input, 66 000 bufret, 200 output-tokens per forespørsel - Qwen3.7 Plus — 500 input, 57 000 bufret, 190 output-tokens per forespørsel - Qwen3.6 Plus — 500 input, 57 000 bufret, 190 output-tokens per forespørsel +- Hy3 — 830 input, 71 500 bufret, 295 output-tokens per forespørsel - MiMo-V2.5 — 830 input, 71 500 bufret, 295 output-tokens per forespørsel - MiMo-V2.5-Pro — 790 input, 86 000 bufret, 305 output-tokens per forespørsel @@ -147,6 +150,7 @@ Estimatene er også basert på følgende priser per 1M tokens og den månedlige | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Du kan spore din nåværende bruk i **konsollen**. @@ -200,6 +204,7 @@ Du kan også få tilgang til Go-modeller gjennom følgende API-endepunkter. | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | [Modell-ID-en](/docs/config/#models) i din OpenCode-konfigurasjon bruker formatet `opencode-go/`. For eksempel, for Kimi K3, vil du diff --git a/packages/web/src/content/docs/pl/go.mdx b/packages/web/src/content/docs/pl/go.mdx index 4a867bf0e724..3f542a924e52 100644 --- a/packages/web/src/content/docs/pl/go.mdx +++ b/packages/web/src/content/docs/pl/go.mdx @@ -68,6 +68,7 @@ Obecna lista modeli obejmuje: - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** Lista modeli może ulec zmianie w miarę testowania i dodawania nowych. @@ -91,7 +92,7 @@ Poniższa tabela przedstawia szacunkową liczbę żądań na podstawie typowych | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -102,6 +103,7 @@ Poniższa tabela przedstawia szacunkową liczbę żądań na podstawie typowych | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | Szacunki te opierają się na zaobserwowanych wzorcach żądań: @@ -116,6 +118,7 @@ Szacunki te opierają się na zaobserwowanych wzorcach żądań: - Qwen3.7 Max — 420 tokenów wejściowych, 66 000 w pamięci podręcznej, 200 tokenów wyjściowych na żądanie - Qwen3.7 Plus — 500 tokenów wejściowych, 57 000 w pamięci podręcznej, 190 tokenów wyjściowych na żądanie - Qwen3.6 Plus — 500 tokenów wejściowych, 57 000 w pamięci podręcznej, 190 tokenów wyjściowych na żądanie +- Hy3 — 830 tokenów wejściowych, 71 500 w pamięci podręcznej, 295 tokenów wyjściowych na żądanie - MiMo-V2.5 — 830 tokenów wejściowych, 71 500 w pamięci podręcznej, 295 tokenów wyjściowych na żądanie - MiMo-V2.5-Pro — 790 tokenów wejściowych, 86 000 w pamięci podręcznej, 305 tokenów wyjściowych na żądanie @@ -141,6 +144,7 @@ Szacunki opierają się również na następujących cenach za 1M tokenów oraz | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Możesz śledzić swoje bieżące zużycie w **konsoli**. @@ -192,6 +196,7 @@ Możesz również uzyskać dostęp do modeli Go za pośrednictwem następującyc | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | [ID modelu](/docs/config/#models) w Twojej konfiguracji OpenCode używa formatu `opencode-go/`. Na przykład dla Kimi K3 należy użyć diff --git a/packages/web/src/content/docs/pt-br/go.mdx b/packages/web/src/content/docs/pt-br/go.mdx index 96c1addfcc91..def6efd471df 100644 --- a/packages/web/src/content/docs/pt-br/go.mdx +++ b/packages/web/src/content/docs/pt-br/go.mdx @@ -74,6 +74,7 @@ A lista atual de modelos inclui: - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** A lista de modelos pode mudar conforme testamos e adicionamos novos. @@ -97,7 +98,7 @@ A tabela abaixo fornece uma contagem estimada de requisições com base nos padr | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -108,6 +109,7 @@ A tabela abaixo fornece uma contagem estimada de requisições com base nos padr | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | As estimativas se baseiam nos padrões de requisições observados: @@ -122,6 +124,7 @@ As estimativas se baseiam nos padrões de requisições observados: - Qwen3.7 Max — 420 tokens de entrada, 66.000 em cache, 200 tokens de saída por requisição - Qwen3.7 Plus — 500 tokens de entrada, 57.000 em cache, 190 tokens de saída por requisição - Qwen3.6 Plus — 500 tokens de entrada, 57.000 em cache, 190 tokens de saída por requisição +- Hy3 — 830 tokens de entrada, 71.500 em cache, 295 tokens de saída por requisição - MiMo-V2.5 — 830 tokens de entrada, 71.500 em cache, 295 tokens de saída por requisição - MiMo-V2.5-Pro — 790 tokens de entrada, 86.000 em cache, 305 tokens de saída por requisição @@ -147,6 +150,7 @@ As estimativas também se baseiam nos seguintes preços por 1M tokens e no uso m | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Você pode acompanhar o seu uso atual no **console**. @@ -200,6 +204,7 @@ Você também pode acessar os modelos do Go através dos seguintes endpoints de | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | O [ID do modelo](/docs/config/#models) na sua configuração do OpenCode usa o formato `opencode-go/`. Por exemplo, para o Kimi K3, você usaria diff --git a/packages/web/src/content/docs/ru/go.mdx b/packages/web/src/content/docs/ru/go.mdx index 62305fbdb695..0bbd43369ba6 100644 --- a/packages/web/src/content/docs/ru/go.mdx +++ b/packages/web/src/content/docs/ru/go.mdx @@ -74,6 +74,7 @@ OpenCode Go работает так же, как и любой другой пр - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** Список моделей может меняться по мере того, как мы тестируем и добавляем новые. @@ -97,7 +98,7 @@ OpenCode Go включает следующие лимиты: | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -108,6 +109,7 @@ OpenCode Go включает следующие лимиты: | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | Эти оценки основаны на наблюдаемых показателях запросов: @@ -122,6 +124,7 @@ OpenCode Go включает следующие лимиты: - Qwen3.7 Max — 420 входных, 66,000 кешированных, 200 выходных токенов на запрос - Qwen3.7 Plus — 500 входных, 57,000 кешированных, 190 выходных токенов на запрос - Qwen3.6 Plus — 500 входных, 57,000 кешированных, 190 выходных токенов на запрос +- Hy3 — 830 входных, 71,500 кешированных, 295 выходных токенов на запрос - MiMo-V2.5 — 830 входных, 71,500 кешированных, 295 выходных токенов на запрос - MiMo-V2.5-Pro — 790 входных, 86,000 кешированных, 305 выходных токенов на запрос @@ -147,6 +150,7 @@ OpenCode Go включает следующие лимиты: | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Вы можете отслеживать текущее использование в **консоли**. @@ -200,6 +204,7 @@ OpenCode Go включает следующие лимиты: | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | [ID модели](/docs/config/#models) в вашем конфиге OpenCode использует формат `opencode-go/`. Например, для Kimi K3 вам нужно diff --git a/packages/web/src/content/docs/th/go.mdx b/packages/web/src/content/docs/th/go.mdx index 36036426da43..48b0c05bf1ed 100644 --- a/packages/web/src/content/docs/th/go.mdx +++ b/packages/web/src/content/docs/th/go.mdx @@ -64,6 +64,7 @@ OpenCode Go ทำงานเหมือนกับผู้ให้บร - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** รายชื่อโมเดลอาจมีการเปลี่ยนแปลงเมื่อเราทำการทดสอบและเพิ่มโมเดลใหม่ๆ @@ -87,7 +88,7 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -98,6 +99,7 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | การประมาณการนี้อ้างอิงจากรูปแบบการใช้งาน request ที่สังเกตพบ: @@ -112,6 +114,7 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: - Qwen3.7 Max — 420 input, 66,000 cached, 200 output tokens ต่อ request - Qwen3.7 Plus — 500 input, 57,000 cached, 190 output tokens ต่อ request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens ต่อ request +- Hy3 — 830 input, 71,500 cached, 295 output tokens ต่อ request - MiMo-V2.5 — 830 input, 71,500 cached, 295 output tokens ต่อ request - MiMo-V2.5-Pro — 790 input, 86,000 cached, 305 output tokens ต่อ request @@ -137,6 +140,7 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | คุณสามารถติดตามการใช้งานปัจจุบันของคุณได้ใน **console** @@ -188,6 +192,7 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | [model id](/docs/config/#models) ใน OpenCode config ของคุณจะใช้รูปแบบ `opencode-go/` ตัวอย่างเช่น สำหรับ Kimi K3 คุณจะใช้ `opencode-go/kimi-k3` ใน config ของคุณ diff --git a/packages/web/src/content/docs/tr/go.mdx b/packages/web/src/content/docs/tr/go.mdx index e24ead2959f7..0611ae31b7bc 100644 --- a/packages/web/src/content/docs/tr/go.mdx +++ b/packages/web/src/content/docs/tr/go.mdx @@ -64,6 +64,7 @@ Mevcut model listesi şunları içerir: - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** Test edip yenilerini ekledikçe model listesi değişebilir. @@ -87,7 +88,7 @@ Aşağıdaki tablo, tipik Go kullanım modellerine dayalı tahmini bir istek say | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -98,6 +99,7 @@ Aşağıdaki tablo, tipik Go kullanım modellerine dayalı tahmini bir istek say | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | Tahminler, gözlemlenen istek modellerine dayanır: @@ -112,6 +114,7 @@ Tahminler, gözlemlenen istek modellerine dayanır: - Qwen3.7 Max — İstek başına 420 girdi, 66.000 önbelleğe alınmış, 200 çıktı token'ı - Qwen3.7 Plus — İstek başına 500 girdi, 57.000 önbelleğe alınmış, 190 çıktı token'ı - Qwen3.6 Plus — İstek başına 500 girdi, 57.000 önbelleğe alınmış, 190 çıktı token'ı +- Hy3 — İstek başına 830 girdi, 71.500 önbelleğe alınmış, 295 çıktı token'ı - MiMo-V2.5 — İstek başına 830 girdi, 71.500 önbelleğe alınmış, 295 çıktı token'ı - MiMo-V2.5-Pro — İstek başına 790 girdi, 86.000 önbelleğe alınmış, 305 çıktı token'ı @@ -137,6 +140,7 @@ Tahminler ayrıca 1M token başına aşağıdaki fiyatlara ve her modelle birlik | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Mevcut kullanımınızı **konsoldan** takip edebilirsiniz. @@ -188,6 +192,7 @@ Go modellerine aşağıdaki API uç noktaları aracılığıyla da erişebilirsi | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | OpenCode yapılandırmanızdaki [model id](/docs/config/#models) formatı `opencode-go/` şeklindedir. Örneğin, Kimi K3 için yapılandırmanızda `opencode-go/kimi-k3` kullanmalısınız. diff --git a/packages/web/src/content/docs/zh-cn/go.mdx b/packages/web/src/content/docs/zh-cn/go.mdx index 604b9101593b..873b3a022418 100644 --- a/packages/web/src/content/docs/zh-cn/go.mdx +++ b/packages/web/src/content/docs/zh-cn/go.mdx @@ -64,6 +64,7 @@ OpenCode Go 的工作方式与 OpenCode 中的其他提供商一样。 - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** 随着我们进行测试和添加新模型,该列表可能会发生变化。 @@ -87,7 +88,7 @@ OpenCode Go 包含以下限制: | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -98,6 +99,7 @@ OpenCode Go 包含以下限制: | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | 预估值基于观察到的请求模式: @@ -114,6 +116,7 @@ OpenCode Go 包含以下限制: - Qwen3.7 Max — 每次请求 420 个输入 token,66,000 个缓存 token,200 个输出 token - Qwen3.7 Plus — 每次请求 500 个输入 token,57,000 个缓存 token,190 个输出 token - Qwen3.6 Plus — 每次请求 500 个输入 token,57,000 个缓存 token,190 个输出 token +- Hy3 — 每次请求 830 个输入 token,71,500 个缓存 token,295 个输出 token 预估值还基于以下每 1M tokens 的价格以及每个模型包含的每月使用额度: @@ -137,6 +140,7 @@ OpenCode Go 包含以下限制: | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | 你可以在 **控制台** 中跟踪你当前的使用情况。 @@ -188,6 +192,7 @@ OpenCode Go 包含以下限制: | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | 你的 OpenCode 配置中的 [模型 ID](/docs/config/#models) 使用 `opencode-go/` 格式。例如,对于 Kimi K3,你将在配置中使用 `opencode-go/kimi-k3`。 diff --git a/packages/web/src/content/docs/zh-tw/go.mdx b/packages/web/src/content/docs/zh-tw/go.mdx index 6434504a3e05..691abaa2a92e 100644 --- a/packages/web/src/content/docs/zh-tw/go.mdx +++ b/packages/web/src/content/docs/zh-tw/go.mdx @@ -64,6 +64,7 @@ OpenCode Go 的運作方式與 OpenCode 中的任何其他供應商相同。 - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** 隨著我們測試並加入新模型,模型清單可能會有所變動。 @@ -87,7 +88,7 @@ OpenCode Go 包含以下限制: | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -98,6 +99,7 @@ OpenCode Go 包含以下限制: | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | 這些預估值是基於觀察到的請求模式: @@ -112,6 +114,7 @@ OpenCode Go 包含以下限制: - Qwen3.7 Max — 每次請求 420 個輸入 token、66,000 個快取 token、200 個輸出 token - Qwen3.7 Plus — 每次請求 500 個輸入 token、57,000 個快取 token、190 個輸出 token - Qwen3.6 Plus — 每次請求 500 個輸入 token、57,000 個快取 token、190 個輸出 token +- Hy3 — 每次請求 830 個輸入 token、71,500 個快取 token、295 個輸出 token - MiMo-V2.5 — 每次請求 830 個輸入 token、71,500 個快取 token、295 個輸出 token - MiMo-V2.5-Pro — 每次請求 790 個輸入 token、86,000 個快取 token、305 個輸出 token @@ -137,6 +140,7 @@ OpenCode Go 包含以下限制: | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | 您可以在 **console** 中追蹤您目前的使用量。 @@ -188,6 +192,7 @@ OpenCode Go 包含以下限制: | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | 您的 OpenCode 設定中的 [model id](/docs/config/#models) 使用 `opencode-go/` 格式。例如,Kimi K3 在設定中應使用 `opencode-go/kimi-k3`。 From 4e067a20142c8081574f1143eda34e68f1a3770b Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:19:06 -0500 Subject: [PATCH 037/150] test(core): remove duplicate patch integration tests (#38389) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- packages/core/test/tool-patch.test.ts | 163 -------------------------- 1 file changed, 163 deletions(-) diff --git a/packages/core/test/tool-patch.test.ts b/packages/core/test/tool-patch.test.ts index f43b49342c37..86ae95e18372 100644 --- a/packages/core/test/tool-patch.test.ts +++ b/packages/core/test/tool-patch.test.ts @@ -387,17 +387,6 @@ describe("PatchTool", () => { ), ) - it.live("updates an empty file", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - const target = path.join(directory, "empty.txt") - yield* Effect.promise(() => fs.writeFile(target, "")) - yield* executeTool(registry, call("*** Begin Patch\n*** Update File: empty.txt\n@@\n+First line\n*** End Patch")) - expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("First line\n") - }), - ), - ) - it.live("rejects deleting a directory", () => withTempTool((directory, registry) => Effect.gen(function* () { @@ -410,40 +399,6 @@ describe("PatchTool", () => { ), ) - it.live("supports an end-of-file anchor", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - const target = path.join(directory, "tail.txt") - yield* Effect.promise(() => fs.writeFile(target, "first\nsecond")) - yield* executeTool( - registry, - call( - "*** Begin Patch\n*** Update File: tail.txt\n@@\n first\n-second\n+second updated\n*** End of File\n*** End Patch", - ), - ) - expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("first\nsecond updated\n") - }), - ), - ) - - it.live("applies an end-of-file chunk to the final duplicate", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - const target = path.join(directory, "duplicates.txt") - yield* Effect.promise(() => fs.writeFile(target, "marker\nend\nmiddle\nmarker\nend\n")) - yield* executeTool( - registry, - call( - "*** Begin Patch\n*** Update File: duplicates.txt\n@@\n-marker\n-end\n+marker changed\n+end\n*** End of File\n*** End Patch", - ), - ) - expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe( - "marker\nend\nmiddle\nmarker changed\nend\n", - ) - }), - ), - ) - it.live("rejects a missing second chunk context", () => withTempTool((directory, registry) => Effect.gen(function* () { @@ -513,20 +468,6 @@ describe("PatchTool", () => { ), ) - it.live("applies multiple hunks to one file", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - const target = path.join(directory, "multi.txt") - yield* Effect.promise(() => fs.writeFile(target, "a\nb\nc\nd\n")) - yield* executeTool( - registry, - call("*** Begin Patch\n*** Update File: multi.txt\n@@\n-b\n+B\n@@\n-d\n+D\n*** End Patch"), - ) - expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("a\nB\nc\nD\n") - }), - ), - ) - it.live("applies successive update operations to one file", () => withTempTool((directory, registry) => Effect.gen(function* () { @@ -566,110 +507,6 @@ describe("PatchTool", () => { ), ) - it.live("appends a trailing newline on update", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - const target = path.join(directory, "no-newline.txt") - yield* Effect.promise(() => fs.writeFile(target, "no newline at end")) - yield* executeTool( - registry, - call( - "*** Begin Patch\n*** Update File: no-newline.txt\n@@\n-no newline at end\n+first line\n+second line\n*** End Patch", - ), - ) - expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("first line\nsecond line\n") - }), - ), - ) - - it.live("disambiguates change context with an @@ header", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - const target = path.join(directory, "context.txt") - yield* Effect.promise(() => fs.writeFile(target, "fn a\nx=10\ny=2\nfn b\nx=10\ny=20\n")) - yield* executeTool( - registry, - call("*** Begin Patch\n*** Update File: context.txt\n@@ fn b\n-x=10\n+x=11\n*** End Patch"), - ) - expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe( - "fn a\nx=10\ny=2\nfn b\nx=11\ny=20\n", - ) - }), - ), - ) - - it.live("parses a heredoc-wrapped patch", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - yield* executeTool( - registry, - call("cat <<'EOF'\n*** Begin Patch\n*** Add File: heredoc.txt\n+with cat\n*** End Patch\nEOF"), - ) - expect(yield* Effect.promise(() => fs.readFile(path.join(directory, "heredoc.txt"), "utf8"))).toBe( - "with cat\n", - ) - }), - ), - ) - - it.live("parses a heredoc-wrapped patch without cat", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - yield* executeTool( - registry, - call("< fs.readFile(path.join(directory, "heredoc.txt"), "utf8"))).toBe( - "without cat\n", - ) - }), - ), - ) - - it.live("matches with trailing whitespace differences", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - const target = path.join(directory, "trailing.txt") - yield* Effect.promise(() => fs.writeFile(target, "line1 \nline2\nline3 \n")) - yield* executeTool( - registry, - call("*** Begin Patch\n*** Update File: trailing.txt\n@@\n-line2\n+changed\n*** End Patch"), - ) - expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("line1 \nchanged\nline3 \n") - }), - ), - ) - - it.live("matches with leading whitespace differences", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - const target = path.join(directory, "leading.txt") - yield* Effect.promise(() => fs.writeFile(target, " line1\nline2\n line3\n")) - yield* executeTool( - registry, - call("*** Begin Patch\n*** Update File: leading.txt\n@@\n-line2\n+changed\n*** End Patch"), - ) - expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe(" line1\nchanged\n line3\n") - }), - ), - ) - - it.live("matches with Unicode punctuation differences", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - const target = path.join(directory, "unicode.txt") - yield* Effect.promise(() => fs.writeFile(target, "He said “hello”\nsome—dash\nend\n")) - yield* executeTool( - registry, - call( - '*** Begin Patch\n*** Update File: unicode.txt\n@@\n-He said "hello"\n+He said "hi"\n*** End Patch', - ), - ) - expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe('He said "hi"\nsome—dash\nend\n') - }), - ), - ) - it.live("rejects an update with missing context", () => withTempTool((directory, registry) => Effect.gen(function* () { From 36979c96419c574a737ad9186863c3718f8115c1 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:50:33 -0500 Subject: [PATCH 038/150] test(core): consolidate provider factory coverage (#38390) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- .../core/test/plugin/provider-alibaba.test.ts | 96 ----------- .../core/test/plugin/provider-cohere.test.ts | 127 -------------- .../test/plugin/provider-deepinfra.test.ts | 161 ------------------ .../core/test/plugin/provider-factory.test.ts | 60 +++++++ .../core/test/plugin/provider-gateway.test.ts | 115 ------------- .../core/test/plugin/provider-groq.test.ts | 122 ------------- .../core/test/plugin/provider-mistral.test.ts | 134 --------------- .../test/plugin/provider-perplexity.test.ts | 127 -------------- .../test/plugin/provider-togetherai.test.ts | 132 -------------- .../core/test/plugin/provider-venice.test.ts | 120 ------------- 10 files changed, 60 insertions(+), 1134 deletions(-) delete mode 100644 packages/core/test/plugin/provider-alibaba.test.ts delete mode 100644 packages/core/test/plugin/provider-cohere.test.ts delete mode 100644 packages/core/test/plugin/provider-deepinfra.test.ts create mode 100644 packages/core/test/plugin/provider-factory.test.ts delete mode 100644 packages/core/test/plugin/provider-gateway.test.ts delete mode 100644 packages/core/test/plugin/provider-groq.test.ts delete mode 100644 packages/core/test/plugin/provider-mistral.test.ts delete mode 100644 packages/core/test/plugin/provider-perplexity.test.ts delete mode 100644 packages/core/test/plugin/provider-togetherai.test.ts delete mode 100644 packages/core/test/plugin/provider-venice.test.ts diff --git a/packages/core/test/plugin/provider-alibaba.test.ts b/packages/core/test/plugin/provider-alibaba.test.ts deleted file mode 100644 index bb7f922e524d..000000000000 --- a/packages/core/test/plugin/provider-alibaba.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { AISDK } from "@opencode-ai/core/aisdk" -import { describe, expect } from "bun:test" -import { createAlibaba } from "@ai-sdk/alibaba" -import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" -import { PluginHost } from "@opencode-ai/core/plugin/host" -import { AlibabaPlugin } from "@opencode-ai/core/plugin/provider/alibaba" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { testEffect } from "../lib/effect" -import { PluginTestLayer } from "./fixture" - -const it = testEffect(PluginTestLayer) - -const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const host = yield* PluginHost.make(plugin) - yield* AlibabaPlugin.effect(host) -}) - -describe("AlibabaPlugin", () => { - it.effect("creates an Alibaba SDK for @ai-sdk/alibaba", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")), - modelID: ModelV2.ID.make("qwen"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/alibaba", - options: { name: "alibaba" }, - }) - expect(result.sdk).toBeDefined() - }), - ) - - it.effect("ignores non-Alibaba SDK packages", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")), - modelID: ModelV2.ID.make("qwen"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/openai-compatible", - options: { name: "alibaba" }, - }) - expect(result.sdk).toBeUndefined() - }), - ) - - it.effect("matches the old bundled Alibaba SDK provider naming", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-alibaba"), ModelV2.ID.make("qwen")), - modelID: ModelV2.ID.make("qwen"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/alibaba", - options: { name: "custom-alibaba", apiKey: "test" }, - }) - const expected = createAlibaba({ apiKey: "test", ...{ name: "custom-alibaba" } }).languageModel("qwen") - const actual = result.sdk?.languageModel("qwen") - expect(actual?.provider).toBe(expected.provider) - expect(actual?.modelId).toBe(expected.modelId) - }), - ) - - it.effect("uses the default languageModel(modelID) behavior", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const item = ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("qwen-plus"), - package: "aisdk:test-provider", - }) - const result = yield* aisdk.runSDK({ model: item, package: "@ai-sdk/alibaba", options: {} }) - const language = result.sdk?.languageModel(item.modelID ?? item.id) - expect(language?.modelId).toBe("qwen-plus") - expect(language?.provider).toBe("alibaba.chat") - }), - ) -}) diff --git a/packages/core/test/plugin/provider-cohere.test.ts b/packages/core/test/plugin/provider-cohere.test.ts deleted file mode 100644 index a4109f74bdb0..000000000000 --- a/packages/core/test/plugin/provider-cohere.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { AISDK } from "@opencode-ai/core/aisdk" -import { describe, expect, mock } from "bun:test" -import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" -import { PluginHost } from "@opencode-ai/core/plugin/host" -import { CoherePlugin } from "@opencode-ai/core/plugin/provider/cohere" -import { ProviderV2 } from "@opencode-ai/core/provider" -import type { LanguageModelV3 } from "@ai-sdk/provider" -import { testEffect } from "../lib/effect" -import { PluginTestLayer } from "./fixture" - -const cohereOptions: Record[] = [] -const it = testEffect(PluginTestLayer) - -const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const host = yield* PluginHost.make(plugin) - yield* CoherePlugin.effect(host) -}) - -function fakeSelectorSdk(calls: string[]) { - const make = (method: string) => (id: string) => { - calls.push(`${method}:${id}`) - return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3 - } - return { - responses: make("responses"), - messages: make("messages"), - chat: make("chat"), - languageModel: make("languageModel"), - } -} - -void mock.module("@ai-sdk/cohere", () => ({ - createCohere: (options: Record) => { - cohereOptions.push({ ...options }) - return { - languageModel: (modelID: string) => ({ - modelID, - provider: `${options.name ?? "cohere"}.chat`, - specificationVersion: "v3", - }), - } - }, -})) - -describe("CoherePlugin", () => { - it.effect("creates a Cohere SDK only for @ai-sdk/cohere", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - - const ignored = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")), - modelID: ModelV2.ID.make("command"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/openai-compatible", - options: { name: "cohere" }, - }) - expect(ignored.sdk).toBeUndefined() - - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")), - modelID: ModelV2.ID.make("command"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/cohere", - options: { name: "cohere" }, - }) - expect(result.sdk).toBeDefined() - }), - ) - - it.effect("uses the model provider ID as the bundled SDK name", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-cohere"), ModelV2.ID.make("command-r-plus")), - modelID: ModelV2.ID.make("command-r-plus"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/cohere", - options: { name: "custom-cohere", apiKey: "test", baseURL: "https://cohere.example" }, - }) - - expect(cohereOptions.at(-1)).toEqual({ - name: "custom-cohere", - apiKey: "test", - baseURL: "https://cohere.example", - }) - expect(result.sdk?.languageModel("command-r-plus").provider).toBe("custom-cohere.chat") - }), - ) - - it.effect("leaves language selection to the default languageModel fallback", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const calls: string[] = [] - const sdk = fakeSelectorSdk(calls) - yield* addPlugin() - const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("command-r-plus"), - package: "aisdk:test-provider", - }), - sdk, - options: {}, - }) - - expect(result.language).toBeUndefined() - expect(calls).toEqual([]) - expect(result.language ?? sdk.languageModel("command-r-plus")).toBeDefined() - expect(calls).toEqual(["languageModel:command-r-plus"]) - }), - ) -}) diff --git a/packages/core/test/plugin/provider-deepinfra.test.ts b/packages/core/test/plugin/provider-deepinfra.test.ts deleted file mode 100644 index 14c41e550d71..000000000000 --- a/packages/core/test/plugin/provider-deepinfra.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { AISDK } from "@opencode-ai/core/aisdk" -import { describe, expect, mock } from "bun:test" -import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" -import { PluginHost } from "@opencode-ai/core/plugin/host" -import { DeepInfraPlugin } from "@opencode-ai/core/plugin/provider/deepinfra" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { testEffect } from "../lib/effect" -import { PluginTestLayer } from "./fixture" - -const it = testEffect(PluginTestLayer) -const deepinfraOptions: Record[] = [] -const deepinfraLanguageModels: string[] = [] - -const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const host = yield* PluginHost.make(plugin) - yield* DeepInfraPlugin.effect(host) -}) - -void mock.module("@ai-sdk/deepinfra", () => ({ - createDeepInfra: (options: Record) => { - const captured = { ...options } - deepinfraOptions.push(captured) - return { - languageModel: (modelID: string) => { - deepinfraLanguageModels.push(modelID) - return { modelID, provider: `${captured.name ?? "deepinfra"}.chat`, specificationVersion: "v3" } - }, - } - }, -})) - -function resetDeepInfraMock() { - deepinfraOptions.length = 0 - deepinfraLanguageModels.length = 0 -} - -describe("DeepInfraPlugin", () => { - it.effect("creates a DeepInfra SDK for @ai-sdk/deepinfra", () => - Effect.gen(function* () { - resetDeepInfraMock() - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:@ai-sdk/deepinfra", - }), - package: "@ai-sdk/deepinfra", - options: { name: "deepinfra" }, - }) - expect(result.sdk).toBeDefined() - }), - ) - - it.effect("passes the model provider ID as the bundled DeepInfra SDK name", () => - Effect.gen(function* () { - resetDeepInfraMock() - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-deepinfra"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:@ai-sdk/deepinfra", - }), - package: "@ai-sdk/deepinfra", - options: { name: "custom-deepinfra", apiKey: "test" }, - }) - expect(result.sdk.languageModel("model").provider).toBe("custom-deepinfra.chat") - expect(deepinfraOptions).toEqual([{ name: "custom-deepinfra", apiKey: "test" }]) - }), - ) - - it.effect("uses the canonical provider ID as the bundled DeepInfra SDK name", () => - Effect.gen(function* () { - resetDeepInfraMock() - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:@ai-sdk/deepinfra", - }), - package: "@ai-sdk/deepinfra", - options: { name: "deepinfra", apiKey: "test" }, - }) - expect(result.sdk.languageModel("model").provider).toBe("deepinfra.chat") - expect(deepinfraOptions).toEqual([{ name: "deepinfra", apiKey: "test" }]) - }), - ) - - it.effect("matches only the exact bundled DeepInfra package", () => - Effect.gen(function* () { - resetDeepInfraMock() - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const packages = [ - "unmatched-package", - "@ai-sdk/deepinfra-compatible", - "file:///tmp/@ai-sdk/deepinfra-provider.js", - ] - yield* Effect.forEach(packages, (item) => - Effect.gen(function* () { - const ignored = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:@ai-sdk/deepinfra", - }), - package: item, - options: { name: "deepinfra" }, - }) - expect(ignored.sdk).toBeUndefined() - }), - ) - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:@ai-sdk/deepinfra", - }), - package: "@ai-sdk/deepinfra", - options: { name: "deepinfra" }, - }) - expect(result.sdk).toBeDefined() - expect(deepinfraOptions).toEqual([{ name: "deepinfra" }]) - }), - ) - - it.effect("uses the default languageModel selection for DeepInfra models", () => - Effect.gen(function* () { - resetDeepInfraMock() - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const sdkEvent = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct")), - modelID: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"), - package: "aisdk:@ai-sdk/deepinfra", - }), - package: "@ai-sdk/deepinfra", - options: { name: "deepinfra" }, - }) - const result = yield* aisdk.runLanguage({ model: sdkEvent.model, sdk: sdkEvent.sdk, options: sdkEvent.options }) - const language = result.language ?? result.sdk.languageModel(result.model.modelID ?? result.model.id) - expect(language.provider).toBe("deepinfra.chat") - expect(deepinfraLanguageModels).toEqual(["meta-llama/Llama-3.3-70B-Instruct"]) - }), - ) -}) diff --git a/packages/core/test/plugin/provider-factory.test.ts b/packages/core/test/plugin/provider-factory.test.ts new file mode 100644 index 000000000000..a884fd0ce0fb --- /dev/null +++ b/packages/core/test/plugin/provider-factory.test.ts @@ -0,0 +1,60 @@ +import { expect } from "bun:test" +import { Effect } from "effect" +import { AISDK } from "@opencode-ai/core/aisdk" +import { ModelV2 } from "@opencode-ai/core/model" +import { PluginV2 } from "@opencode-ai/core/plugin" +import { PluginHost } from "@opencode-ai/core/plugin/host" +import { AlibabaPlugin } from "@opencode-ai/core/plugin/provider/alibaba" +import { CoherePlugin } from "@opencode-ai/core/plugin/provider/cohere" +import { DeepInfraPlugin } from "@opencode-ai/core/plugin/provider/deepinfra" +import { GatewayPlugin } from "@opencode-ai/core/plugin/provider/gateway" +import { GroqPlugin } from "@opencode-ai/core/plugin/provider/groq" +import { MistralPlugin } from "@opencode-ai/core/plugin/provider/mistral" +import { PerplexityPlugin } from "@opencode-ai/core/plugin/provider/perplexity" +import { TogetherAIPlugin } from "@opencode-ai/core/plugin/provider/togetherai" +import { VenicePlugin } from "@opencode-ai/core/plugin/provider/venice" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { testEffect } from "../lib/effect" +import { PluginTestLayer } from "./fixture" + +const modelID = ModelV2.ID.make("test-model") +const options = { name: "custom-provider", apiKey: "test", baseURL: "https://example.test" } +const providers = [ + { id: "alibaba", plugin: AlibabaPlugin, package: "@ai-sdk/alibaba", provider: "alibaba.chat" }, + { id: "cohere", plugin: CoherePlugin, package: "@ai-sdk/cohere", provider: "cohere.chat" }, + { id: "deepinfra", plugin: DeepInfraPlugin, package: "@ai-sdk/deepinfra", provider: "deepinfra.chat" }, + { id: "gateway", plugin: GatewayPlugin, package: "@ai-sdk/gateway", provider: "gateway" }, + { id: "groq", plugin: GroqPlugin, package: "@ai-sdk/groq", provider: "groq.chat" }, + { id: "mistral", plugin: MistralPlugin, package: "@ai-sdk/mistral", provider: "mistral.chat" }, + { id: "perplexity", plugin: PerplexityPlugin, package: "@ai-sdk/perplexity", provider: "perplexity" }, + { id: "togetherai", plugin: TogetherAIPlugin, package: "@ai-sdk/togetherai", provider: "togetherai.chat" }, + { id: "venice", plugin: VenicePlugin, package: "venice-ai-sdk-provider", provider: "custom-provider.chat" }, +] as const + +const it = testEffect(PluginTestLayer) + +providers.forEach((item) => + it.effect(`${item.id} loads only its exact package`, () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* item.plugin.effect(host) + const model = ModelV2.Info.make({ + ...ModelV2.Info.empty(ProviderV2.ID.make(item.id), modelID), + modelID, + package: ProviderV2.aisdk(item.package), + }) + const matched = yield* aisdk.runSDK({ model, package: item.package, options }) + const ignored = yield* aisdk.runSDK({ model, package: `${item.package}/unsupported`, options }) + const language = matched.sdk?.languageModel(modelID) + + expect({ + provider: language?.provider, + modelID: language?.modelId, + version: language?.specificationVersion, + ignored: ignored.sdk === undefined, + }).toEqual({ provider: item.provider, modelID: "test-model", version: "v3", ignored: true }) + }), + ), +) diff --git a/packages/core/test/plugin/provider-gateway.test.ts b/packages/core/test/plugin/provider-gateway.test.ts deleted file mode 100644 index 722bbde9b6f7..000000000000 --- a/packages/core/test/plugin/provider-gateway.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { AISDK } from "@opencode-ai/core/aisdk" -import { describe, expect, mock } from "bun:test" -import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" -import { PluginHost } from "@opencode-ai/core/plugin/host" -import { GatewayPlugin } from "@opencode-ai/core/plugin/provider/gateway" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { testEffect } from "../lib/effect" -import { PluginTestLayer } from "./fixture" - -const gatewayCalls: Record[] = [] -const vercelGatewayModels = ["anthropic/claude-sonnet-4", "openai/gpt-5", "google/gemini-2.5-pro"] -const it = testEffect(PluginTestLayer) - -const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const host = yield* PluginHost.make(plugin) - yield* GatewayPlugin.effect(host) -}) - -mock.module("@ai-sdk/gateway", () => ({ - createGateway(options: Record) { - gatewayCalls.push({ ...options }) - return { - languageModel(modelID: string) { - return { - modelId: modelID, - provider: options.name, - specificationVersion: "v3", - } - }, - } - }, -})) - -describe("GatewayPlugin", () => { - it.effect("creates a Gateway SDK for @ai-sdk/gateway", () => - Effect.gen(function* () { - gatewayCalls.length = 0 - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gateway"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/gateway", - options: { name: "gateway" }, - }) - expect(result.sdk).toBeDefined() - expect(gatewayCalls).toHaveLength(1) - }), - ) - - it.effect("passes the model providerID as the Gateway SDK name", () => - Effect.gen(function* () { - gatewayCalls.length = 0 - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make("anthropic/claude-sonnet-4")), - modelID: ModelV2.ID.make("anthropic/claude-sonnet-4"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/gateway", - options: { name: "vercel", apiKey: "test-key" }, - }) - - expect(gatewayCalls).toEqual([{ name: "vercel", apiKey: "test-key" }]) - expect(result.sdk.languageModel("anthropic/claude-sonnet-4").provider).toBe("vercel") - }), - ) - - it.effect("matches Vercel AI Gateway models by their @ai-sdk/gateway package", () => - Effect.gen(function* () { - gatewayCalls.length = 0 - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - - for (const modelID of vercelGatewayModels) { - const ignored = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)), - modelID: ModelV2.ID.make(modelID), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/vercel", - options: { name: "vercel" }, - }) - expect(ignored.sdk).toBeUndefined() - - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)), - modelID: ModelV2.ID.make(modelID), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/gateway", - options: { name: "vercel" }, - }) - expect(result.sdk).toBeDefined() - } - - expect(gatewayCalls).toHaveLength(3) - }), - ) -}) diff --git a/packages/core/test/plugin/provider-groq.test.ts b/packages/core/test/plugin/provider-groq.test.ts deleted file mode 100644 index b8900384b7da..000000000000 --- a/packages/core/test/plugin/provider-groq.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { AISDK } from "@opencode-ai/core/aisdk" -import { describe, expect } from "bun:test" -import { createGroq } from "@ai-sdk/groq" -import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" -import { PluginHost } from "@opencode-ai/core/plugin/host" -import { GroqPlugin } from "@opencode-ai/core/plugin/provider/groq" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { testEffect } from "../lib/effect" -import { PluginTestLayer } from "./fixture" - -const it = testEffect(PluginTestLayer) - -const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const host = yield* PluginHost.make(plugin) - yield* GroqPlugin.effect(host) -}) - -describe("GroqPlugin", () => { - it.effect("creates a Groq SDK for @ai-sdk/groq", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")), - modelID: ModelV2.ID.make("llama"), - package: "aisdk:@ai-sdk/groq", - }), - package: "@ai-sdk/groq", - options: { name: "groq" }, - }) - expect(result.sdk).toBeDefined() - }), - ) - - it.effect("ignores non-Groq SDK packages", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")), - modelID: ModelV2.ID.make("llama"), - package: "aisdk:@ai-sdk/groq", - }), - package: "@ai-sdk/openai-compatible", - options: { name: "groq" }, - }) - expect(result.sdk).toBeUndefined() - }), - ) - - it.effect("only matches the bundled @ai-sdk/groq package exactly", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")), - modelID: ModelV2.ID.make("llama"), - package: "aisdk:@ai-sdk/groq", - }), - package: "@ai-sdk/groq/compat", - options: { name: "groq" }, - }) - expect(result.sdk).toBeUndefined() - }), - ) - - it.effect("matches the old bundled Groq SDK provider naming", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-groq"), ModelV2.ID.make("llama")), - modelID: ModelV2.ID.make("llama"), - package: "aisdk:@ai-sdk/groq", - }), - package: "@ai-sdk/groq", - options: { name: "custom-groq", apiKey: "test" }, - }) - const expected = createGroq({ name: "custom-groq", apiKey: "test" } as Parameters[0] & { - name: string - }).languageModel("llama") - const actual = result.sdk?.languageModel("llama") - expect(actual?.provider).toBe(expected.provider) - expect(actual?.modelId).toBe(expected.modelId) - }), - ) - - it.effect("uses the default languageModel(modelID) behavior", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const sdk = createGroq({ name: "groq", apiKey: "test" } as Parameters[0] & { - name: string - }) - const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("llama-api"), - package: "aisdk:@ai-sdk/groq", - }), - sdk, - options: { name: "groq", apiKey: "test" }, - }) - const language = result.language ?? sdk.languageModel(result.model.modelID ?? result.model.id) - expect(language.modelId).toBe("llama-api") - expect(language.provider).toBe("groq.chat") - }), - ) -}) diff --git a/packages/core/test/plugin/provider-mistral.test.ts b/packages/core/test/plugin/provider-mistral.test.ts deleted file mode 100644 index 182873482cfe..000000000000 --- a/packages/core/test/plugin/provider-mistral.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { AISDK } from "@opencode-ai/core/aisdk" -import type { LanguageModelV3 } from "@ai-sdk/provider" -import { describe, expect } from "bun:test" -import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" -import { PluginHost } from "@opencode-ai/core/plugin/host" -import { MistralPlugin } from "@opencode-ai/core/plugin/provider/mistral" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { testEffect } from "../lib/effect" -import { PluginTestLayer } from "./fixture" - -const it = testEffect(PluginTestLayer) - -const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const host = yield* PluginHost.make(plugin) - yield* MistralPlugin.effect(host) -}) - -describe("MistralPlugin", () => { - it.effect("creates a Mistral SDK for @ai-sdk/mistral", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")), - modelID: ModelV2.ID.make("mistral-large"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/mistral", - options: { name: "mistral" }, - }) - expect(result.sdk).toBeDefined() - }), - ) - - it.effect("ignores non-Mistral SDK packages", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")), - modelID: ModelV2.ID.make("mistral-large"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/openai-compatible", - options: { name: "mistral" }, - }) - expect(result.sdk).toBeUndefined() - }), - ) - - it.effect("matches the old bundled Mistral SDK provider name for the bundled provider ID", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const providers: string[] = [] - yield* addPlugin() - yield* aisdk.hook.sdk((event) => - Effect.sync(() => { - providers.push(event.sdk.languageModel("mistral-large").provider) - }), - ) - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")), - modelID: ModelV2.ID.make("mistral-large"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/mistral", - options: { name: "mistral" }, - }) - expect(result.sdk).toBeDefined() - expect(providers).toEqual(["mistral.chat"]) - }), - ) - - it.effect("matches the old bundled Mistral SDK provider name for custom provider IDs", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const providers: string[] = [] - yield* addPlugin() - yield* aisdk.hook.sdk((event) => - Effect.sync(() => { - providers.push(event.sdk.languageModel("mistral-large").provider) - }), - ) - yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-mistral"), ModelV2.ID.make("mistral-large")), - modelID: ModelV2.ID.make("mistral-large"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/mistral", - options: { name: "custom-mistral" }, - }) - expect(providers).toEqual(["mistral.chat"]) - }), - ) - - it.effect("leaves Mistral language selection on the default sdk.languageModel(modelID) path", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const calls: string[] = [] - const sdk = { - languageModel: (id: string) => { - calls.push(`languageModel:${id}`) - return { modelId: id, provider: "languageModel", specificationVersion: "v3" } as unknown as LanguageModelV3 - }, - } - yield* addPlugin() - const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("mistral-large"), - package: "aisdk:test-provider", - }), - sdk, - options: {}, - }) - const language = result.language ?? sdk.languageModel(result.model.modelID ?? result.model.id) - expect(calls).toEqual(["languageModel:mistral-large"]) - expect(language).toBeDefined() - }), - ) -}) diff --git a/packages/core/test/plugin/provider-perplexity.test.ts b/packages/core/test/plugin/provider-perplexity.test.ts deleted file mode 100644 index d66f5dd71c44..000000000000 --- a/packages/core/test/plugin/provider-perplexity.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { AISDK } from "@opencode-ai/core/aisdk" -import { describe, expect } from "bun:test" -import type { LanguageModelV3 } from "@ai-sdk/provider" -import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" -import { PluginHost } from "@opencode-ai/core/plugin/host" -import { PerplexityPlugin } from "@opencode-ai/core/plugin/provider/perplexity" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { testEffect } from "../lib/effect" -import { PluginTestLayer } from "./fixture" - -const it = testEffect(PluginTestLayer) - -const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const host = yield* PluginHost.make(plugin) - yield* PerplexityPlugin.effect(host) -}) - -function fakeSelectorSdk(calls: string[]) { - const make = (method: string) => (id: string) => { - calls.push(`${method}:${id}`) - return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3 - } - return { - responses: make("responses"), - messages: make("messages"), - chat: make("chat"), - languageModel: make("languageModel"), - } -} - -describe("PerplexityPlugin", () => { - it.effect("creates a Perplexity SDK for the exact @ai-sdk/perplexity package", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")), - modelID: ModelV2.ID.make("sonar"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/perplexity", - options: { name: "perplexity" }, - }) - expect(result.sdk).toBeDefined() - }), - ) - - it.effect("ignores packages that are not the bundled Perplexity package", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")), - modelID: ModelV2.ID.make("sonar"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/perplexity-compatible", - options: { name: "perplexity" }, - }) - expect(result.sdk).toBeUndefined() - }), - ) - - it.effect("uses the Perplexity provider ID as the SDK name for the bundled provider", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")), - modelID: ModelV2.ID.make("sonar"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/perplexity", - options: { name: "perplexity" }, - }) - expect(result.sdk.languageModel("sonar").provider).toBe("perplexity") - }), - ) - - it.effect("creates bundled Perplexity SDKs for custom provider IDs", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-perplexity"), ModelV2.ID.make("sonar")), - modelID: ModelV2.ID.make("sonar"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/perplexity", - options: { name: "custom-perplexity" }, - }) - expect(result.sdk.languageModel("sonar").provider).toBe("perplexity") - }), - ) - - it.effect("leaves Perplexity language selection to the default languageModel fallback", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const calls: string[] = [] - yield* addPlugin() - const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("sonar"), - package: "aisdk:test-provider", - }), - sdk: fakeSelectorSdk(calls), - options: {}, - }) - expect(calls).toEqual([]) - expect(result.language).toBeUndefined() - }), - ) -}) diff --git a/packages/core/test/plugin/provider-togetherai.test.ts b/packages/core/test/plugin/provider-togetherai.test.ts deleted file mode 100644 index 1fffb03156a4..000000000000 --- a/packages/core/test/plugin/provider-togetherai.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { AISDK } from "@opencode-ai/core/aisdk" -import { describe, expect } from "bun:test" -import type { LanguageModelV3 } from "@ai-sdk/provider" -import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" -import { PluginHost } from "@opencode-ai/core/plugin/host" -import { TogetherAIPlugin } from "@opencode-ai/core/plugin/provider/togetherai" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { testEffect } from "../lib/effect" -import { PluginTestLayer } from "./fixture" - -const it = testEffect(PluginTestLayer) - -const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const host = yield* PluginHost.make(plugin) - yield* TogetherAIPlugin.effect(host) -}) - -function fakeSelectorSdk(calls: string[]) { - const make = (method: string) => (id: string) => { - calls.push(`${method}:${id}`) - return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3 - } - return { - responses: make("responses"), - messages: make("messages"), - chat: make("chat"), - languageModel: make("languageModel"), - } -} - -describe("TogetherAIPlugin", () => { - it.effect("creates a TogetherAI SDK for @ai-sdk/togetherai", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/togetherai", - options: { name: "togetherai" }, - }) - expect(result.sdk).toBeDefined() - }), - ) - - it.effect("matches the old bundled provider package exactly", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - - const ignored = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:test-provider", - }), - package: "file:///tmp/@ai-sdk/togetherai-provider.js", - options: { name: "togetherai" }, - }) - expect(ignored.sdk).toBeUndefined() - - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/togetherai", - options: { name: "togetherai" }, - }) - expect(result.sdk).toBeDefined() - }), - ) - - it.effect("creates bundled TogetherAI SDKs for custom provider IDs", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-togetherai"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/togetherai", - options: { name: "custom-togetherai" }, - }) - - expect(result.sdk.languageModel("model").provider).toBe("togetherai.chat") - }), - ) - - it.effect("defaults language selection to sdk.languageModel with the model API ID", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const calls: string[] = [] - yield* addPlugin() - - const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty( - ProviderV2.ID.make("togetherai"), - ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"), - ), - modelID: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"), - package: "aisdk:test-provider", - }), - sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, - options: {}, - }) - - expect(result.language).toBeUndefined() - expect(calls).toEqual([]) - expect( - result.language ?? fakeSelectorSdk(calls).languageModel(result.model.modelID ?? result.model.id), - ).toBeDefined() - expect(calls).toEqual(["languageModel:meta-llama/Llama-3.3-70B-Instruct-Turbo"]) - }), - ) -}) diff --git a/packages/core/test/plugin/provider-venice.test.ts b/packages/core/test/plugin/provider-venice.test.ts deleted file mode 100644 index 057d2c6ded94..000000000000 --- a/packages/core/test/plugin/provider-venice.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { AISDK } from "@opencode-ai/core/aisdk" -import { describe, expect } from "bun:test" -import type { LanguageModelV3 } from "@ai-sdk/provider" -import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" -import { PluginHost } from "@opencode-ai/core/plugin/host" -import { VenicePlugin } from "@opencode-ai/core/plugin/provider/venice" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { testEffect } from "../lib/effect" -import { PluginTestLayer } from "./fixture" - -const it = testEffect(PluginTestLayer) - -const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const host = yield* PluginHost.make(plugin) - yield* VenicePlugin.effect(host) -}) - -function fakeSelectorSdk(calls: string[]) { - const make = (method: string) => (id: string) => { - calls.push(`${method}:${id}`) - return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3 - } - return { - responses: make("responses"), - messages: make("messages"), - chat: make("chat"), - languageModel: make("languageModel"), - } -} - -describe("VenicePlugin", () => { - it.effect("creates a Venice SDK for venice-ai-sdk-provider", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:test-provider", - }), - package: "venice-ai-sdk-provider", - options: { name: "venice" }, - }) - expect(result.sdk).toBeDefined() - }), - ) - - it.effect("uses the model provider ID as the bundled Venice SDK name", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-venice"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:test-provider", - }), - package: "venice-ai-sdk-provider", - options: { name: "custom-venice", apiKey: "test" }, - }) - expect(result.sdk).toBeDefined() - expect(result.sdk.languageModel("model").provider).toBe("custom-venice.chat") - }), - ) - - it.effect("only handles the bundled venice-ai-sdk-provider package", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const similar = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:test-provider", - }), - package: "file:///tmp/venice-ai-sdk-provider.js", - options: { name: "venice" }, - }) - const other = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/openai-compatible", - options: { name: "venice" }, - }) - expect(similar.sdk).toBeUndefined() - expect(other.sdk).toBeUndefined() - }), - ) - - it.effect("leaves Venice language selection to the default languageModel fallback", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const calls: string[] = [] - yield* addPlugin() - const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("alias"), - package: "aisdk:test-provider", - }), - sdk: fakeSelectorSdk(calls), - options: {}, - }) - expect(calls).toEqual([]) - expect(result.language).toBeUndefined() - }), - ) -}) From b6e14b5a7415ad5c92f72a295339fd0564cbe8b1 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 22 Jul 2026 17:56:22 -0400 Subject: [PATCH 039/150] refactor(tui): finish V2 theme migration (#38383) --- .../client/src/promise/generated/types.ts | 4 +--- .../test/fixtures/opencode-v2-openapi.json | 22 +++---------------- packages/core/src/config/agent.ts | 5 +---- packages/core/src/v1/config/agent.ts | 7 ++---- packages/core/test/config/agent.test.ts | 10 ++++++--- packages/core/test/config/config.test.ts | 4 ++-- packages/docs/agents.mdx | 7 +++--- packages/docs/openapi.json | 22 +++---------------- packages/schema/src/agent.ts | 7 +++--- packages/schema/test/contract-hygiene.test.ts | 6 +++++ packages/tui/src/component/bg-pulse.tsx | 8 +++---- packages/tui/src/context/local.tsx | 19 ++-------------- packages/tui/src/context/theme.tsx | 15 +++---------- packages/www/content/docs/(docs)/agents.mdx | 7 +++--- packages/www/public/openapi.json | 22 +++---------------- 15 files changed, 46 insertions(+), 119 deletions(-) diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 6c869b0e2e6b..966ec22cc03c 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -8,8 +8,6 @@ export type ModelRef = { id: string; providerID: string; variant?: string } export type ProviderSettings = { [x: string]: JsonValue } -export type AgentColor = string | "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info" - export type PermissionV2Effect = "allow" | "deny" | "ask" export type PluginInfo = { id: string } @@ -2005,7 +2003,7 @@ export type AgentInfo = { description?: string mode: "subagent" | "primary" | "all" hidden: boolean - color?: AgentColor + color?: string steps?: number permissions: PermissionV2Ruleset } diff --git a/packages/codemode/test/fixtures/opencode-v2-openapi.json b/packages/codemode/test/fixtures/opencode-v2-openapi.json index 543a41817372..d430f5d21dd8 100644 --- a/packages/codemode/test/fixtures/opencode-v2-openapi.json +++ b/packages/codemode/test/fixtures/opencode-v2-openapi.json @@ -10561,26 +10561,10 @@ "additionalProperties": false }, "Agent.Color": { - "anyOf": [ - { - "type": "string", - "allOf": [ - { - "pattern": "^#[0-9a-fA-F]{6}$" - } - ] - }, + "type": "string", + "allOf": [ { - "type": "string", - "enum": [ - "primary", - "secondary", - "accent", - "success", - "warning", - "error", - "info" - ] + "pattern": "^#[0-9a-fA-F]{6}$" } ] }, diff --git a/packages/core/src/config/agent.ts b/packages/core/src/config/agent.ts index fc5edf51192a..075feea25075 100644 --- a/packages/core/src/config/agent.ts +++ b/packages/core/src/config/agent.ts @@ -6,10 +6,7 @@ import { ConfigProvider } from "./provider" import { ConfigModel } from "./model" import { PositiveInt } from "../schema" -export const Color = Schema.Union([ - Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)), - Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]), -]) +export const Color = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)) export class Info extends Schema.Class("ConfigV2.Agent")({ model: ConfigModel.Selection.pipe(Schema.optional), diff --git a/packages/core/src/v1/config/agent.ts b/packages/core/src/v1/config/agent.ts index b220bd7ef87d..09838a919685 100644 --- a/packages/core/src/v1/config/agent.ts +++ b/packages/core/src/v1/config/agent.ts @@ -4,10 +4,7 @@ import { Schema, SchemaGetter } from "effect" import { PositiveInt } from "../../schema" import { ConfigPermissionV1 } from "./permission" -const Color = Schema.Union([ - Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)), - Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]), -]) +const Color = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)) const AgentSchema = Schema.StructWithRest( Schema.Struct({ @@ -29,7 +26,7 @@ const AgentSchema = Schema.StructWithRest( }), options: Schema.optional(Schema.Record(Schema.String, Schema.Any)), color: Schema.optional(Color).annotate({ - description: "Hex color code (e.g., #FF5733) or theme color (e.g., primary)", + description: "Hex color code (e.g., #FF5733)", }), steps: Schema.optional(PositiveInt).annotate({ description: "Maximum number of agentic iterations before forcing text-only response", diff --git a/packages/core/test/config/agent.test.ts b/packages/core/test/config/agent.test.ts index 0380c38e3a32..855ea370e2f7 100644 --- a/packages/core/test/config/agent.test.ts +++ b/packages/core/test/config/agent.test.ts @@ -1,4 +1,4 @@ -import { describe, expect } from "bun:test" +import { describe, expect, test } from "bun:test" import fs from "fs/promises" import path from "path" import { Effect, Schema } from "effect" @@ -23,6 +23,10 @@ const defaultPermissions = [ { action: "external_directory", resource: "*", effect: "ask" }, ] satisfies PermissionV2.Ruleset +test("rejects named agent color tokens", () => { + expect(() => decode({ agents: { reviewer: { color: "warning" } } })).toThrow() +}) + describe("ConfigAgentPlugin.Plugin", () => { it.effect("matches POSIX paths against home-relative permissions", () => Effect.gen(function* () { @@ -160,7 +164,7 @@ describe("ConfigAgentPlugin.Plugin", () => { description: "Reviews changes", mode: "subagent", hidden: true, - color: "warning", + color: "#ff6b6b", steps: 12, request: { headers: { first: "one", shared: "first" }, @@ -197,7 +201,7 @@ describe("ConfigAgentPlugin.Plugin", () => { description: "Reviews changes", mode: "subagent", hidden: true, - color: "warning", + color: "#ff6b6b", steps: 12, model: { providerID: "anthropic", id: "claude-sonnet" }, }) diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index 64e08b5e7963..6584cc1c94cf 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -738,7 +738,7 @@ describe("Config", () => { system: "Find regressions.", mode: "subagent", hidden: false, - color: "warning", + color: "#ff6b6b", steps: 12, disabled: false, permissions: [{ action: "edit", resource: "*", effect: "deny" }], @@ -824,7 +824,7 @@ describe("Config", () => { expect(reviewer?.system).toBe("Find regressions.") expect(reviewer?.mode).toBe("subagent") expect(reviewer?.hidden).toBe(false) - expect(reviewer?.color).toBe("warning") + expect(reviewer?.color).toBe("#ff6b6b") expect(reviewer?.steps).toBe(12) expect(reviewer?.disabled).toBe(false) expect(reviewer?.permissions).toEqual([{ action: "edit", resource: "*", effect: "deny" }]) diff --git a/packages/docs/agents.mdx b/packages/docs/agents.mdx index a81ae6ec3c27..ca7ee80a2075 100644 --- a/packages/docs/agents.mdx +++ b/packages/docs/agents.mdx @@ -89,7 +89,7 @@ becomes `system`: description: Reviews changes without modifying files mode: subagent model: anthropic/claude-sonnet-4-5#high -color: warning +color: "#ff6b6b" steps: 8 permissions: - action: edit @@ -118,7 +118,7 @@ Use the `agents` field in any [OpenCode configuration file](/config): "mode": "all", "model": "anthropic/claude-sonnet-4-5#high", "system": "Review the current changes. Report findings before any summary.", - "color": "warning", + "color": "#ff6b6b", "steps": 8, "permissions": [ { "action": "edit", "resource": "*", "effect": "deny" }, @@ -250,8 +250,7 @@ security boundary. ### `color` -Sets the agent's UI color. Use a six-digit hex color such as `#ff6b6b`, or one -of `primary`, `secondary`, `accent`, `success`, `warning`, `error`, or `info`. +Sets the agent's UI color. Use a six-digit hex color such as `#ff6b6b`. ### `disabled` diff --git a/packages/docs/openapi.json b/packages/docs/openapi.json index 543a41817372..d430f5d21dd8 100644 --- a/packages/docs/openapi.json +++ b/packages/docs/openapi.json @@ -10561,26 +10561,10 @@ "additionalProperties": false }, "Agent.Color": { - "anyOf": [ - { - "type": "string", - "allOf": [ - { - "pattern": "^#[0-9a-fA-F]{6}$" - } - ] - }, + "type": "string", + "allOf": [ { - "type": "string", - "enum": [ - "primary", - "secondary", - "accent", - "success", - "warning", - "error", - "info" - ] + "pattern": "^#[0-9a-fA-F]{6}$" } ] }, diff --git a/packages/schema/src/agent.ts b/packages/schema/src/agent.ts index 8806a9c46938..399e9e9131e0 100644 --- a/packages/schema/src/agent.ts +++ b/packages/schema/src/agent.ts @@ -16,10 +16,9 @@ export type ID = typeof ID.Type export const Name = Schema.String.pipe(Schema.brand("Agent.Name")) export type Name = typeof Name.Type -export const Color = Schema.Union([ - Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)), - Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]), -]).annotate({ identifier: "Agent.Color" }) +export const Color = Schema.String.annotate({ identifier: "Agent.Color" }).check( + Schema.isPattern(/^#[0-9a-fA-F]{6}$/), +) export type Color = typeof Color.Type export interface Info extends Schema.Schema.Type {} diff --git a/packages/schema/test/contract-hygiene.test.ts b/packages/schema/test/contract-hygiene.test.ts index c441ad88c381..c66287026902 100644 --- a/packages/schema/test/contract-hygiene.test.ts +++ b/packages/schema/test/contract-hygiene.test.ts @@ -20,6 +20,12 @@ import { PersistedRevert } from "../src/session-revert.js" import { optional } from "../src/schema.js" describe("contract hygiene", () => { + test("restricts agent colors to six-digit hex values", () => { + const decode = Schema.decodeUnknownSync(Agent.Color) + expect(decode("#ff6b6b")).toBe("#ff6b6b") + expect(() => decode("warning")).toThrow() + }) + test("keeps absolute costs distinct from model rates", () => { const usd = Money.USD.make(1) const rate = Money.USDPerMillionTokens.make(1) diff --git a/packages/tui/src/component/bg-pulse.tsx b/packages/tui/src/component/bg-pulse.tsx index 2112fe442095..064cc314f93a 100644 --- a/packages/tui/src/component/bg-pulse.tsx +++ b/packages/tui/src/component/bg-pulse.tsx @@ -70,7 +70,7 @@ declare module "@opentui/solid" { extend({ go_upsell_art: GoUpsellArtRenderable }) export function BgPulse() { - const { theme } = useTheme() + const { themeV2, mode } = useTheme().contextual("elevated") const renderer = useRenderer() let targetFps = renderer.targetFps let maxFps = renderer.maxFps @@ -91,9 +91,9 @@ export function BgPulse() { ) diff --git a/packages/tui/src/context/local.tsx b/packages/tui/src/context/local.tsx index 03716cdf12ac..98faea2f9d0b 100644 --- a/packages/tui/src/context/local.tsx +++ b/packages/tui/src/context/local.tsx @@ -23,16 +23,6 @@ import { useRoute } from "./route" import { useData } from "./data" import { usePermission } from "./permission" -export type LocalTheme = { - secondary: RGBA - accent: RGBA - success: RGBA - warning: RGBA - primary: RGBA - error: RGBA - info: RGBA -} - export function parseModel(model: string) { const [providerID, ...rest] = model.split("/") return { @@ -60,7 +50,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const data = useData() const client = useClient() const toast = useToast() - const { theme, themeV2, mode } = useTheme() + const { themeV2, mode } = useTheme() const route = useRoute() const paths = useTuiPaths() const args = useArgs() @@ -128,12 +118,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ if (index === -1) return colors()[0] const agent = visibleAgents()[index] - if (agent?.color) { - const color = agent.color - if (color.startsWith("#")) return RGBA.fromHex(color) - // already validated by config, just satisfying TS here - return theme[color as keyof typeof theme] as RGBA - } + if (agent?.color) return RGBA.fromHex(agent.color) return colors()[index % colors().length] }, } diff --git a/packages/tui/src/context/theme.tsx b/packages/tui/src/context/theme.tsx index 88f91cb50de2..aff085e969ed 100644 --- a/packages/tui/src/context/theme.tsx +++ b/packages/tui/src/context/theme.tsx @@ -71,7 +71,6 @@ type State = { type ContextName = "elevated" | "overlay" type ThemeService = { - theme: Theme themeV2: ComponentTheme contextual(context: ContextName): ThemeService readonly selected: string @@ -280,7 +279,7 @@ const themeContext = createSimpleContext({ if (supported.includes(store.mode)) return store.mode return supported[0] ?? store.mode } - const values = createMemo(() => resolveTheme(source(), mode())) + const legacySyntaxTheme = createMemo(() => resolveTheme(source(), mode())) const valuesV2 = createMemo(() => resolveThemeFile(file(), mode(), sourceName())) valuesV2() themePerformance.set("Init", `${(performance.now() - initStarted).toFixed(2)} ms`) @@ -298,21 +297,13 @@ const themeContext = createSimpleContext({ }, mode), } - createEffect(() => renderer.setBackgroundColor(values().background)) + createEffect(() => renderer.setBackgroundColor(valuesV2().background.default)) - const syntax = createSyntaxStyleMemo(() => generateSyntax(values())) - - const theme = new Proxy(values(), { - get(_target, prop) { - // @ts-expect-error Properties are forwarded to the current reactive value. - return values()[prop] - }, - }) + const syntax = createSyntaxStyleMemo(() => generateSyntax(legacySyntaxTheme())) function contextual(context: ContextName) { return contextualServices[context] } const service: ThemeService = { - theme, themeV2, contextual, get selected() { diff --git a/packages/www/content/docs/(docs)/agents.mdx b/packages/www/content/docs/(docs)/agents.mdx index b1275960cb51..d5668402db4b 100644 --- a/packages/www/content/docs/(docs)/agents.mdx +++ b/packages/www/content/docs/(docs)/agents.mdx @@ -89,7 +89,7 @@ becomes `system`: description: Reviews changes without modifying files mode: subagent model: anthropic/claude-sonnet-4-5#high -color: warning +color: "#ff6b6b" steps: 8 permissions: - action: edit @@ -118,7 +118,7 @@ Use the `agents` field in any [OpenCode configuration file](/docs/config): "mode": "all", "model": "anthropic/claude-sonnet-4-5#high", "system": "Review the current changes. Report findings before any summary.", - "color": "warning", + "color": "#ff6b6b", "steps": 8, "permissions": [ { "action": "edit", "resource": "*", "effect": "deny" }, @@ -250,8 +250,7 @@ security boundary. ### `color` -Sets the agent's UI color. Use a six-digit hex color such as `#ff6b6b`, or one -of `primary`, `secondary`, `accent`, `success`, `warning`, `error`, or `info`. +Sets the agent's UI color. Use a six-digit hex color such as `#ff6b6b`. ### `disabled` diff --git a/packages/www/public/openapi.json b/packages/www/public/openapi.json index 543a41817372..d430f5d21dd8 100644 --- a/packages/www/public/openapi.json +++ b/packages/www/public/openapi.json @@ -10561,26 +10561,10 @@ "additionalProperties": false }, "Agent.Color": { - "anyOf": [ - { - "type": "string", - "allOf": [ - { - "pattern": "^#[0-9a-fA-F]{6}$" - } - ] - }, + "type": "string", + "allOf": [ { - "type": "string", - "enum": [ - "primary", - "secondary", - "accent", - "success", - "warning", - "error", - "info" - ] + "pattern": "^#[0-9a-fA-F]{6}$" } ] }, From 381f6c47b46a2a4f89d37d8c698ada1b50c36057 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 22 Jul 2026 18:30:31 -0400 Subject: [PATCH 040/150] docs(tui): add generated V2 theme reference (#38396) --- .github/workflows/test.yml | 5 + packages/docs/README.md | 11 ++ packages/docs/docs.json | 1 + packages/docs/index.mdx | 2 +- packages/docs/package.json | 10 +- packages/docs/script/generate-theme-tokens.ts | 136 ++++++++++++++++++ .../docs/snippets/generated/theme-tokens.mdx | 79 ++++++++++ packages/docs/themes.mdx | 129 +++++++++++++++++ packages/tui/src/theme/v2/schema.ts | 2 +- packages/tui/test/theme/v2/resolve.test.ts | 11 +- script/generate.ts | 2 + 11 files changed, 381 insertions(+), 7 deletions(-) create mode 100644 packages/docs/script/generate-theme-tokens.ts create mode 100644 packages/docs/snippets/generated/theme-tokens.mdx create mode 100644 packages/docs/themes.mdx diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b486b68a93b7..1ae28ea87449 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -97,6 +97,11 @@ jobs: working-directory: packages/client run: bun run check:generated + - name: Check generated documentation + if: runner.os == 'Linux' + working-directory: packages/docs + run: bun run check:generated + e2e: name: e2e (${{ matrix.settings.name }}) if: github.ref_name != 'v2' && github.head_ref != 'v2' diff --git a/packages/docs/README.md b/packages/docs/README.md index 17b06848e1f0..1aff8cf6dc30 100644 --- a/packages/docs/README.md +++ b/packages/docs/README.md @@ -19,4 +19,15 @@ bun validate bun broken-links ``` +The V2 theme token reference is generated from +`packages/tui/src/theme/v2/schema.ts`. Regenerate it after schema changes: + +```bash +bun run generate +``` + +`bun validate` checks that the committed snippet is current. The repository's +generation workflow also refreshes it on pushes to `dev`, so Mintlify always +receives the generated MDX as part of the published docs tree. + The hosted preview is available at [opencode.mintlify.site](https://opencode.mintlify.site). diff --git a/packages/docs/docs.json b/packages/docs/docs.json index ff41ab4b92ed..a9349f29639f 100644 --- a/packages/docs/docs.json +++ b/packages/docs/docs.json @@ -38,6 +38,7 @@ "attachments", "compaction", "warming", + "themes", "formatters", "lsp", "references" diff --git a/packages/docs/index.mdx b/packages/docs/index.mdx index 909585390347..bbb3ccfdfe5e 100644 --- a/packages/docs/index.mdx +++ b/packages/docs/index.mdx @@ -158,6 +158,6 @@ limitations and safety details. ## Customize -Make OpenCode your own by [picking a theme](https://opencode.ai/docs/themes), [customizing +Make OpenCode your own by [picking a theme](/themes), [customizing keybinds](https://opencode.ai/docs/keybinds), [configuring formatters](/formatters), [creating commands](/commands), or editing the [OpenCode config](/config). diff --git a/packages/docs/package.json b/packages/docs/package.json index 01f35998c52d..b8a9ead7cae2 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -3,11 +3,15 @@ "name": "@opencode-ai/docs", "private": true, "scripts": { - "dev": "bun --bun mint dev --no-open --port 3333", - "validate": "bun --bun mint validate", + "dev": "bun run generate && bun --bun mint dev --no-open --port 3333", + "generate": "bun script/generate-theme-tokens.ts", + "check:generated": "bun script/generate-theme-tokens.ts --check", + "validate": "bun run check:generated && bun --bun mint validate", "broken-links": "bun --bun mint broken-links" }, "devDependencies": { - "mint": "4.2.666" + "effect": "catalog:", + "mint": "4.2.666", + "prettier": "3.6.2" } } diff --git a/packages/docs/script/generate-theme-tokens.ts b/packages/docs/script/generate-theme-tokens.ts new file mode 100644 index 000000000000..0c441075f962 --- /dev/null +++ b/packages/docs/script/generate-theme-tokens.ts @@ -0,0 +1,136 @@ +#!/usr/bin/env bun + +import { Schema, SchemaAST } from "effect" +import { format } from "prettier" +import { ThemeDefinition, ThemeFile } from "../../tui/src/theme/v2/schema" + +const target = import.meta.dir + "/../snippets/generated/theme-tokens.mdx" +const root = requireObject(ThemeDefinition.ast) +const hue = requireObject(requireField(root, "hue").type) +const hueNames = hue.propertySignatures.map((field) => String(field.name)) +const hueSteps = requireObject(requireField(hue, hueNames[0]).type).propertySignatures.map((field) => + String(field.name), +) +const contexts = root.propertySignatures + .map((field) => String(field.name)) + .filter((name) => name.startsWith("@context:")) +const tokens = root.propertySignatures + .filter((field) => { + const name = String(field.name) + return name !== "hue" && name !== "categorical" && !name.startsWith("@context:") + }) + .flatMap((field) => tokenPaths(field.type, String(field.name))) +const groups = Map.groupBy(tokens, (token) => + token + .split(".") + .slice(0, token.split(".").length > 2 ? 2 : 1) + .join("."), +) +const table = [...groups] + .map(([group, values]) => `| \`${group}\` | ${values.map((value) => `\`${value}\``).join("
    ")} |`) + .join("\n") +const example = { + version: 2, + light: { + hue: { + accent: "$hue.purple", + interactive: "$hue.purple", + }, + text: { + default: "$hue.neutral.900", + }, + background: { + default: "#fafafa", + }, + }, + dark: { + mergeMode: true, + text: { + default: "$hue.neutral.100", + }, + background: { + default: "#101014", + }, + }, +} satisfies ThemeFile +Schema.decodeUnknownSync(ThemeFile)(example) +const output = await format( + `{/* Generated by packages/docs/script/generate-theme-tokens.ts. Do not edit. */} + +\`\`\`json title="my-theme.json" +${JSON.stringify(example, null, 2)} +\`\`\` + +## Token reference + +This reference is generated from the Effect schema in +\`packages/tui/src/theme/v2/schema.ts\`. Changes to the runtime schema update +this section through \`bun run generate\`. + +### Hue tokens + +Every hue is a ${hueSteps.length}-step scale. Define a scale with all of these +steps, or alias it to another hue with a value such as \`$hue.blue\`. + +| | Values | +| --- | --- | +| Hues | ${hueNames.map((name) => `\`${name}\``).join(", ")} | +| Steps | ${hueSteps.map((step) => `\`${step}\``).join(", ")} | + +Reference a hue color as \`$hue..\`, for example +\`$hue.interactive.500\`. + +### Semantic tokens + +Semantic values can reference another token by prefixing its path with \`$\`, +for example \`$text.default\`. Stateful tokens inherit their \`default\` +value when a state is omitted. + +| Group | Tokens | +| --- | --- | +${table} + +### Contexts + +${contexts.map((context) => `\`${context}\``).join(" and ")} accept partial +overrides of the semantic tokens above. Components apply these contexts to +surfaces that need different contrast without changing the base theme. +`, + { parser: "mdx", printWidth: 120, semi: false }, +) + +if (process.argv.includes("--check")) { + const current = await Bun.file(target).text() + if (current === output) process.exit(0) + console.error("Generated theme token documentation is stale. Run `bun run generate` from packages/docs.") + process.exit(1) +} + +await Bun.write(target, output) + +function requireObject(ast: SchemaAST.AST): SchemaAST.Objects { + if (SchemaAST.isObjects(ast)) return ast + if (SchemaAST.isUnion(ast)) { + const object = ast.types.map(findObject).find((value) => value !== undefined) + if (object) return object + } + throw new Error(`Expected an object schema, received ${ast._tag}`) +} + +function findObject(ast: SchemaAST.AST): SchemaAST.Objects | undefined { + if (SchemaAST.isObjects(ast)) return ast + if (SchemaAST.isUnion(ast)) return ast.types.map(findObject).find((value) => value !== undefined) + if (SchemaAST.isSuspend(ast)) return findObject(ast.thunk()) +} + +function requireField(ast: SchemaAST.Objects, name: string) { + const field = ast.propertySignatures.find((field) => String(field.name) === name) + if (field) return field + throw new Error(`Theme schema field not found: ${name}`) +} + +function tokenPaths(ast: SchemaAST.AST, prefix: string): string[] { + const object = findObject(ast) + if (!object || object.propertySignatures.length === 0) return [prefix] + return object.propertySignatures.flatMap((field) => tokenPaths(field.type, `${prefix}.${String(field.name)}`)) +} diff --git a/packages/docs/snippets/generated/theme-tokens.mdx b/packages/docs/snippets/generated/theme-tokens.mdx new file mode 100644 index 000000000000..932072f6cb4c --- /dev/null +++ b/packages/docs/snippets/generated/theme-tokens.mdx @@ -0,0 +1,79 @@ +{/* Generated by packages/docs/script/generate-theme-tokens.ts. Do not edit. */} + +```json title="my-theme.json" +{ + "version": 2, + "light": { + "hue": { + "accent": "$hue.purple", + "interactive": "$hue.purple" + }, + "text": { + "default": "$hue.neutral.900" + }, + "background": { + "default": "#fafafa" + } + }, + "dark": { + "mergeMode": true, + "text": { + "default": "$hue.neutral.100" + }, + "background": { + "default": "#101014" + } + } +} +``` + +## Token reference + +This reference is generated from the Effect schema in +`packages/tui/src/theme/v2/schema.ts`. Changes to the runtime schema update +this section through `bun run generate`. + +### Hue tokens + +Every hue is a 9-step scale. Define a scale with all of these +steps, or alias it to another hue with a value such as `$hue.blue`. + +| | Values | +| ----- | -------------------------------------------------------------------------------------------------------- | +| Hues | `gray`, `red`, `orange`, `yellow`, `green`, `cyan`, `blue`, `purple`, `accent`, `interactive`, `neutral` | +| Steps | `100`, `200`, `300`, `400`, `500`, `600`, `700`, `800`, `900` | + +Reference a hue color as `$hue..`, for example +`$hue.interactive.500`. + +### Semantic tokens + +Semantic values can reference another token by prefixing its path with `$`, +for example `$text.default`. Stateful tokens inherit their `default` +value when a state is omitted. + +| Group | Tokens | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `text` | `text.default`
    `text.subdued` | +| `text.action` | `text.action.primary.default`
    `text.action.primary.$hovered`
    `text.action.primary.$focused`
    `text.action.primary.$pressed`
    `text.action.primary.$selected`
    `text.action.primary.$disabled`
    `text.action.destructive.default`
    `text.action.destructive.$hovered`
    `text.action.destructive.$focused`
    `text.action.destructive.$pressed`
    `text.action.destructive.$selected`
    `text.action.destructive.$disabled` | +| `text.formfield` | `text.formfield.default`
    `text.formfield.$hovered`
    `text.formfield.$focused`
    `text.formfield.$pressed`
    `text.formfield.$selected`
    `text.formfield.$disabled` | +| `text.feedback` | `text.feedback.error.default`
    `text.feedback.error.subdued`
    `text.feedback.warning.default`
    `text.feedback.warning.subdued`
    `text.feedback.success.default`
    `text.feedback.success.subdued`
    `text.feedback.info.default`
    `text.feedback.info.subdued` | +| `background` | `background.default` | +| `background.surface` | `background.surface.offset`
    `background.surface.overlay` | +| `background.action` | `background.action.primary.default`
    `background.action.primary.$hovered`
    `background.action.primary.$focused`
    `background.action.primary.$pressed`
    `background.action.primary.$selected`
    `background.action.primary.$disabled`
    `background.action.destructive.default`
    `background.action.destructive.$hovered`
    `background.action.destructive.$focused`
    `background.action.destructive.$pressed`
    `background.action.destructive.$selected`
    `background.action.destructive.$disabled` | +| `background.formfield` | `background.formfield.default`
    `background.formfield.$hovered`
    `background.formfield.$focused`
    `background.formfield.$pressed`
    `background.formfield.$selected`
    `background.formfield.$disabled` | +| `background.feedback` | `background.feedback.error.default`
    `background.feedback.warning.default`
    `background.feedback.success.default`
    `background.feedback.info.default` | +| `border` | `border.default` | +| `scrollbar` | `scrollbar.default` | +| `diff.text` | `diff.text.added`
    `diff.text.removed`
    `diff.text.context`
    `diff.text.hunkHeader` | +| `diff.background` | `diff.background.added`
    `diff.background.removed`
    `diff.background.context` | +| `diff.highlight` | `diff.highlight.added`
    `diff.highlight.removed` | +| `diff.lineNumber` | `diff.lineNumber.text`
    `diff.lineNumber.background.added`
    `diff.lineNumber.background.removed` | +| `syntax` | `syntax.comment`
    `syntax.keyword`
    `syntax.function`
    `syntax.variable`
    `syntax.string`
    `syntax.number`
    `syntax.type`
    `syntax.operator`
    `syntax.punctuation` | +| `markdown` | `markdown.text`
    `markdown.heading`
    `markdown.link`
    `markdown.linkText`
    `markdown.code`
    `markdown.blockQuote`
    `markdown.emphasis`
    `markdown.strong`
    `markdown.horizontalRule`
    `markdown.listItem`
    `markdown.listEnumeration`
    `markdown.image`
    `markdown.imageText`
    `markdown.codeBlock` | + +### Contexts + +`@context:elevated` and `@context:overlay` accept partial +overrides of the semantic tokens above. Components apply these contexts to +surfaces that need different contrast without changing the base theme. diff --git a/packages/docs/themes.mdx b/packages/docs/themes.mdx new file mode 100644 index 000000000000..49f41768f603 --- /dev/null +++ b/packages/docs/themes.mdx @@ -0,0 +1,129 @@ +--- +title: "Themes" +description: "Choose a built-in TUI theme or create a custom color scheme." +--- + +import ThemeTokens from "/snippets/generated/theme-tokens.mdx" + +OpenCode includes built-in light and dark themes and can load custom themes +from your global configuration or a project directory. The default theme is +`opencode`. + +## Choose a theme + +In the full-screen TUI, run: + +```text +/themes +``` + +You can also open the picker with `ctrl+x`, then `t`, using the +default keybindings. + +Use `/settings` to change both the theme and its color mode. OpenCode supports +three modes: + +| Mode | Behavior | +| -------- | -------------------------------------------------------- | +| `system` | Follow the terminal's detected light or dark appearance. | +| `dark` | Always use the theme's dark colors. | +| `light` | Always use the theme's light colors. | + +Your selection is stored in `~/.config/opencode/cli.json`, or the equivalent +path under `$XDG_CONFIG_HOME`: + +```json title="cli.json" +{ + "theme": { + "name": "tokyonight", + "mode": "system" + } +} +``` + + + Theme selection applies to the full-screen TUI. Direct interactive runs use colors derived from the terminal palette + and honor only the color mode. + + +## Built-in themes + +OpenCode currently includes: + +| | | | +| ------------ | ------------------- | ---------------------- | +| `aura` | `ayu` | `carbonfox` | +| `catppuccin` | `catppuccin-frappe` | `catppuccin-macchiato` | +| `cobalt2` | `cursor` | `dracula` | +| `everforest` | `flexoki` | `github` | +| `gruvbox` | `kanagawa` | `lucent-orng` | +| `material` | `matrix` | `mercury` | +| `monokai` | `nightowl` | `nord` | +| `one-dark` | `opencode` | `orng` | +| `osaka-jade` | `palenight` | `rosepine` | +| `solarized` | `synthwave84` | `tokyonight` | +| `vercel` | `vesper` | `zenburn` | + +When OpenCode can read your terminal palette, the picker also includes +`system`. The `system` theme generates its colors from your terminal's +foreground, background, and ANSI palette. + +## Custom themes + +Create a JSON file in either of these locations: + +```text +~/.config/opencode/themes/my-theme.json +.opencode/themes/my-theme.json +``` + +OpenCode checks the global theme directory first, followed by every +`.opencode/themes` directory from the filesystem root down to the current +directory. A more local file with the same filename overrides an earlier one. +The filename becomes the theme name, so `my-theme.json` appears as `my-theme`. + +Custom theme files must be strict JSON. Comments and trailing commas are not +supported. + +### Format + +V2 themes organize colors into hue scales and semantic tokens. Set `version` +to `2` and define at least one of `light` or `dark`: + + + Native V2 custom theme files are not loaded directly by the current beta. Existing custom files use the V1 format and + are migrated to these tokens at runtime. This reference tracks the native V2 schema while direct file loading is + completed. + + +By default, a theme inherits OpenCode's complete theme, so you only need to +define overrides. Set `mergeMode` to `true` to inherit one mode from the other +before applying that mode's overrides. Set `standalone` to `true` only when you +intend to supply a complete independent theme. + +Each token accepts: + +- A hex color such as `"#5c9cf5"` +- `"transparent"` to use the terminal default +- A hue reference such as `"$hue.blue.500"` +- Another semantic token reference such as `"$text.default"` + +Syntax and markdown tokens accept hex colors and hue references. Other +semantic tokens can reference any semantic token. + + + +If you add or edit a custom theme while OpenCode is running, restart the TUI to +reload it. + +## Terminal colors + +Themes display most accurately in a terminal with truecolor support. Check +your terminal with: + +```bash +echo $COLORTERM +``` + +Most modern terminals report `truecolor` or `24bit`. Without truecolor, +OpenCode approximates theme colors using the available terminal palette. diff --git a/packages/tui/src/theme/v2/schema.ts b/packages/tui/src/theme/v2/schema.ts index 076bdfc63533..76a33f7a0999 100644 --- a/packages/tui/src/theme/v2/schema.ts +++ b/packages/tui/src/theme/v2/schema.ts @@ -243,7 +243,7 @@ const MergeModeDefinition = Schema.Struct({ "@context:overlay": Schema.optional(ThemeTokensDefinition), }) export type MergeModeDefinition = Schema.Schema.Type -export const ModeDefinition = Schema.Union([FileThemeDefinition, MergeModeDefinition]) +export const ModeDefinition = Schema.Union([MergeModeDefinition, FileThemeDefinition]) export type ModeDefinition = Schema.Schema.Type const FileMetadata = { diff --git a/packages/tui/test/theme/v2/resolve.test.ts b/packages/tui/test/theme/v2/resolve.test.ts index 237b4c79fc04..e91a79d2b20a 100644 --- a/packages/tui/test/theme/v2/resolve.test.ts +++ b/packages/tui/test/theme/v2/resolve.test.ts @@ -192,8 +192,15 @@ test("standalone themes skip OpenCode defaults and use the red core fallback", ( }) test("uses defaults for the selected mode when it merges the other mode", () => { - const theme = resolveThemeFile({ version: 2, light: { hue: light.hue }, dark: { mergeMode: true } }, "dark") - expect(theme.background.default.toInts()).toEqual(resolveTheme(dark).background.default.toInts()) + const theme = resolveThemeFile( + { + version: 2, + light: { hue: light.hue, background: { default: "#123456" } }, + dark: { mergeMode: true }, + }, + "dark", + ) + expect(theme.background.default.toInts()).toEqual([18, 52, 86, 255]) }) test("resolves matched action variants and states", () => { diff --git a/script/generate.ts b/script/generate.ts index 8fc251d89d41..dbf38f8a3c21 100755 --- a/script/generate.ts +++ b/script/generate.ts @@ -6,4 +6,6 @@ await $`bun ./packages/sdk/js/script/build.ts` await $`bun dev generate > ../sdk/openapi.json`.cwd("packages/opencode") +await $`bun run generate`.cwd("packages/docs") + await $`./script/format.ts` From a817fe5e6ce078918a1488b390b4b08a1852ee14 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 22 Jul 2026 19:20:28 -0400 Subject: [PATCH 041/150] fix(schema): loosen agent color response --- packages/client/src/promise/generated/types.ts | 4 +++- packages/schema/src/agent.ts | 4 +--- packages/schema/test/agent.test.ts | 10 ++++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 packages/schema/test/agent.test.ts diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 966ec22cc03c..a08c7b324689 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -8,6 +8,8 @@ export type ModelRef = { id: string; providerID: string; variant?: string } export type ProviderSettings = { [x: string]: JsonValue } +export type AgentColor = string + export type PermissionV2Effect = "allow" | "deny" | "ask" export type PluginInfo = { id: string } @@ -2003,7 +2005,7 @@ export type AgentInfo = { description?: string mode: "subagent" | "primary" | "all" hidden: boolean - color?: string + color?: AgentColor steps?: number permissions: PermissionV2Ruleset } diff --git a/packages/schema/src/agent.ts b/packages/schema/src/agent.ts index 399e9e9131e0..8001a2967ee0 100644 --- a/packages/schema/src/agent.ts +++ b/packages/schema/src/agent.ts @@ -16,9 +16,7 @@ export type ID = typeof ID.Type export const Name = Schema.String.pipe(Schema.brand("Agent.Name")) export type Name = typeof Name.Type -export const Color = Schema.String.annotate({ identifier: "Agent.Color" }).check( - Schema.isPattern(/^#[0-9a-fA-F]{6}$/), -) +export const Color = Schema.String.annotate({ identifier: "Agent.Color" }) export type Color = typeof Color.Type export interface Info extends Schema.Schema.Type {} diff --git a/packages/schema/test/agent.test.ts b/packages/schema/test/agent.test.ts new file mode 100644 index 000000000000..30535ebaecfc --- /dev/null +++ b/packages/schema/test/agent.test.ts @@ -0,0 +1,10 @@ +import { expect, test } from "bun:test" +import { Schema } from "effect" +import { Agent } from "../src/agent.js" + +test("Agent.Color preserves configured colors at the public boundary", () => { + const encode = Schema.encodeSync(Agent.Color) + + expect(encode("info")).toBe("info") + expect(encode("custom-color")).toBe("custom-color") +}) From d86f732df325a4d5933b47a34c5759cdad784117 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 22 Jul 2026 21:33:25 -0400 Subject: [PATCH 042/150] refactor(tui): generate syntax from V2 theme (#38397) --- packages/tui/src/context/theme.tsx | 6 +- packages/tui/src/theme/v2/syntax.ts | 93 +++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 packages/tui/src/theme/v2/syntax.ts diff --git a/packages/tui/src/context/theme.tsx b/packages/tui/src/context/theme.tsx index aff085e969ed..16ffdeaccbee 100644 --- a/packages/tui/src/context/theme.tsx +++ b/packages/tui/src/context/theme.tsx @@ -4,10 +4,8 @@ import { DEFAULT_THEMES, addTheme, allThemes, - generateSyntax, hasTheme, isTheme, - resolveTheme, selectedForeground, setCustomThemes, setSystemTheme, @@ -16,6 +14,7 @@ import { type Theme, type ThemeJson, } from "../theme" +import { generateSyntax } from "../theme/v2/syntax" import { generateSystem, terminalMode } from "../theme/system" import { discoverThemes, themeDirectories } from "../theme/discovery" import { createComponentTheme, type ComponentTheme } from "../theme/v2/component" @@ -279,7 +278,6 @@ const themeContext = createSimpleContext({ if (supported.includes(store.mode)) return store.mode return supported[0] ?? store.mode } - const legacySyntaxTheme = createMemo(() => resolveTheme(source(), mode())) const valuesV2 = createMemo(() => resolveThemeFile(file(), mode(), sourceName())) valuesV2() themePerformance.set("Init", `${(performance.now() - initStarted).toFixed(2)} ms`) @@ -299,7 +297,7 @@ const themeContext = createSimpleContext({ createEffect(() => renderer.setBackgroundColor(valuesV2().background.default)) - const syntax = createSyntaxStyleMemo(() => generateSyntax(legacySyntaxTheme())) + const syntax = createSyntaxStyleMemo(() => generateSyntax(valuesV2(), mode())) function contextual(context: ContextName) { return contextualServices[context] } diff --git a/packages/tui/src/theme/v2/syntax.ts b/packages/tui/src/theme/v2/syntax.ts new file mode 100644 index 000000000000..4a8917f089d6 --- /dev/null +++ b/packages/tui/src/theme/v2/syntax.ts @@ -0,0 +1,93 @@ +import { SyntaxStyle, type RGBA, type ThemeTokenStyle } from "@opentui/core" +import type { Mode, ResolvedThemeView } from "./index" + +export function generateSyntax(theme: ResolvedThemeView, mode: Mode) { + const step = mode === "light" ? 800 : 200 + const syntax = theme.syntax + const markdown = theme.markdown + const feedback = theme.text.feedback + + return SyntaxStyle.fromTheme([ + rule(["default"], theme.text.default), + rule(["prompt"], theme.hue.accent[step]), + rule(["extmark.file"], feedback.warning.default, { bold: true }), + rule(["extmark.agent"], theme.categorical[0][step], { bold: true }), + // V1 migration preserves its selected/inverse foreground in this action state. + rule(["extmark.paste"], theme.text.action.primary.focused, { + background: feedback.warning.default, + bold: true, + }), + rule(["comment", "comment.documentation"], syntax.comment, { italic: true }), + rule(["string", "symbol", "character.special", "character"], syntax.string), + rule(["number", "boolean", "constant", "float"], syntax.number), + rule(["keyword.return", "keyword.conditional", "keyword.repeat", "keyword.coroutine"], syntax.keyword, { + italic: true, + }), + rule(["keyword.type"], syntax.type, { bold: true, italic: true }), + rule(["keyword.function", "function.method"], syntax.function), + rule(["keyword"], syntax.keyword, { italic: true }), + rule(["keyword.import", "string.escape", "string.regexp", "tag.attribute", "keyword.export"], syntax.keyword), + rule(["operator", "keyword.operator", "punctuation.delimiter", "keyword.conditional.ternary"], syntax.operator), + rule( + ["variable", "variable.parameter", "function.method.call", "function.call", "property", "parameter", "field"], + syntax.variable, + ), + rule(["variable.member", "function", "constructor"], syntax.function), + rule(["type", "module", "class", "namespace"], syntax.type), + rule(["type.definition"], syntax.type, { bold: true }), + rule(["punctuation", "punctuation.bracket"], syntax.punctuation), + rule( + ["variable.builtin", "type.builtin", "function.builtin", "module.builtin", "constant.builtin", "variable.super"], + feedback.error.default, + ), + rule(["keyword.directive", "keyword.modifier", "keyword.exception"], syntax.keyword, { italic: true }), + rule(["punctuation.special", "tag.delimiter"], syntax.operator), + rule( + [ + "markup.heading", + "markup.heading.2", + "markup.heading.3", + "markup.heading.4", + "markup.heading.5", + "markup.heading.6", + ], + markdown.heading, + { bold: true }, + ), + rule(["markup.heading.1"], markdown.heading, { bold: true, underline: true }), + rule(["markup.bold", "markup.strong"], markdown.strong, { bold: true }), + rule(["markup.italic"], markdown.emphasis, { italic: true }), + rule(["markup.list"], markdown.listItem), + rule(["markup.quote"], markdown.blockQuote, { italic: true }), + rule(["markup.raw", "markup.raw.block"], markdown.code), + rule(["markup.raw.inline"], markdown.code, { background: theme.background.default }), + rule(["markup.link", "markup.link.url", "string.special", "string.special.url"], markdown.link, { + underline: true, + }), + rule(["markup.link.label"], markdown.linkText, { underline: true }), + rule(["label"], markdown.linkText), + rule(["spell", "nospell"], theme.text.default), + rule(["markup.underline"], theme.text.default, { underline: true }), + rule(["comment.error"], feedback.error.default, { italic: true, bold: true }), + rule(["comment.warning"], feedback.warning.default, { italic: true, bold: true }), + rule(["comment.todo", "comment.note"], feedback.info.default, { italic: true, bold: true }), + rule(["attribute", "annotation"], feedback.warning.default), + rule(["tag"], feedback.error.default), + rule(["markup.strikethrough", "markup.list.unchecked", "debug"], theme.text.subdued), + rule(["markup.list.checked"], feedback.success.default), + rule(["diff.plus"], theme.diff.text.added, { background: theme.diff.background.added }), + rule(["diff.minus"], theme.diff.text.removed, { background: theme.diff.background.removed }), + rule(["diff.delta"], theme.diff.text.context, { background: theme.diff.background.context }), + rule(["error"], feedback.error.default, { bold: true }), + rule(["warning"], feedback.warning.default, { bold: true }), + rule(["info"], feedback.info.default), + ]) +} + +function rule( + scope: string[], + foreground: RGBA, + style: Omit = {}, +): ThemeTokenStyle { + return { scope, style: { foreground, ...style } } +} From 48bcbd09efe4eda454ed862909956f60e94f064f Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 22 Jul 2026 20:49:08 -0500 Subject: [PATCH 043/150] fix(ai): handle incomplete responses without reasons (#38374) --- packages/ai/src/protocols/openai-responses.ts | 5 ++-- .../ai/test/provider/openai-responses.test.ts | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/ai/src/protocols/openai-responses.ts b/packages/ai/src/protocols/openai-responses.ts index 87ca8c75e1a4..2973acf00054 100644 --- a/packages/ai/src/protocols/openai-responses.ts +++ b/packages/ai/src/protocols/openai-responses.ts @@ -253,7 +253,7 @@ const OpenAIResponsesEvent = Schema.Struct({ Schema.Struct({ id: Schema.optional(Schema.String), service_tier: optionalNull(Schema.String), - incomplete_details: optionalNull(Schema.Struct({ reason: Schema.String })), + incomplete_details: optionalNull(Schema.Struct({ reason: Schema.optional(Schema.String) })), usage: optionalNull(OpenAIResponsesUsage), error: optionalNull(OpenAIResponsesErrorPayload), }), @@ -602,7 +602,8 @@ const mapUsage = (usage: OpenAIResponsesUsage | null | undefined) => { const mapFinishReason = (event: OpenAIResponsesEvent, hasFunctionCall: boolean): FinishReason => { const reason = event.response?.incomplete_details?.reason - if (reason === undefined || reason === null) return hasFunctionCall ? "tool-calls" : "stop" + if (reason === undefined || reason === null) + return hasFunctionCall ? "tool-calls" : event.type === "response.incomplete" ? "unknown" : "stop" if (reason === "max_output_tokens") return "length" if (reason === "content_filter") return "content-filter" return hasFunctionCall ? "tool-calls" : "unknown" diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index 11466a6896f9..ffbd3294341c 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -870,6 +870,32 @@ describe("OpenAI Responses route", () => { }), ) + it.effect("maps incomplete response reasons", () => + Effect.gen(function* () { + const generate = (incompleteDetails: object) => + LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents({ + type: "response.incomplete", + response: { id: "resp_incomplete", incomplete_details: incompleteDetails }, + }), + ), + ), + ) + + const length = yield* generate({ reason: "max_output_tokens" }) + const contentFilter = yield* generate({ reason: "content_filter" }) + const unknown = yield* generate({}) + + expect([length.finishReason, contentFilter.finishReason, unknown.finishReason]).toEqual([ + "length", + "content-filter", + "unknown", + ]) + }), + ) + // OpenAI's documented stream orders output text within one message item; no // provider-valid same-kind overlap is evidenced, so done boundaries close it. it.effect("closes sequential output messages before starting the next", () => From 203b9f59b73b695664d08834fba98fb630ca3421 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 22 Jul 2026 22:26:08 -0400 Subject: [PATCH 044/150] fix(core): load dynamic models for generation (#38401) --- packages/core/src/generate.ts | 78 ++-- packages/core/src/location-services.ts | 2 + packages/core/src/model-resolver.ts | 344 ++++++++++++++++++ .../core/src/plugin/provider/openai-codex.ts | 2 +- packages/core/src/session/runner/model.ts | 344 ++---------------- packages/core/test/generate.test.ts | 111 ++++++ ...r-model.test.ts => model-resolver.test.ts} | 130 ++----- packages/core/test/session-compact.test.ts | 17 +- packages/core/test/session-generate.test.ts | 17 +- .../core/test/session-runner-recorded.test.ts | 17 +- packages/core/test/session-runner.test.ts | 21 +- packages/core/test/tool-search.test.ts | 4 +- 12 files changed, 585 insertions(+), 502 deletions(-) create mode 100644 packages/core/src/model-resolver.ts create mode 100644 packages/core/test/generate.test.ts rename packages/core/test/{session-runner-model.test.ts => model-resolver.test.ts} (80%) diff --git a/packages/core/src/generate.ts b/packages/core/src/generate.ts index f4a79e9aa260..432827bce0b6 100644 --- a/packages/core/src/generate.ts +++ b/packages/core/src/generate.ts @@ -2,12 +2,10 @@ export * as Generate from "./generate" import { LLM, LLMClient, LLMError } from "@opencode-ai/ai" import { Context, Effect, Layer, Schema } from "effect" -import { Catalog } from "./catalog" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { llmClient } from "./effect/app-node-platform" -import { Integration } from "./integration" +import { ModelResolver } from "./model-resolver" import { ModelV2 } from "./model" -import { SessionRunnerModel } from "./session/runner/model" export interface TextInput { readonly prompt: string @@ -19,10 +17,10 @@ export class ModelSelectionError extends Schema.TaggedErrorClass()( - "Generate.UnavailableError", - { message: Schema.String, service: Schema.optional(Schema.String) }, -) {} +export class UnavailableError extends Schema.TaggedErrorClass()("Generate.UnavailableError", { + message: Schema.String, + service: Schema.optional(Schema.String), +}) {} export type Error = ModelSelectionError | UnavailableError @@ -35,56 +33,34 @@ export class Service extends Context.Service()("@opencode/v2 export const layer = Layer.effect( Service, Effect.gen(function* () { - const catalog = yield* Catalog.Service - const integrations = yield* Integration.Service const llm = yield* LLMClient.Service + const resolver = yield* ModelResolver.Service - const selectModel = Effect.fn("Generate.selectModel")(function* (requested?: ModelV2.Ref) { - const selected = requested - ? yield* catalog.model.get(requested.providerID, requested.id) - : yield* catalog.model.default().pipe( - Effect.flatMap((model) => - model && SessionRunnerModel.supported(model) - ? Effect.succeed(model) - : Effect.map(catalog.model.available(), (models) => models.find(SessionRunnerModel.supported)), - ), - ) - if (!selected) + const runText = Effect.fn("Generate.text")(function* (input: TextInput) { + const resolved = yield* resolver.resolve(input.model).pipe( + Effect.catchTags({ + "SessionRunnerModel.VariantUnavailableError": (error) => + input.model + ? new ModelSelectionError({ message: error.message }) + : new UnavailableError({ message: error.message, service: error.providerID }), + "SessionRunnerModel.UnsupportedPackageError": (error) => + input.model + ? new ModelSelectionError({ message: error.message }) + : new UnavailableError({ message: error.message, service: error.providerID }), + }), + ) + if (!resolved) return yield* new ModelSelectionError({ - message: requested - ? `Model unavailable: ${requested.providerID}/${requested.id}` + message: input.model + ? `Model unavailable: ${input.model.providerID}/${input.model.id}` : "No model specified and no supported model is available", }) - return yield* SessionRunnerModel.withVariant(selected, requested?.variant).pipe( - Effect.mapError( - () => - new ModelSelectionError({ - message: `Variant unavailable for ${selected.providerID}/${selected.id}: ${requested?.variant}`, - }), - ), - ) - }) - - const runText = Effect.fn("Generate.text")(function* (input: TextInput) { - const selected = yield* selectModel(input.model) - const provider = yield* catalog.provider.get(selected.providerID) - const connection = yield* integrations.connection.active( - provider?.integrationID ?? Integration.ID.make(selected.providerID), - ) - const credential = connection ? yield* integrations.connection.resolve(connection) : undefined - const model = yield* SessionRunnerModel.fromCatalogModel(selected, credential).pipe( - Effect.mapError((error) => - input.model - ? new ModelSelectionError({ message: error.message }) - : new UnavailableError({ message: error.message, service: selected.providerID }), - ), - ) - const response = yield* llm.generate(LLM.request({ model, prompt: input.prompt })).pipe( + const response = yield* llm.generate(LLM.request({ model: resolved.model, prompt: input.prompt })).pipe( Effect.mapError( (error: LLMError) => new UnavailableError({ message: error.message, - service: selected.providerID, + service: resolved.ref.providerID, }), ), ) @@ -106,4 +82,8 @@ export const layer = Layer.effect( }), ) -export const node = makeLocationNode({ service: Service, layer, deps: [Catalog.node, Integration.node, llmClient] }) +export const node = makeLocationNode({ + service: Service, + layer, + deps: [ModelResolver.node, llmClient], +}) diff --git a/packages/core/src/location-services.ts b/packages/core/src/location-services.ts index 7be9fefe3fbe..adf1afe62bea 100644 --- a/packages/core/src/location-services.ts +++ b/packages/core/src/location-services.ts @@ -20,6 +20,7 @@ import { Integration } from "./integration" import { Location } from "./location" import { LocationMutation } from "./location-mutation" import { LocationServiceMap } from "./location-service-map" +import { ModelResolver } from "./model-resolver" import { MCP } from "./mcp/index" import { PermissionV2 } from "./permission" import { PluginV2 } from "./plugin" @@ -58,6 +59,7 @@ const locationServiceNodes = [ Reference.node, Integration.node, Catalog.node, + ModelResolver.node, AISDK.node, PluginV2.node, PluginSupervisor.node, diff --git a/packages/core/src/model-resolver.ts b/packages/core/src/model-resolver.ts new file mode 100644 index 000000000000..44faea8b12d5 --- /dev/null +++ b/packages/core/src/model-resolver.ts @@ -0,0 +1,344 @@ +export * as ModelResolver from "./model-resolver" + +import { makeLocationNode } from "@opencode-ai/util/effect/app-node" +import { Model } from "@opencode-ai/ai" +// ast-grep-ignore: no-star-import +import * as AnthropicMessages from "@opencode-ai/ai/protocols/anthropic-messages" +// ast-grep-ignore: no-star-import +import * as OpenAICompatibleChat from "@opencode-ai/ai/protocols/openai-compatible-chat" +// ast-grep-ignore: no-star-import +import * as OpenAIResponses from "@opencode-ai/ai/protocols/openai-responses" +import { Auth, type AnyRoute } from "@opencode-ai/ai/route" +import { Context, Effect, Layer, Schema } from "effect" +import { produce } from "immer" +import { AISDK } from "./aisdk" +import { Catalog } from "./catalog" +import { Credential } from "./credential" +import { Integration } from "./integration" +import { ModelV2 } from "./model" +import { Npm } from "@opencode-ai/util/npm" +import { OpenAICodex } from "./plugin/provider/openai-codex" +import { ProviderV2 } from "./provider" + +export class VariantUnavailableError extends Schema.TaggedErrorClass()( + "SessionRunnerModel.VariantUnavailableError", + { + providerID: ProviderV2.ID, + modelID: ModelV2.ID, + variant: ModelV2.VariantID, + }, +) { + override get message() { + return `Variant unavailable for ${this.providerID}/${this.modelID}: ${this.variant}` + } +} + +export class UnsupportedPackageError extends Schema.TaggedErrorClass()( + "SessionRunnerModel.UnsupportedPackageError", + { + providerID: ProviderV2.ID, + modelID: ModelV2.ID, + package: Schema.String, + }, +) { + override get message() { + return `Unsupported package for ${this.providerID}/${this.modelID}: ${this.package}` + } +} + +export type Error = VariantUnavailableError | UnsupportedPackageError | Integration.AuthorizationError + +export interface Resolved { + /** Route-level model for provider requests; its id is the provider API model id, which may differ from the catalog id. */ + readonly model: Model + /** Selected catalog identity. Durable records and displays must use this, never the API model id. */ + readonly ref: ModelV2.Ref + /** Catalog capabilities used to shape requests before provider lowering. */ + readonly capabilities: ModelV2.Capabilities + /** Catalog pricing in dollars per million tokens. */ + readonly cost: ModelV2.Info["cost"] +} + +export interface Interface { + readonly resolve: (requested?: ModelV2.Ref) => Effect.Effect + readonly resolveModel: (model: ModelV2.Info, variant?: ModelV2.VariantID) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/ModelResolver") {} + +const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => { + if (credential?.type === "key") return Auth.value(credential.key) + if (credential?.type === "oauth") return Auth.value(credential.access) + const value = model.settings?.apiKey + if (typeof value === "string") return Auth.value(value) + return undefined +} + +const withDefaults = (model: ModelV2.Info, route: AnyRoute) => + route.with({ + provider: model.providerID, + endpoint: typeof model.settings?.baseURL === "string" ? { baseURL: model.settings.baseURL } : undefined, + headers: providerHeaders(model), + providerOptions: providerOptions(model), + http: model.body === undefined ? undefined : { body: model.body }, + limits: { context: model.limit.context, output: model.limit.output }, + }) + +const providerHeaders = (model: ModelV2.Info) => { + const packageName = ProviderV2.packageName(model.package) + const generated = new Map() + if (packageName === "@ai-sdk/openai" && typeof model.settings?.organization === "string") + generated.set("OpenAI-Organization", model.settings.organization) + if (packageName === "@ai-sdk/openai" && typeof model.settings?.project === "string") + generated.set("OpenAI-Project", model.settings.project) + if (packageName === "@ai-sdk/anthropic" && typeof model.settings?.authToken === "string") + generated.set("Authorization", `Bearer ${model.settings.authToken}`) + return ProviderV2.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers) +} + +const providerOptions = ( + model: ModelV2.Info, +): { readonly [key: string]: { readonly [key: string]: unknown } } | undefined => { + if (!ProviderV2.isAISDK(model.package) || model.settings === undefined) return undefined + const { apiKey: _, baseURL: _baseURL, ...settings } = model.settings + if (Object.keys(settings).length === 0) return undefined + const packageName = ProviderV2.packageName(model.package) + if (packageName === "@ai-sdk/openai") return { openai: settings } + if (packageName === "@ai-sdk/anthropic") return { anthropic: settings } + if (packageName === "@ai-sdk/openai-compatible") return { openai: settings } + return undefined +} + +export const withVariant = ( + model: ModelV2.Info, + variantID: ModelV2.VariantID | undefined, +): Effect.Effect => { + const id = variantID === "default" ? undefined : variantID + const variant = model.variants?.find((item) => item.id === id) + if (!variant && variantID !== undefined && variantID !== "default") + return Effect.fail( + new VariantUnavailableError({ + providerID: model.providerID, + modelID: model.id, + variant: variantID, + }), + ) + return Effect.succeed( + variant + ? produce(model, (draft) => { + draft.settings = ProviderV2.mergeOverlay(draft.settings, variant.settings) + draft.headers = ProviderV2.mergeHeaders(draft.headers, variant.headers) + draft.body = ProviderV2.mergeOverlay(draft.body, variant.body) + }) + : model, + ) +} + +export interface Dependencies { + readonly loadPackage?: (specifier: string) => Effect.Effect + readonly loadAISDK?: (model: ModelV2.Info) => Effect.Effect +} + +export const fromCatalogModel = ( + model: ModelV2.Info, + credential?: Credential.Value, + dependencies?: Dependencies, +): Effect.Effect => { + const resolved = produce(model, (draft) => { + if (draft.settings?.apiKey === "") delete draft.settings.apiKey + if (credential?.type === "key" && credential.metadata !== undefined) + draft.body = ProviderV2.mergeOverlay(draft.body, credential.metadata) + }) + const packageName = ProviderV2.packageName(resolved.package) + const key = apiKey(resolved, credential) + + if (OpenAICodex.isChatGPT(credential) && !ProviderV2.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) { + return Effect.succeed(codexModel(resolved, credential, key)) + } + + if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") { + if (OpenAICodex.isChatGPT(credential)) return Effect.succeed(codexModel(resolved, credential, key)) + return Effect.succeed( + withDefaults(resolved, OpenAIResponses.route) + .with({ auth: key === undefined ? Auth.none : Auth.bearer(key) }) + .model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }), + ) + } + if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") { + return Effect.succeed( + withDefaults(resolved, AnthropicMessages.route) + .with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) }) + .model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }), + ) + } + if ( + ProviderV2.isAISDK(resolved.package) && + packageName === "@ai-sdk/openai-compatible" && + typeof resolved.settings?.baseURL === "string" + ) { + return Effect.succeed( + withDefaults(resolved, OpenAICompatibleChat.route) + .with({ auth: key === undefined ? Auth.none : Auth.bearer(key) }) + .model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }), + ) + } + if (ProviderV2.isAISDK(resolved.package)) { + if (!dependencies?.loadAISDK) return Effect.fail(unsupported(resolved)) + const runtime = produce(resolved, (draft) => { + draft.settings = ProviderV2.mergeOverlay(draft.settings, { + ...(credential?.type === "key" ? { apiKey: credential.key } : {}), + ...(credential?.type === "oauth" ? { apiKey: credential.access } : {}), + ...credential?.metadata, + }) + }) + return dependencies.loadAISDK(runtime).pipe(Effect.mapError(() => unsupported(resolved))) + } + if (!resolved.package) return Effect.fail(unsupported(resolved)) + + const specifier = resolved.package + return Effect.gen(function* () { + const module = yield* (dependencies?.loadPackage ?? ProviderV2.loadPackage)(specifier).pipe( + Effect.mapError(() => unsupported(resolved)), + ) + const configured = { ...resolved.settings, ...credential?.metadata } + const settings = { + ...(credential ? withoutNativeAuthSettings(configured) : configured), + ...nativeCredentialSettings(specifier, credential), + headers: resolved.headers, + body: resolved.body, + limits: { context: resolved.limit.context, output: resolved.limit.output }, + } + return yield* Effect.try({ + try: () => { + const runtime = module.model(resolved.modelID ?? resolved.id, settings) + return Model.update(runtime, { + provider: resolved.providerID, + compatibility: resolved.compatibility + ? Object.assign({}, runtime.compatibility, resolved.compatibility) + : runtime.compatibility, + }) + }, + catch: () => unsupported(resolved), + }) + }) +} + +const isNativeOpenAI = (packageName: string | undefined) => + packageName === "@opencode-ai/ai/providers/openai" || + packageName?.startsWith("@opencode-ai/ai/providers/openai/") === true + +const nativeCredentialSettings = (specifier: string, credential: Credential.Value | undefined) => { + if (!credential) return {} + if (credential.type === "key") return { apiKey: credential.key } + if ( + specifier === "@opencode-ai/ai/providers/anthropic" || + specifier === "@opencode-ai/ai/providers/anthropic-compatible" + ) + return { authToken: credential.access } + if ( + specifier === "@opencode-ai/ai/providers/google-vertex" || + specifier.startsWith("@opencode-ai/ai/providers/google-vertex/") + ) + return { accessToken: credential.access } + return { apiKey: credential.access } +} + +const withoutNativeAuthSettings = (settings: Record) => { + const { accessToken: _accessToken, apiKey: _apiKey, authToken: _authToken, ...rest } = settings + return rest +} + +const codexModel = ( + model: ModelV2.Info, + credential: Credential.Value | undefined, + key: ReturnType | undefined, +) => { + const account = OpenAICodex.accountID(credential) + return withDefaults(model, OpenAIResponses.route) + .with({ + endpoint: { baseURL: OpenAICodex.baseURL }, + auth: (key === undefined ? Auth.none : Auth.bearer(key)).andThen( + account === undefined ? Auth.none : Auth.headers({ "chatgpt-account-id": account }), + ), + }) + .model({ id: model.modelID ?? model.id, compatibility: model.compatibility }) +} + +const unsupported = (model: ModelV2.Info) => + new UnsupportedPackageError({ + providerID: model.providerID, + modelID: model.id, + package: model.package ?? "unknown", + }) + +export const resolveModel = ( + model: ModelV2.Info, + variant: ModelV2.VariantID | undefined, + credential?: Credential.Value, + dependencies?: Dependencies, +) => withVariant(model, variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential, dependencies))) + +export const supported = (model: ModelV2.Info) => Boolean(model.package) + +/** Resolves catalog selections into runtime models for the current Location. */ +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const catalog = yield* Catalog.Service + const integrations = yield* Integration.Service + const npm = yield* Npm.Service + const aisdk = yield* AISDK.Service + const load = Effect.fn("ModelResolver.resolveModel")(function* ( + selected: ModelV2.Info, + variant?: ModelV2.VariantID, + ) { + const provider = yield* catalog.provider.get(selected.providerID) + const connection = yield* integrations.connection.active( + provider?.integrationID ?? Integration.ID.make(selected.providerID), + ) + const model = yield* resolveModel( + selected, + variant, + connection ? yield* integrations.connection.resolve(connection) : undefined, + { + loadPackage: (specifier) => ProviderV2.loadPackage(specifier, npm), + loadAISDK: (model) => aisdk.model(model), + }, + ) + return { + model, + ref: ModelV2.Ref.make({ + id: selected.id, + providerID: selected.providerID, + ...(variant === undefined ? {} : { variant }), + }), + capabilities: selected.capabilities, + cost: selected.cost, + } + }) + return Service.of({ + resolve: Effect.fn("ModelResolver.resolve")(function* (requested) { + const selected = requested + ? yield* catalog.model.get(requested.providerID, requested.id) + : yield* catalog.model + .default() + .pipe( + Effect.flatMap((model) => + model && supported(model) + ? Effect.succeed(model) + : Effect.map(catalog.model.available(), (models) => models.find(supported)), + ), + ) + if (!selected) return undefined + return yield* load(selected, requested?.variant) + }), + resolveModel: load, + }) + }), +) + +export const node = makeLocationNode({ + service: Service, + layer, + deps: [Catalog.node, Integration.node, Npm.node, AISDK.node], +}) diff --git a/packages/core/src/plugin/provider/openai-codex.ts b/packages/core/src/plugin/provider/openai-codex.ts index 8d4389d969e9..eb734b29f3d5 100644 --- a/packages/core/src/plugin/provider/openai-codex.ts +++ b/packages/core/src/plugin/provider/openai-codex.ts @@ -1,7 +1,7 @@ export * as OpenAICodex from "./openai-codex" // TEMPORARY SEAM (#34765): plugins have no hook into LLM route construction, so -// codex routing lives in SessionRunnerModel.fromCatalogModel and catalog filtering +// Codex routing lives in ModelResolver and catalog filtering. // in OpenAIPlugin, sharing this module. Once the native provider packages land // (#33689/#33925/#34462) this should collapse into the native OpenAI provider. // The eligibility rules mirror V1's CodexAuthPlugin allowlist; models.dev has no diff --git a/packages/core/src/session/runner/model.ts b/packages/core/src/session/runner/model.ts index 6013ce7b740a..d0902d7d674b 100644 --- a/packages/core/src/session/runner/model.ts +++ b/packages/core/src/session/runner/model.ts @@ -2,30 +2,16 @@ export * as SessionRunnerModel from "./model" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Model } from "@opencode-ai/ai" -// ast-grep-ignore: no-star-import -import * as AnthropicMessages from "@opencode-ai/ai/protocols/anthropic-messages" -// ast-grep-ignore: no-star-import -import * as OpenAICompatibleChat from "@opencode-ai/ai/protocols/openai-compatible-chat" -// ast-grep-ignore: no-star-import -import * as OpenAIResponses from "@opencode-ai/ai/protocols/openai-responses" -import { Auth, type AnyRoute } from "@opencode-ai/ai/route" import { Context, Effect, Layer, Schema } from "effect" -import { produce } from "immer" -import { AISDK } from "../../aisdk" import { Catalog } from "../../catalog" -import { Credential } from "../../credential" -import { Integration } from "../../integration" +import { ModelResolver } from "../../model-resolver" import { ModelV2 } from "../../model" -import { Npm } from "@opencode-ai/util/npm" -import { OpenAICodex } from "../../plugin/provider/openai-codex" import { ProviderV2 } from "../../provider" import { SessionSchema } from "../schema" export class ModelNotSelectedError extends Schema.TaggedErrorClass()( "SessionRunnerModel.ModelNotSelectedError", - { - sessionID: SessionSchema.ID, - }, + { sessionID: SessionSchema.ID }, ) { override get message() { return `No model is available for session ${this.sessionID}` @@ -34,59 +20,19 @@ export class ModelNotSelectedError extends Schema.TaggedErrorClass()( "SessionRunnerModel.ModelUnavailableError", - { - providerID: ProviderV2.ID, - modelID: ModelV2.ID, - }, + { providerID: ProviderV2.ID, modelID: ModelV2.ID }, ) { override get message() { return `Model unavailable: ${this.providerID}/${this.modelID}` } } +export const VariantUnavailableError = ModelResolver.VariantUnavailableError +export type VariantUnavailableError = ModelResolver.VariantUnavailableError +export const UnsupportedPackageError = ModelResolver.UnsupportedPackageError +export type UnsupportedPackageError = ModelResolver.UnsupportedPackageError -export class VariantUnavailableError extends Schema.TaggedErrorClass()( - "SessionRunnerModel.VariantUnavailableError", - { - providerID: ProviderV2.ID, - modelID: ModelV2.ID, - variant: ModelV2.VariantID, - }, -) { - override get message() { - return `Variant unavailable for ${this.providerID}/${this.modelID}: ${this.variant}` - } -} - -export class UnsupportedPackageError extends Schema.TaggedErrorClass()( - "SessionRunnerModel.UnsupportedPackageError", - { - providerID: ProviderV2.ID, - modelID: ModelV2.ID, - package: Schema.String, - }, -) { - override get message() { - return `Unsupported package for ${this.providerID}/${this.modelID}: ${this.package}` - } -} - -export type Error = - | ModelNotSelectedError - | ModelUnavailableError - | VariantUnavailableError - | UnsupportedPackageError - | Integration.AuthorizationError - -export interface Resolved { - /** Route-level model for provider requests; its id is the provider API model id, which may differ from the catalog id. */ - readonly model: Model - /** Selected catalog identity. Durable records and displays must use this, never the API model id. */ - readonly ref: ModelV2.Ref - /** Catalog capabilities used to shape requests before provider lowering. */ - readonly capabilities: ModelV2.Capabilities - /** Catalog pricing in dollars per million tokens. */ - readonly cost: ModelV2.Info["cost"] -} +export type Error = ModelNotSelectedError | ModelUnavailableError | ModelResolver.Error +export type Resolved = ModelResolver.Resolved export interface Interface { readonly resolve: (session: SessionSchema.Info) => Effect.Effect @@ -94,9 +40,6 @@ export interface Interface { export class Service extends Context.Service()("@opencode/v2/SessionRunnerModel") {} -/** Test or embedding seam for supplying a model resolver directly. */ -export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Service, Service.of({ resolve })) - /** Builds a Resolved whose catalog identity mirrors the route model. Test or embedding seam. */ export const resolved = ( model: Model, @@ -116,276 +59,31 @@ export const resolved = ( cost: options.cost, }) -const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => { - if (credential?.type === "key") return Auth.value(credential.key) - if (credential?.type === "oauth") return Auth.value(credential.access) - const value = model.settings?.apiKey - if (typeof value === "string") return Auth.value(value) -} - -const withDefaults = (model: ModelV2.Info, route: AnyRoute) => - route.with({ - provider: model.providerID, - endpoint: typeof model.settings?.baseURL === "string" ? { baseURL: model.settings.baseURL } : undefined, - headers: providerHeaders(model), - providerOptions: providerOptions(model), - http: model.body === undefined ? undefined : { body: model.body }, - limits: { context: model.limit.context, output: model.limit.output }, - }) - -const providerHeaders = (model: ModelV2.Info) => { - const packageName = ProviderV2.packageName(model.package) - const generated = new Map() - if (packageName === "@ai-sdk/openai" && typeof model.settings?.organization === "string") - generated.set("OpenAI-Organization", model.settings.organization) - if (packageName === "@ai-sdk/openai" && typeof model.settings?.project === "string") - generated.set("OpenAI-Project", model.settings.project) - if (packageName === "@ai-sdk/anthropic" && typeof model.settings?.authToken === "string") - generated.set("Authorization", `Bearer ${model.settings.authToken}`) - return ProviderV2.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers) -} - -const providerOptions = ( - model: ModelV2.Info, -): { readonly [key: string]: { readonly [key: string]: unknown } } | undefined => { - if (!ProviderV2.isAISDK(model.package) || model.settings === undefined) return undefined - const { apiKey: _, baseURL: _baseURL, ...settings } = model.settings - if (Object.keys(settings).length === 0) return undefined - const packageName = ProviderV2.packageName(model.package) - if (packageName === "@ai-sdk/openai") return { openai: settings } - if (packageName === "@ai-sdk/anthropic") return { anthropic: settings } - if (packageName === "@ai-sdk/openai-compatible") return { openai: settings } -} - -export const withVariant = ( - model: ModelV2.Info, - variantID: ModelV2.VariantID | undefined, -): Effect.Effect => { - const id = variantID === "default" ? undefined : variantID - const variant = model.variants?.find((item) => item.id === id) - if (!variant && variantID !== undefined && variantID !== "default") - return Effect.fail( - new VariantUnavailableError({ - providerID: model.providerID, - modelID: model.id, - variant: variantID, - }), - ) - return Effect.succeed( - variant - ? produce(model, (draft) => { - draft.settings = ProviderV2.mergeOverlay(draft.settings, variant.settings) - draft.headers = ProviderV2.mergeHeaders(draft.headers, variant.headers) - draft.body = ProviderV2.mergeOverlay(draft.body, variant.body) - }) - : model, - ) -} - -export interface Dependencies { - readonly loadPackage?: (specifier: string) => Effect.Effect - readonly loadAISDK?: (model: ModelV2.Info) => Effect.Effect -} - -export const fromCatalogModel = ( - model: ModelV2.Info, - credential?: Credential.Value, - dependencies: Dependencies = {}, -): Effect.Effect => { - const resolved = produce(model, (draft) => { - if (draft.settings?.apiKey === "") delete draft.settings.apiKey - if (credential?.type === "key" && credential.metadata !== undefined) - draft.body = ProviderV2.mergeOverlay(draft.body, credential.metadata) - }) - const packageName = ProviderV2.packageName(resolved.package) - const key = apiKey(resolved, credential) - - if (OpenAICodex.isChatGPT(credential) && !ProviderV2.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) { - return Effect.succeed(codexModel(resolved, credential, key)) - } - - if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") { - if (OpenAICodex.isChatGPT(credential)) return Effect.succeed(codexModel(resolved, credential, key)) - return Effect.succeed( - withDefaults(resolved, OpenAIResponses.route) - .with({ auth: key === undefined ? Auth.none : Auth.bearer(key) }) - .model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }), - ) - } - if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") { - return Effect.succeed( - withDefaults(resolved, AnthropicMessages.route) - .with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) }) - .model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }), - ) - } - if ( - ProviderV2.isAISDK(resolved.package) && - packageName === "@ai-sdk/openai-compatible" && - typeof resolved.settings?.baseURL === "string" - ) { - return Effect.succeed( - withDefaults(resolved, OpenAICompatibleChat.route) - .with({ auth: key === undefined ? Auth.none : Auth.bearer(key) }) - .model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }), - ) - } - if (ProviderV2.isAISDK(resolved.package)) { - if (!dependencies.loadAISDK) return Effect.fail(unsupported(resolved)) - const runtime = produce(resolved, (draft) => { - draft.settings = ProviderV2.mergeOverlay(draft.settings, { - ...(credential?.type === "key" ? { apiKey: credential.key } : {}), - ...(credential?.type === "oauth" ? { apiKey: credential.access } : {}), - ...credential?.metadata, - }) - }) - return dependencies.loadAISDK(runtime).pipe(Effect.mapError(() => unsupported(resolved))) - } - if (!resolved.package) return Effect.fail(unsupported(resolved)) - - const specifier = resolved.package - return Effect.gen(function* () { - const module = yield* (dependencies.loadPackage ?? ProviderV2.loadPackage)(specifier).pipe( - Effect.mapError(() => unsupported(resolved)), - ) - const configured = { ...resolved.settings, ...credential?.metadata } - const settings = { - ...(credential ? withoutNativeAuthSettings(configured) : configured), - ...nativeCredentialSettings(specifier, credential), - headers: resolved.headers, - body: resolved.body, - limits: { context: resolved.limit.context, output: resolved.limit.output }, - } - return yield* Effect.try({ - try: () => { - const runtime = module.model(resolved.modelID ?? resolved.id, settings) - return Model.update(runtime, { - provider: resolved.providerID, - compatibility: resolved.compatibility - ? { ...runtime.compatibility, ...resolved.compatibility } - : runtime.compatibility, - }) - }, - catch: () => unsupported(resolved), - }) - }) -} - -const isNativeOpenAI = (packageName: string | undefined) => - packageName === "@opencode-ai/ai/providers/openai" || - packageName?.startsWith("@opencode-ai/ai/providers/openai/") === true - -const nativeCredentialSettings = (specifier: string, credential: Credential.Value | undefined) => { - if (!credential) return {} - if (credential.type === "key") return { apiKey: credential.key } - if ( - specifier === "@opencode-ai/ai/providers/anthropic" || - specifier === "@opencode-ai/ai/providers/anthropic-compatible" - ) - return { authToken: credential.access } - if ( - specifier === "@opencode-ai/ai/providers/google-vertex" || - specifier.startsWith("@opencode-ai/ai/providers/google-vertex/") - ) - return { accessToken: credential.access } - return { apiKey: credential.access } -} - -const withoutNativeAuthSettings = (settings: Record) => { - const { accessToken: _accessToken, apiKey: _apiKey, authToken: _authToken, ...rest } = settings - return rest -} - -const codexModel = ( - model: ModelV2.Info, - credential: Credential.Value | undefined, - key: ReturnType | undefined, -) => { - const account = OpenAICodex.accountID(credential) - return withDefaults(model, OpenAIResponses.route) - .with({ - endpoint: { baseURL: OpenAICodex.baseURL }, - auth: (key === undefined ? Auth.none : Auth.bearer(key)).andThen( - account === undefined ? Auth.none : Auth.headers({ "chatgpt-account-id": account }), - ), - }) - .model({ id: model.modelID ?? model.id, compatibility: model.compatibility }) -} - -const unsupported = (model: ModelV2.Info) => - new UnsupportedPackageError({ - providerID: model.providerID, - modelID: model.id, - package: model.package ?? "unknown", - }) - -export const resolve = ( - session: SessionSchema.Info, - model: ModelV2.Info, - credential?: Credential.Value, - dependencies?: Dependencies, -) => - withVariant(model, session.model?.variant).pipe( - Effect.flatMap((model) => fromCatalogModel(model, credential, dependencies)), - ) - -export const supported = (model: ModelV2.Info) => Boolean(model.package) - -/** Resolves models from the catalog belonging to the current Location runtime. */ const layer = Layer.effect( Service, Effect.gen(function* () { const catalog = yield* Catalog.Service - const integrations = yield* Integration.Service - const npm = yield* Npm.Service - const aisdk = yield* AISDK.Service + const resolver = yield* ModelResolver.Service return Service.of({ resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) { // Location plugins populate and filter the catalog asynchronously during layer startup. - const defaultModel = session.model ? undefined : yield* catalog.model.default() - const selected = session.model - ? (yield* catalog.model.available()).find( - (model) => model.providerID === session.model?.providerID && model.id === session.model.id, - ) - : defaultModel && supported(defaultModel) - ? defaultModel - : (yield* catalog.model.available()).find(supported) - if (!selected && session.model) + if (!session.model) { + const resolved = yield* resolver.resolve() + if (resolved) return resolved + return yield* new ModelNotSelectedError({ sessionID: session.id }) + } + const selected = (yield* catalog.model.available()).find( + (model) => model.providerID === session.model?.providerID && model.id === session.model.id, + ) + if (!selected) return yield* new ModelUnavailableError({ providerID: session.model.providerID, modelID: session.model.id, }) - if (!selected) return yield* new ModelNotSelectedError({ sessionID: session.id }) - const provider = yield* catalog.provider.get(selected.providerID) - const connection = yield* integrations.connection.active( - provider?.integrationID ?? Integration.ID.make(selected.providerID), - ) - const model = yield* resolve( - session, - selected, - connection ? yield* integrations.connection.resolve(connection) : undefined, - { - loadPackage: (specifier) => ProviderV2.loadPackage(specifier, npm), - loadAISDK: (model) => aisdk.model(model), - }, - ) - return { - model, - ref: ModelV2.Ref.make({ - id: selected.id, - providerID: selected.providerID, - ...(session.model?.variant === undefined ? {} : { variant: session.model.variant }), - }), - capabilities: selected.capabilities, - cost: selected.cost, - } + return yield* resolver.resolveModel(selected, session.model.variant) }), }) }), ) -export const node = makeLocationNode({ - service: Service, - layer, - deps: [Catalog.node, Integration.node, Npm.node, AISDK.node], -}) +export const node = makeLocationNode({ service: Service, layer, deps: [Catalog.node, ModelResolver.node] }) diff --git a/packages/core/test/generate.test.ts b/packages/core/test/generate.test.ts new file mode 100644 index 000000000000..d990f96aceae --- /dev/null +++ b/packages/core/test/generate.test.ts @@ -0,0 +1,111 @@ +import { expect } from "bun:test" +import { LLMClient, LLMEvent, LLMResponse, Model } from "@opencode-ai/ai" +import { OpenAIChat } from "@opencode-ai/ai/protocols" +import { AISDK } from "@opencode-ai/core/aisdk" +import { Catalog } from "@opencode-ai/core/catalog" +import { Generate } from "@opencode-ai/core/generate" +import { Integration } from "@opencode-ai/core/integration" +import { ModelResolver } from "@opencode-ai/core/model-resolver" +import { ModelV2 } from "@opencode-ai/core/model" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { Npm } from "@opencode-ai/util/npm" +import { Effect, Layer, Stream } from "effect" +import { testEffect } from "./lib/effect" + +const selected = ModelV2.Info.make({ + ...ModelV2.Info.empty(ProviderV2.ID.make("test-provider"), ModelV2.ID.make("gemini")), + package: ProviderV2.aisdk("@ai-sdk/google"), +}) +const runtime = Model.make({ id: "gemini", provider: "test-provider", route: OpenAIChat.route }) + +const catalog = Layer.mock(Catalog.Service, { + provider: { + get: () => Effect.succeed(undefined), + all: () => Effect.die("unused"), + available: () => Effect.die("unused"), + }, + model: { + get: () => Effect.succeed(selected), + all: () => Effect.die("unused"), + available: () => Effect.die("unused"), + default: () => Effect.die("unused"), + small: () => Effect.die("unused"), + }, +}) +const integrations = Layer.mock(Integration.Service, { + connection: { + active: () => Effect.succeed(undefined), + resolve: () => Effect.die("unused"), + key: () => Effect.die("unused"), + update: () => Effect.die("unused"), + remove: () => Effect.die("unused"), + }, + oauth: { + connect: () => Effect.die("unused"), + status: () => Effect.die("unused"), + complete: () => Effect.die("unused"), + cancel: () => Effect.die("unused"), + }, + command: { + connect: () => Effect.die("unused"), + status: () => Effect.die("unused"), + cancel: () => Effect.die("unused"), + }, +}) +const npm = Layer.mock(Npm.Service, { + add: () => Effect.die("unused"), + install: () => Effect.die("unused"), + which: () => Effect.die("unused"), +}) +const aisdk = Layer.mock(AISDK.Service, { + hook: { + sdk: () => Effect.die("unused"), + language: () => Effect.die("unused"), + }, + model: () => Effect.succeed(runtime), +}) +const client = Layer.mock(LLMClient.Service)({ + prepare: () => Effect.die("unused"), + stream: () => Stream.die("unused"), + generate: () => + Effect.sync(() => { + const response = LLMResponse.fromEvents([ + LLMEvent.textStart({ id: "generate" }), + LLMEvent.textDelta({ id: "generate", text: "OK" }), + LLMEvent.textEnd({ id: "generate" }), + LLMEvent.finish({ reason: "stop" }), + ]) + if (!response) throw new Error("Incomplete generate response") + return response + }), +}) + +const resolver = ModelResolver.layer.pipe(Layer.provide(Layer.mergeAll(catalog, integrations, npm, aisdk))) +const it = testEffect(Generate.layer.pipe(Layer.provide(Layer.merge(resolver, client)))) +const resolverIt = testEffect(resolver) + +it.effect("loads dynamic AI SDK models", () => + Effect.gen(function* () { + const generate = yield* Generate.Service + const result = yield* generate.text({ + prompt: "Return exactly OK", + model: ModelV2.Ref.make({ providerID: selected.providerID, id: selected.id }), + }) + + expect(result).toBe("OK") + }), +) + +resolverIt.effect("resolves dynamic models with their catalog metadata", () => + Effect.gen(function* () { + const resolver = yield* ModelResolver.Service + const result = yield* resolver.resolve(ModelV2.Ref.make({ providerID: selected.providerID, id: selected.id })) + + expect(result).toEqual({ + model: runtime, + ref: ModelV2.Ref.make({ providerID: selected.providerID, id: selected.id }), + capabilities: selected.capabilities, + cost: selected.cost, + }) + }), +) diff --git a/packages/core/test/session-runner-model.test.ts b/packages/core/test/model-resolver.test.ts similarity index 80% rename from packages/core/test/session-runner-model.test.ts rename to packages/core/test/model-resolver.test.ts index 38fc707918c1..9eedc4265b64 100644 --- a/packages/core/test/session-runner-model.test.ts +++ b/packages/core/test/model-resolver.test.ts @@ -1,17 +1,13 @@ import { describe, expect } from "bun:test" import { LLM, Model } from "@opencode-ai/ai" import { LLMClient } from "@opencode-ai/ai/route" -import { DateTime, Effect } from "effect" -import { Money } from "@opencode-ai/schema/money" +import { Effect } from "effect" import { Headers } from "effect/unstable/http" import { Credential } from "@opencode-ai/core/credential" import { Integration } from "@opencode-ai/core/integration" import { ModelV2 } from "@opencode-ai/core/model" import { ProviderV2 } from "@opencode-ai/core/provider" -import { ProjectV2 } from "@opencode-ai/core/project" -import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" -import { SessionV2 } from "@opencode-ai/core/session" -import { AbsolutePath } from "@opencode-ai/core/schema" +import { ModelResolver } from "@opencode-ai/core/model-resolver" import { it } from "./lib/effect" interface ModelOptions { @@ -43,13 +39,13 @@ const model = (packageName: string | undefined, options: ModelOptions = {}) => limit: { context: 100, output: 20 }, }) -describe("SessionRunnerModel", () => { +describe("ModelResolver", () => { it.effect("uses the API modelID instead of the catalog ID for native OpenAI routes", () => Effect.gen(function* () { const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }) - const resolved = yield* SessionRunnerModel.fromCatalogModel(catalog) + const resolved = yield* ModelResolver.fromCatalogModel(catalog) expect(catalog.id).toBe(ModelV2.ID.make("test-model")) expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" }) @@ -68,7 +64,7 @@ describe("SessionRunnerModel", () => { it.effect("keeps catalog apiKey credentials out of provider JSON", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { apiKey: "secret", baseURL: "https://openai.example/v1" }, }), @@ -82,7 +78,7 @@ describe("SessionRunnerModel", () => { it.effect("treats an empty configured API key as omitted", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { apiKey: "", baseURL: "https://openai.example/v1" }, }), @@ -101,7 +97,7 @@ describe("SessionRunnerModel", () => { it.effect("uses merged API settings for OpenAI-compatible auth and request defaults", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), { compatibility: { reasoningField: "vendor_reasoning" }, settings: { @@ -130,7 +126,7 @@ describe("SessionRunnerModel", () => { }), ) - it.effect("overlays selected OpenAI Session variant settings and bodies", () => + it.effect("overlays selected OpenAI variant settings and bodies", () => Effect.gen(function* () { const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, @@ -147,22 +143,7 @@ describe("SessionRunnerModel", () => { }, ], }) - const session = SessionV2.Info.make({ - id: SessionV2.ID.make("ses_model_variant"), - projectID: ProjectV2.ID.global, - title: "test", - model: { - id: catalog.id, - providerID: catalog.providerID, - variant: ModelV2.VariantID.make("high"), - }, - cost: Money.USD.zero, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) }, - location: { directory: AbsolutePath.make("/project") }, - }) - - const resolved = yield* SessionRunnerModel.resolve(session, catalog) + const resolved = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("high")) expect(resolved.route.defaults.headers).toMatchObject({ "x-test": "header", "x-variant": "high" }) expect(resolved.route.defaults.http?.body).toEqual({ @@ -177,7 +158,7 @@ describe("SessionRunnerModel", () => { }), ) - it.effect("overlays selected OpenAI-compatible Session variant bodies", () => + it.effect("overlays selected OpenAI-compatible variant bodies", () => Effect.gen(function* () { const catalog = model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), { settings: { baseURL: "https://compatible.example/v1" }, @@ -190,18 +171,7 @@ describe("SessionRunnerModel", () => { }, ], }) - const session = SessionV2.Info.make({ - id: SessionV2.ID.make("ses_compatible_variant"), - projectID: ProjectV2.ID.global, - title: "test", - model: { id: catalog.id, providerID: catalog.providerID, variant: ModelV2.VariantID.make("high") }, - cost: Money.USD.zero, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) }, - location: { directory: AbsolutePath.make("/project") }, - }) - - const resolved = yield* SessionRunnerModel.resolve(session, catalog) + const resolved = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("high")) expect(resolved.route.defaults.http?.body).toEqual({ custom_extension: { enabled: true }, @@ -211,27 +181,12 @@ describe("SessionRunnerModel", () => { }), ) - it.effect("rejects an explicit unavailable Session variant during model resolution", () => + it.effect("rejects an explicit unavailable variant during model resolution", () => Effect.gen(function* () { const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }) - const session = SessionV2.Info.make({ - id: SessionV2.ID.make("ses_model_variant_unavailable"), - projectID: ProjectV2.ID.global, - title: "test", - model: { - id: catalog.id, - providerID: catalog.providerID, - variant: ModelV2.VariantID.make("unknown"), - }, - cost: Money.USD.zero, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) }, - location: { directory: AbsolutePath.make("/project") }, - }) - - const failure = yield* SessionRunnerModel.resolve(session, catalog).pipe(Effect.flip) + const failure = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("unknown")).pipe(Effect.flip) expect(failure).toMatchObject({ _tag: "SessionRunnerModel.VariantUnavailableError", @@ -243,7 +198,7 @@ describe("SessionRunnerModel", () => { }), ) - it.effect("overlays selected Anthropic Session variant settings", () => + it.effect("overlays selected Anthropic variant settings", () => Effect.gen(function* () { const catalog = model(ProviderV2.aisdk("@ai-sdk/anthropic"), { settings: { baseURL: "https://anthropic.example/v1" }, @@ -256,18 +211,7 @@ describe("SessionRunnerModel", () => { }, ], }) - const session = SessionV2.Info.make({ - id: SessionV2.ID.make("ses_anthropic_variant"), - projectID: ProjectV2.ID.global, - title: "test", - model: { id: catalog.id, providerID: catalog.providerID, variant: ModelV2.VariantID.make("high") }, - cost: Money.USD.zero, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) }, - location: { directory: AbsolutePath.make("/project") }, - }) - - const resolved = yield* SessionRunnerModel.resolve(session, catalog) + const resolved = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("high")) expect(resolved.route.defaults.http?.body).toEqual({ custom_extension: { enabled: true }, @@ -280,7 +224,7 @@ describe("SessionRunnerModel", () => { it.effect("maps catalog Anthropic AI SDK models into native routes", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/anthropic"), { settings: { baseURL: "https://anthropic.example/v1" }, }), @@ -296,7 +240,7 @@ describe("SessionRunnerModel", () => { it.effect("uses resolved credentials for bearer auth", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, headers: {}, @@ -320,7 +264,7 @@ describe("SessionRunnerModel", () => { it.effect("prefers stored credentials over configured auth", () => Effect.gen(function* () { const credential = Credential.Key.make({ type: "key", key: "stored-secret", metadata: { tenant: "work" } }) - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { apiKey: "configured-secret", baseURL: "https://openai.example/v1" }, headers: {}, @@ -343,7 +287,7 @@ describe("SessionRunnerModel", () => { it.effect("does not project OAuth account metadata into the request body", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, headers: {}, @@ -365,7 +309,7 @@ describe("SessionRunnerModel", () => { it.effect("routes ChatGPT OAuth credentials to the codex backend", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, headers: {}, @@ -400,7 +344,7 @@ describe("SessionRunnerModel", () => { it.effect("routes native OpenAI provider packages with ChatGPT credentials to the codex backend", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model("@opencode-ai/ai/providers/openai", { settings: { baseURL: "https://openai.example/v1" }, }), @@ -429,7 +373,7 @@ describe("SessionRunnerModel", () => { it.effect("does not route native OpenAI-compatible packages to the codex backend", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model("@opencode-ai/ai/providers/openai-compatible", { settings: { baseURL: "https://compatible.example/v1" }, }), @@ -450,7 +394,7 @@ describe("SessionRunnerModel", () => { it.effect("maps legacy OpenAI organization and project settings to headers", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { organization: "org_123", project: "proj_123" }, }), @@ -465,7 +409,7 @@ describe("SessionRunnerModel", () => { it.effect("routes ChatGPT OAuth credentials without an account id to the codex backend", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, headers: {}, @@ -496,7 +440,7 @@ describe("SessionRunnerModel", () => { it.effect("keeps non-ChatGPT OAuth credentials on the configured endpoint", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, headers: {}, @@ -528,12 +472,12 @@ describe("SessionRunnerModel", () => { it.effect("loads dynamic native provider packages through the injected package loader", () => Effect.gen(function* () { - const native = yield* SessionRunnerModel.fromCatalogModel( + const native = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }), ) - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model("@opencode-ai/ai/providers/custom", { settings: { region: "test" }, headers: { "x-package": "header" }, @@ -565,7 +509,7 @@ describe("SessionRunnerModel", () => { it.effect("maps OAuth credentials to native provider auth settings", () => Effect.gen(function* () { - const native = yield* SessionRunnerModel.fromCatalogModel( + const native = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }), @@ -588,7 +532,7 @@ describe("SessionRunnerModel", () => { ] as const yield* Effect.forEach(packages, ([specifier, key]) => - SessionRunnerModel.fromCatalogModel(model(specifier, { settings: { apiKey: "configured-key" } }), credential, { + ModelResolver.fromCatalogModel(model(specifier, { settings: { apiKey: "configured-key" } }), credential, { loadPackage: () => Effect.succeed({ model: (modelID, settings) => { @@ -604,12 +548,12 @@ describe("SessionRunnerModel", () => { it.effect("loads arbitrary AISDK packages through the injected AISDK loader", () => Effect.gen(function* () { - const native = yield* SessionRunnerModel.fromCatalogModel( + const native = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }), ) - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/google"), { modelID: "gemini-api-model", settings: { project: "test" }, @@ -644,7 +588,7 @@ describe("SessionRunnerModel", () => { it.effect("rejects AISDK packages without an available loader", () => Effect.gen(function* () { - const failure = yield* SessionRunnerModel.fromCatalogModel( + const failure = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/google"), { settings: { baseURL: "https://google.example/v1" }, }), @@ -662,12 +606,12 @@ describe("SessionRunnerModel", () => { it.effect("drops an empty API key before loading an AISDK package", () => Effect.gen(function* () { - const native = yield* SessionRunnerModel.fromCatalogModel( + const native = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }), ) - yield* SessionRunnerModel.fromCatalogModel( + yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/google"), { settings: { apiKey: "", baseURL: "https://google.example/v1" }, }), @@ -685,9 +629,9 @@ describe("SessionRunnerModel", () => { it.effect("reports whether a catalog model declares a provider package", () => Effect.sync(() => { - expect(SessionRunnerModel.supported(model(ProviderV2.aisdk("@ai-sdk/openai")))).toBe(true) - expect(SessionRunnerModel.supported(model("@opencode-ai/ai/providers/custom"))).toBe(true) - expect(SessionRunnerModel.supported(model(undefined))).toBe(false) + expect(ModelResolver.supported(model(ProviderV2.aisdk("@ai-sdk/openai")))).toBe(true) + expect(ModelResolver.supported(model("@opencode-ai/ai/providers/custom"))).toBe(true) + expect(ModelResolver.supported(model(undefined))).toBe(false) }), ) }) diff --git a/packages/core/test/session-compact.test.ts b/packages/core/test/session-compact.test.ts index d37cc83c1267..3ad15fc730ad 100644 --- a/packages/core/test/session-compact.test.ts +++ b/packages/core/test/session-compact.test.ts @@ -49,14 +49,15 @@ const client = Layer.mock(LLMClient.Service)({ generate: () => Effect.die("unused"), }) const config = Layer.mock(Config.Service)({ entries: () => Effect.succeed([]) }) -const models = SessionRunnerModel.layerWith(() => - Effect.succeed( - SessionRunnerModel.resolved(model, { - capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, - cost: [], - }), - ), -) +const models = Layer.mock(SessionRunnerModel.Service)({ + resolve: () => + Effect.succeed( + SessionRunnerModel.resolved(model, { + capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, + cost: [], + }), + ), +}) const locations = Layer.effect( LocationServiceMap.Service, LayerMap.make( diff --git a/packages/core/test/session-generate.test.ts b/packages/core/test/session-generate.test.ts index 3f0183167c4b..98e7aedc0c7c 100644 --- a/packages/core/test/session-generate.test.ts +++ b/packages/core/test/session-generate.test.ts @@ -66,14 +66,15 @@ const client = Layer.mock(LLMClient.Service)({ return response }), }) -const models = SessionRunnerModel.layerWith(() => - Effect.succeed( - SessionRunnerModel.resolved(model, { - capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, - cost: [], - }), - ), -) +const models = Layer.mock(SessionRunnerModel.Service)({ + resolve: () => + Effect.succeed( + SessionRunnerModel.resolved(model, { + capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, + cost: [], + }), + ), +}) const builtins = Layer.mock(InstructionBuiltIns.Service, { load: () => Effect.succeed( diff --git a/packages/core/test/session-runner-recorded.test.ts b/packages/core/test/session-runner-recorded.test.ts index d26fa37df443..fb3619e4c697 100644 --- a/packages/core/test/session-runner-recorded.test.ts +++ b/packages/core/test/session-runner-recorded.test.ts @@ -73,14 +73,15 @@ const model = OpenAIChat.route generation: { maxTokens: 20, temperature: 0 }, }) .model({ id: "gpt-4o-mini" }) -const models = SessionRunnerModel.layerWith(() => - Effect.succeed( - SessionRunnerModel.resolved(model, { - capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, - cost: [], - }), - ), -) +const models = Layer.mock(SessionRunnerModel.Service)({ + resolve: () => + Effect.succeed( + SessionRunnerModel.resolved(model, { + capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, + cost: [], + }), + ), +}) const systemContext = Layer.mock(InstructionBuiltIns.Service, { load: () => Effect.succeed(Instructions.empty) }) const instructionContext = Layer.mock(InstructionDiscovery.Service, { load: () => Effect.succeed(Instructions.empty) }) const skillInstructions = Layer.mock(SkillInstructions.Service, { load: () => Effect.succeed(Instructions.empty) }) diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 6ce9cf0f203d..86e9d7c576db 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -280,17 +280,18 @@ const echo = Layer.effectDiscard( const echoNode = makeLocationNode({ name: "test/session-runner-tools", layer: echo, deps: [ToolRegistry.node] }) let modelResolveHook = Effect.void let currentModel = model -const models = SessionRunnerModel.layerWith((session) => - modelResolveHook.pipe( - Effect.as( - SessionRunnerModel.resolved(session.model?.id === "replacement" ? replacementModel : currentModel, { - capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, - cost: [], - variant: session.model?.variant, - }), +const models = Layer.mock(SessionRunnerModel.Service)({ + resolve: (session) => + modelResolveHook.pipe( + Effect.as( + SessionRunnerModel.resolved(session.model?.id === "replacement" ? replacementModel : currentModel, { + capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, + cost: [], + variant: session.model?.variant, + }), + ), ), - ), -) +}) const systemContextKey = Instructions.Key.make("test/context") let systemBaseline = "Initial context" let systemRemoved = false diff --git a/packages/core/test/tool-search.test.ts b/packages/core/test/tool-search.test.ts index 41e25953c418..4367d1b82e66 100644 --- a/packages/core/test/tool-search.test.ts +++ b/packages/core/test/tool-search.test.ts @@ -88,8 +88,8 @@ describe("search tools", () => { expect(glob.output?.structured).toEqual({ count: FileSystem.DEFAULT_SEARCH_LIMIT }) expect(grep.output?.structured).toEqual({ matches: FileSystem.DEFAULT_SEARCH_LIMIT }) - expect(glob.output?.content).toEqual([{ type: "text", text: glob.result.value }]) - expect(grep.output?.content).toEqual([{ type: "text", text: grep.result.value }]) + expect(glob.output?.content).toEqual([{ type: "text", text: String(glob.result.value) }]) + expect(grep.output?.content).toEqual([{ type: "text", text: String(grep.result.value) }]) expect(String(glob.result.value).split("\n")).toHaveLength(FileSystem.DEFAULT_SEARCH_LIMIT) expect(grep.result.value).toStartWith(`Found ${FileSystem.DEFAULT_SEARCH_LIMIT} matches\n`) }), From f1f0f47ee22ae2972a7acef1b5d6dbb7e0578d1e Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 22 Jul 2026 22:38:13 -0400 Subject: [PATCH 045/150] fix(core): migrate named agent colors (#38414) --- packages/core/src/v1/config/agent.ts | 7 +++++-- packages/core/src/v1/config/migrate.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/core/src/v1/config/agent.ts b/packages/core/src/v1/config/agent.ts index 09838a919685..b220bd7ef87d 100644 --- a/packages/core/src/v1/config/agent.ts +++ b/packages/core/src/v1/config/agent.ts @@ -4,7 +4,10 @@ import { Schema, SchemaGetter } from "effect" import { PositiveInt } from "../../schema" import { ConfigPermissionV1 } from "./permission" -const Color = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)) +const Color = Schema.Union([ + Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)), + Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]), +]) const AgentSchema = Schema.StructWithRest( Schema.Struct({ @@ -26,7 +29,7 @@ const AgentSchema = Schema.StructWithRest( }), options: Schema.optional(Schema.Record(Schema.String, Schema.Any)), color: Schema.optional(Color).annotate({ - description: "Hex color code (e.g., #FF5733)", + description: "Hex color code (e.g., #FF5733) or theme color (e.g., primary)", }), steps: Schema.optional(PositiveInt).annotate({ description: "Maximum number of agentic iterations before forcing text-only response", diff --git a/packages/core/src/v1/config/migrate.ts b/packages/core/src/v1/config/migrate.ts index 6c0a342abe7b..61a49f57cf47 100644 --- a/packages/core/src/v1/config/migrate.ts +++ b/packages/core/src/v1/config/migrate.ts @@ -161,7 +161,7 @@ export function migrateAgent(info: ConfigAgentV1.Info) { description: info.description, mode: info.mode, hidden: info.hidden, - color: info.color, + color: info.color === undefined ? undefined : info.color.startsWith("#") ? info.color : "#aaaaaa", steps: info.steps, disabled: info.disable, permissions: permissions(info.permission), From 6e8aefcfa07fa49ea6c9988d371353b1b76d69f7 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 21:42:53 -0500 Subject: [PATCH 046/150] fix(ai): normalize Bedrock cache usage (#38427) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- packages/ai/src/protocols/bedrock-converse.ts | 19 +++---- packages/ai/src/schema/events.ts | 7 +-- ...s-cachepoint-on-identical-second-call.json | 53 +++++++++++++++++++ .../bedrock-converse-cache.recorded.test.ts | 22 +++++--- .../ai/test/provider/bedrock-converse.test.ts | 33 ++++++++++++ 5 files changed, 114 insertions(+), 20 deletions(-) create mode 100644 packages/ai/test/fixtures/recordings/bedrock-converse-cache/writes-then-reads-cachepoint-on-identical-second-call.json diff --git a/packages/ai/src/protocols/bedrock-converse.ts b/packages/ai/src/protocols/bedrock-converse.ts index 801fb8a98a27..16052648a9fc 100644 --- a/packages/ai/src/protocols/bedrock-converse.ts +++ b/packages/ai/src/protocols/bedrock-converse.ts @@ -436,21 +436,22 @@ const mapFinishReason = (reason: string): FinishReason => { return "unknown" } -// AWS Bedrock Converse reports `inputTokens` (inclusive total) with -// `cacheReadInputTokens` and `cacheWriteInputTokens` as subsets. Pass -// the total through and derive the non-cached breakdown. Bedrock does -// not break reasoning out of `outputTokens` for any current model. +// AWS reports inputTokens separately from cache reads and writes. +// Bedrock does not break reasoning out of outputTokens for current models. const mapUsage = (usage: BedrockUsageSchema | undefined): Usage | undefined => { if (!usage) return undefined - const cacheTotal = (usage.cacheReadInputTokens ?? 0) + (usage.cacheWriteInputTokens ?? 0) - const nonCached = ProviderShared.subtractTokens(usage.inputTokens, cacheTotal) + const inputTokens = ProviderShared.sumTokens( + usage.inputTokens, + usage.cacheReadInputTokens, + usage.cacheWriteInputTokens, + ) return new Usage({ - inputTokens: usage.inputTokens, + inputTokens, outputTokens: usage.outputTokens, - nonCachedInputTokens: nonCached, + nonCachedInputTokens: usage.inputTokens, cacheReadInputTokens: usage.cacheReadInputTokens, cacheWriteInputTokens: usage.cacheWriteInputTokens, - totalTokens: ProviderShared.totalTokens(usage.inputTokens, usage.outputTokens, usage.totalTokens), + totalTokens: ProviderShared.totalTokens(inputTokens, usage.outputTokens, usage.totalTokens), providerMetadata: { bedrock: usage }, }) } diff --git a/packages/ai/src/schema/events.ts b/packages/ai/src/schema/events.ts index 5be1c4cf9002..18454b84707c 100644 --- a/packages/ai/src/schema/events.ts +++ b/packages/ai/src/schema/events.ts @@ -34,11 +34,12 @@ import { ProviderFailureClassification } from "./errors" * * **Semantics by provider**: * - * - OpenAI Chat / Responses / Gemini / Bedrock: provider reports inclusive + * - OpenAI Chat / Responses / Gemini: provider reports inclusive * `inputTokens` and an inclusive `outputTokens`; mapper subtracts to * derive the breakdown. - * - Anthropic: provider reports the breakdown natively (`input_tokens` is - * non-cached only); mapper sums to derive the inclusive `inputTokens`. + * - Anthropic and Bedrock report the input breakdown natively: Anthropic's + * `input_tokens` and Bedrock's `inputTokens` are non-cached only. Their + * mappers sum the breakdown to derive the inclusive `inputTokens`. * Anthropic does *not* break extended-thinking out of `output_tokens`, so * `reasoningTokens` is `undefined` and `outputTokens` carries the * combined total — a documented limitation of the Anthropic API. diff --git a/packages/ai/test/fixtures/recordings/bedrock-converse-cache/writes-then-reads-cachepoint-on-identical-second-call.json b/packages/ai/test/fixtures/recordings/bedrock-converse-cache/writes-then-reads-cachepoint-on-identical-second-call.json new file mode 100644 index 000000000000..8fd307e2202f --- /dev/null +++ b/packages/ai/test/fixtures/recordings/bedrock-converse-cache/writes-then-reads-cachepoint-on-identical-second-call.json @@ -0,0 +1,53 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:bedrock-converse-cache", + "provider:amazon-bedrock", + "protocol:bedrock-converse", + "cache" + ], + "name": "bedrock-converse-cache/writes-then-reads-cachepoint-on-identical-second-call", + "recordedAt": "2026-07-23T02:29:10.955Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-haiku-4-5-20251001-v1%3A0/converse-stream", + "headers": { + "content-type": "application/json" + }, + "body": "{\"modelId\":\"us.anthropic.claude-haiku-4-5-20251001-v1:0\",\"messages\":[{\"role\":\"user\",\"content\":[{\"text\":\"Say hi.\"}]}],\"system\":[{\"text\":\"You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. \"},{\"cachePoint\":{\"type\":\"default\"}}],\"inferenceConfig\":{\"maxTokens\":16,\"temperature\":0}}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "application/vnd.amazon.eventstream" + }, + "body": "AAAAiwAAAFImcW4yCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1uIiwicm9sZSI6ImFzc2lzdGFudCJ9uwonDAAAANUAAABX0TjrFws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiJIaS4ifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNCJ9BToCUgAAAJMAAABWcYx2aAs6ZXZlbnQtdHlwZQcAEGNvbnRlbnRCbG9ja1N0b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwicCI6ImFiY2RlZmdoaWprbG1ubyJ9uOXHGAAAALAAAABRaYm2Hws6ZXZlbnQtdHlwZQcAC21lc3NhZ2VTdG9wDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVIiwic3RvcFJlYXNvbiI6ImVuZF90dXJuIn0SuCAcAAABgQAAAE6znPl5CzpldmVudC10eXBlBwAIbWV0YWRhdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJtZXRyaWNzIjp7ImxhdGVuY3lNcyI6MTE5OH0sInAiOiJhYmNkZWYiLCJ1c2FnZSI6eyJjYWNoZURldGFpbHMiOlt7ImlucHV0VG9rZW5zIjo1NzUyLCJ0dGwiOiI1bSJ9XSwiY2FjaGVSZWFkSW5wdXRUb2tlbkNvdW50IjowLCJjYWNoZVJlYWRJbnB1dFRva2VucyI6MCwiY2FjaGVXcml0ZUlucHV0VG9rZW5Db3VudCI6NTc1MiwiY2FjaGVXcml0ZUlucHV0VG9rZW5zIjo1NzUyLCJpbnB1dFRva2VucyI6OSwib3V0cHV0VG9rZW5zIjoyLCJzZXJ2ZXJUb29sVXNhZ2UiOnt9LCJ0b3RhbFRva2VucyI6NTc2M319YVPHOQ==", + "bodyEncoding": "base64" + } + }, + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-haiku-4-5-20251001-v1%3A0/converse-stream", + "headers": { + "content-type": "application/json" + }, + "body": "{\"modelId\":\"us.anthropic.claude-haiku-4-5-20251001-v1:0\",\"messages\":[{\"role\":\"user\",\"content\":[{\"text\":\"Say hi.\"}]}],\"system\":[{\"text\":\"You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. \"},{\"cachePoint\":{\"type\":\"default\"}}],\"inferenceConfig\":{\"maxTokens\":16,\"temperature\":0}}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "application/vnd.amazon.eventstream" + }, + "body": "AAAApgAAAFIfIIWHCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PIiwicm9sZSI6ImFzc2lzdGFudCJ9AcVkFwAAAKkAAABX7Rrm2Qs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiJIaS4ifSwicCI6ImFiY2RlZmdoaWprbG0iffxI0NkAAACiAAAAVu3N514LOmV2ZW50LXR5cGUHABBjb250ZW50QmxvY2tTdG9wDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0QifWQnKBAAAACFAAAAUQBIgekLOmV2ZW50LXR5cGUHAAttZXNzYWdlU3RvcA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7InAiOiJhYmNkIiwic3RvcFJlYXNvbiI6ImVuZF90dXJuIn0c+t0FAAABTQAAAE6fefyjCzpldmVudC10eXBlBwAIbWV0YWRhdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJtZXRyaWNzIjp7ImxhdGVuY3lNcyI6OTcwfSwicCI6ImFiY2QiLCJ1c2FnZSI6eyJjYWNoZVJlYWRJbnB1dFRva2VuQ291bnQiOjU3NTIsImNhY2hlUmVhZElucHV0VG9rZW5zIjo1NzUyLCJjYWNoZVdyaXRlSW5wdXRUb2tlbkNvdW50IjowLCJjYWNoZVdyaXRlSW5wdXRUb2tlbnMiOjAsImlucHV0VG9rZW5zIjo5LCJvdXRwdXRUb2tlbnMiOjIsInNlcnZlclRvb2xVc2FnZSI6e30sInRvdGFsVG9rZW5zIjo1NzYzfX0J7IoM", + "bodyEncoding": "base64" + } + } + ] +} diff --git a/packages/ai/test/provider/bedrock-converse-cache.recorded.test.ts b/packages/ai/test/provider/bedrock-converse-cache.recorded.test.ts index 8702e4eb4034..8209ab1121af 100644 --- a/packages/ai/test/provider/bedrock-converse-cache.recorded.test.ts +++ b/packages/ai/test/provider/bedrock-converse-cache.recorded.test.ts @@ -13,12 +13,8 @@ const RECORDING_REGION = process.env.BEDROCK_RECORDING_REGION ?? "us-east-1" // call wouldn't deterministically prove cache mapping works. Override with // BEDROCK_CACHE_MODEL_ID if your account has access elsewhere. const model = AmazonBedrock.configure({ - credentials: { - region: RECORDING_REGION, - accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? "fixture", - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? "fixture", - sessionToken: process.env.AWS_SESSION_TOKEN, - }, + apiKey: process.env.AWS_BEARER_TOKEN_BEDROCK ?? "fixture", + region: RECORDING_REGION, }).model(process.env.BEDROCK_CACHE_MODEL_ID ?? "us.anthropic.claude-haiku-4-5-20251001-v1:0") const cacheRequest = LLM.request({ @@ -36,7 +32,7 @@ const recorded = recordedTests({ prefix: "bedrock-converse-cache", provider: "amazon-bedrock", protocol: "bedrock-converse", - requires: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"], + requires: ["AWS_BEARER_TOKEN_BEDROCK"], // Two identical requests in one cassette — replay walks the cassette in // recording order so the second call replays the cached-hit interaction. }) @@ -45,10 +41,20 @@ describe("Bedrock Converse cache recorded", () => { recorded.effect.with("writes then reads cachePoint on identical second call", { tags: ["cache"] }, () => Effect.gen(function* () { const first = yield* LLMClient.generate(cacheRequest) - expect(first.usage?.cacheReadInputTokens ?? 0).toBeGreaterThanOrEqual(0) + expect(first.usage?.cacheWriteInputTokens ?? 0).toBeGreaterThan(0) + expect(first.usage?.inputTokens).toBe( + (first.usage?.nonCachedInputTokens ?? 0) + + (first.usage?.cacheReadInputTokens ?? 0) + + (first.usage?.cacheWriteInputTokens ?? 0), + ) const second = yield* LLMClient.generate(cacheRequest) expect(second.usage?.cacheReadInputTokens ?? 0).toBeGreaterThan(0) + expect(second.usage?.inputTokens).toBe( + (second.usage?.nonCachedInputTokens ?? 0) + + (second.usage?.cacheReadInputTokens ?? 0) + + (second.usage?.cacheWriteInputTokens ?? 0), + ) }), ) }) diff --git a/packages/ai/test/provider/bedrock-converse.test.ts b/packages/ai/test/provider/bedrock-converse.test.ts index 59ca67333bf5..776032966c84 100644 --- a/packages/ai/test/provider/bedrock-converse.test.ts +++ b/packages/ai/test/provider/bedrock-converse.test.ts @@ -269,6 +269,39 @@ describe("Bedrock Converse route", () => { }), ) + it.effect("adds cache reads and writes to Bedrock input usage", () => + Effect.gen(function* () { + const body = eventStreamBody( + ["messageStart", { role: "assistant" }], + ["contentBlockDelta", { contentBlockIndex: 0, delta: { text: "Hello" } }], + ["contentBlockStop", { contentBlockIndex: 0 }], + ["messageStop", { stopReason: "end_turn" }], + [ + "metadata", + { + usage: { + inputTokens: 5, + outputTokens: 2, + totalTokens: 12, + cacheReadInputTokens: 3, + cacheWriteInputTokens: 2, + }, + }, + ], + ) + const response = yield* LLMClient.generate(baseRequest).pipe(Effect.provide(fixedBytes(body))) + + expect(response.usage).toMatchObject({ + inputTokens: 10, + nonCachedInputTokens: 5, + cacheReadInputTokens: 3, + cacheWriteInputTokens: 2, + outputTokens: 2, + totalTokens: 12, + }) + }), + ) + it.effect("assembles streamed tool call input", () => Effect.gen(function* () { const body = eventStreamBody( From b6f85c2250ba81d826ea118a8256db53a7a7d8b3 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 23:20:16 -0500 Subject: [PATCH 047/150] fix(core): default custom model capabilities (#38449) Co-authored-by: Aiden Cline --- packages/core/src/catalog.ts | 2 +- packages/core/src/github-copilot/models.ts | 2 +- packages/core/test/aisdk.test.ts | 2 +- packages/core/test/config/provider.test.ts | 80 +++++++++++++++++++ packages/core/test/generate.test.ts | 2 +- .../core/test/github-copilot/models.test.ts | 4 +- .../plugin/provider-amazon-bedrock.test.ts | 42 +++++----- .../test/plugin/provider-anthropic.test.ts | 4 +- .../provider-azure-cognitive-services.test.ts | 12 +-- .../core/test/plugin/provider-azure.test.ts | 18 ++--- .../test/plugin/provider-cerebras.test.ts | 6 +- .../provider-cloudflare-ai-gateway.test.ts | 22 ++--- .../provider-cloudflare-workers-ai.test.ts | 12 +-- .../core/test/plugin/provider-dynamic.test.ts | 16 ++-- .../core/test/plugin/provider-factory.test.ts | 2 +- .../plugin/provider-github-copilot.test.ts | 30 +++---- .../core/test/plugin/provider-gitlab.test.ts | 16 ++-- .../provider-google-vertex-anthropic.test.ts | 16 ++-- .../plugin/provider-google-vertex.test.ts | 8 +- .../core/test/plugin/provider-google.test.ts | 8 +- .../plugin/provider-openai-compatible.test.ts | 10 +-- .../core/test/plugin/provider-openai.test.ts | 8 +- .../test/plugin/provider-opencode.test.ts | 14 ++-- .../test/plugin/provider-openrouter.test.ts | 4 +- .../test/plugin/provider-sap-ai-core.test.ts | 2 +- .../plugin/provider-snowflake-cortex.test.ts | 12 +-- .../core/test/plugin/provider-vercel.test.ts | 2 +- .../core/test/plugin/provider-xai.test.ts | 10 +-- packages/core/test/shared-schema.test.ts | 4 +- packages/docs/models.mdx | 12 +-- packages/schema/src/model.ts | 4 +- packages/schema/test/contract-hygiene.test.ts | 2 +- 32 files changed, 235 insertions(+), 153 deletions(-) diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts index 9f634447907f..9b8f916e7075 100644 --- a/packages/core/src/catalog.ts +++ b/packages/core/src/catalog.ts @@ -116,7 +116,7 @@ const layer = Layer.effect( draft.providers.set(providerID, record) } const model = - record.models.get(modelID) ?? (ModelV2.Info.empty(providerID, modelID) as ModelV2.MutableInfo) + record.models.get(modelID) ?? (ModelV2.Info.default(providerID, modelID) as ModelV2.MutableInfo) if (!record.models.has(modelID)) record.models.set(modelID, model) fn(model) model.id = modelID diff --git a/packages/core/src/github-copilot/models.ts b/packages/core/src/github-copilot/models.ts index 8790cefba2c3..52f98753b5f1 100644 --- a/packages/core/src/github-copilot/models.ts +++ b/packages/core/src/github-copilot/models.ts @@ -135,7 +135,7 @@ function build(id: ModelV2.ID, remote: UsableModel, baseURL: string, previous?: const released = previous?.time.released || Date.parse(version) return ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.githubCopilot, id), + ...ModelV2.Info.default(ProviderV2.ID.githubCopilot, id), id, modelID: ModelV2.ID.make(remote.id), providerID: ProviderV2.ID.githubCopilot, diff --git a/packages/core/test/aisdk.test.ts b/packages/core/test/aisdk.test.ts index 0bc16e8f597e..9e7e0d0c6eaf 100644 --- a/packages/core/test/aisdk.test.ts +++ b/packages/core/test/aisdk.test.ts @@ -12,7 +12,7 @@ const it = testEffect(AISDK.locationLayer) const model = (packageName: string, settings: Record = {}) => ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("test-provider"), ModelV2.ID.make("catalog-model")), + ...ModelV2.Info.default(ProviderV2.ID.make("test-provider"), ModelV2.ID.make("catalog-model")), modelID: ModelV2.ID.make("api-model"), package: ProviderV2.aisdk(packageName), settings, diff --git a/packages/core/test/config/provider.test.ts b/packages/core/test/config/provider.test.ts index c3c5b9c42057..60597836f2df 100644 --- a/packages/core/test/config/provider.test.ts +++ b/packages/core/test/config/provider.test.ts @@ -49,6 +49,86 @@ function withEnv(vars: Record, effect: () = const decode = Schema.decodeUnknownSync(Config.Info) describe("ConfigProviderPlugin.Plugin", () => { + it.effect("defaults custom models to agent capabilities", () => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + const providerID = ProviderV2.ID.make("custom") + const modelID = ModelV2.ID.make("chat") + const config = Config.Service.of({ + entries: () => + Effect.succeed([ + new Config.Document({ + type: "document", + info: decode({ + providers: { + custom: { + package: "aisdk:@ai-sdk/openai-compatible", + models: { chat: {} }, + }, + }, + }), + }), + ]), + }) + + yield* addPlugin(config) + + const model = required(yield* catalog.model.get(providerID, modelID)) + expect(model.capabilities).toEqual({ tools: true, input: ["text", "image"], output: ["text"] }) + }), + ) + + it.effect("preserves catalog capabilities unless config overrides them", () => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + const providerID = ProviderV2.ID.make("custom") + const inheritedID = ModelV2.ID.make("inherited") + const overriddenID = ModelV2.ID.make("overridden") + yield* catalog.transform((draft) => { + draft.model.update(providerID, inheritedID, (model) => { + model.capabilities = { tools: false, input: ["text"], output: ["text"] } + }) + draft.model.update(providerID, overriddenID, (model) => { + model.capabilities = { tools: false, input: ["text"], output: ["text"] } + }) + }) + const config = Config.Service.of({ + entries: () => + Effect.succeed([ + new Config.Document({ + type: "document", + info: decode({ + providers: { + custom: { + package: "aisdk:@ai-sdk/openai-compatible", + models: { + inherited: { name: "Inherited" }, + overridden: { + capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, + }, + }, + }, + }, + }), + }), + ]), + }) + + yield* addPlugin(config) + + expect((yield* catalog.model.get(providerID, inheritedID))?.capabilities).toEqual({ + tools: false, + input: ["text"], + output: ["text"], + }) + expect((yield* catalog.model.get(providerID, overriddenID))?.capabilities).toEqual({ + tools: true, + input: ["text", "image"], + output: ["text"], + }) + }), + ) + it.effect("keeps configured model variant bodies unchanged", () => Effect.gen(function* () { const catalog = yield* Catalog.Service diff --git a/packages/core/test/generate.test.ts b/packages/core/test/generate.test.ts index d990f96aceae..c0ad30d3831c 100644 --- a/packages/core/test/generate.test.ts +++ b/packages/core/test/generate.test.ts @@ -13,7 +13,7 @@ import { Effect, Layer, Stream } from "effect" import { testEffect } from "./lib/effect" const selected = ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("test-provider"), ModelV2.ID.make("gemini")), + ...ModelV2.Info.default(ProviderV2.ID.make("test-provider"), ModelV2.ID.make("gemini")), package: ProviderV2.aisdk("@ai-sdk/google"), }) const runtime = Model.make({ id: "gemini", provider: "test-provider", route: OpenAIChat.route }) diff --git a/packages/core/test/github-copilot/models.test.ts b/packages/core/test/github-copilot/models.test.ts index 31d8f710cd2c..9bd07971fdf1 100644 --- a/packages/core/test/github-copilot/models.test.ts +++ b/packages/core/test/github-copilot/models.test.ts @@ -49,12 +49,12 @@ test("defensively syncs advertised Copilot models", async () => { try { const existing = ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.githubCopilot, ModelV2.ID.make("gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.githubCopilot, ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), name: "GPT-5 local", }) const stale = ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.githubCopilot, ModelV2.ID.make("stale")), + ...ModelV2.Info.default(ProviderV2.ID.githubCopilot, ModelV2.ID.make("stale")), modelID: ModelV2.ID.make("stale"), }) const models = await CopilotModels.get(server.url.origin, {}, [existing, stale]) diff --git a/packages/core/test/plugin/provider-amazon-bedrock.test.ts b/packages/core/test/plugin/provider-amazon-bedrock.test.ts index db63910c33f5..39f46c49a2c4 100644 --- a/packages/core/test/plugin/provider-amazon-bedrock.test.ts +++ b/packages/core/test/plugin/provider-amazon-bedrock.test.ts @@ -108,7 +108,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -134,7 +134,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -169,7 +169,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -190,7 +190,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -210,7 +210,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -230,7 +230,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -251,7 +251,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -281,7 +281,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -310,7 +310,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), modelID: ModelV2.ID.make("openai.gpt-5.5"), package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"), }), @@ -338,7 +338,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), modelID: ModelV2.ID.make("openai.gpt-5.5"), package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"), }), @@ -347,7 +347,7 @@ describe("AmazonBedrockPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")), modelID: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"), package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"), }), @@ -365,7 +365,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/anthropic"), }), @@ -393,7 +393,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -425,7 +425,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -434,7 +434,7 @@ describe("AmazonBedrockPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -443,7 +443,7 @@ describe("AmazonBedrockPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -452,7 +452,7 @@ describe("AmazonBedrockPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -461,7 +461,7 @@ describe("AmazonBedrockPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -487,7 +487,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -574,7 +574,7 @@ describe("AmazonBedrockPlugin", () => { for (const item of cases) { yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)), modelID: ModelV2.ID.make(item.modelID), package: ProviderV2.aisdk("test-provider"), }), @@ -594,7 +594,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), diff --git a/packages/core/test/plugin/provider-anthropic.test.ts b/packages/core/test/plugin/provider-anthropic.test.ts index af7b80dc6735..0c0bcba461e8 100644 --- a/packages/core/test/plugin/provider-anthropic.test.ts +++ b/packages/core/test/plugin/provider-anthropic.test.ts @@ -63,7 +63,7 @@ describe("AnthropicPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-anthropic"), ModelV2.ID.make("claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom-anthropic"), ModelV2.ID.make("claude-sonnet-4-5")), modelID: ModelV2.ID.make("claude-sonnet-4-5"), package: ProviderV2.aisdk("@ai-sdk/anthropic"), }), @@ -81,7 +81,7 @@ describe("AnthropicPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")), modelID: ModelV2.ID.make("claude-sonnet-4-5"), package: ProviderV2.aisdk("@ai-sdk/anthropic"), }), diff --git a/packages/core/test/plugin/provider-azure-cognitive-services.test.ts b/packages/core/test/plugin/provider-azure-cognitive-services.test.ts index f52f013105d1..5b9ca4bf86f9 100644 --- a/packages/core/test/plugin/provider-azure-cognitive-services.test.ts +++ b/packages/core/test/plugin/provider-azure-cognitive-services.test.ts @@ -121,7 +121,7 @@ describe("AzureCognitiveServicesPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")), + ...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")), modelID: ModelV2.ID.make("deployment"), package: "aisdk:test-provider", }), @@ -140,7 +140,7 @@ describe("AzureCognitiveServicesPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")), + ...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")), modelID: ModelV2.ID.make("deployment"), package: "aisdk:test-provider", }), @@ -149,7 +149,7 @@ describe("AzureCognitiveServicesPlugin", () => { }) const ignored = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")), + ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("deployment")), modelID: ModelV2.ID.make("deployment"), package: "aisdk:test-provider", }), @@ -170,7 +170,7 @@ describe("AzureCognitiveServicesPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("messages-deployment")), + ...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("messages-deployment")), modelID: ModelV2.ID.make("messages-deployment"), package: "aisdk:test-provider", }), @@ -179,7 +179,7 @@ describe("AzureCognitiveServicesPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")), + ...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")), modelID: ModelV2.ID.make("chat-deployment"), package: "aisdk:test-provider", }), @@ -188,7 +188,7 @@ describe("AzureCognitiveServicesPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("language-deployment")), + ...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("language-deployment")), modelID: ModelV2.ID.make("language-deployment"), package: "aisdk:test-provider", }), diff --git a/packages/core/test/plugin/provider-azure.test.ts b/packages/core/test/plugin/provider-azure.test.ts index 6cf515c0c608..227e3bfa7834 100644 --- a/packages/core/test/plugin/provider-azure.test.ts +++ b/packages/core/test/plugin/provider-azure.test.ts @@ -148,7 +148,7 @@ describe("AzurePlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), + ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), modelID: ModelV2.ID.make("deployment"), package: ProviderV2.aisdk("test-provider"), }), @@ -168,7 +168,7 @@ describe("AzurePlugin", () => { const exit = yield* aisdk .runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), + ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), modelID: ModelV2.ID.make("deployment"), package: ProviderV2.aisdk("test-provider"), }), @@ -189,7 +189,7 @@ describe("AzurePlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), + ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), modelID: ModelV2.ID.make("deployment"), package: ProviderV2.aisdk("test-provider"), }), @@ -208,7 +208,7 @@ describe("AzurePlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), + ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), modelID: ModelV2.ID.make("deployment"), package: ProviderV2.aisdk("test-provider"), }), @@ -227,7 +227,7 @@ describe("AzurePlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), + ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), modelID: ModelV2.ID.make("deployment"), package: ProviderV2.aisdk("test-provider"), body: { useCompletionUrls: true }, @@ -247,7 +247,7 @@ describe("AzurePlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), + ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), modelID: ModelV2.ID.make("deployment"), package: ProviderV2.aisdk("test-provider"), }), @@ -256,7 +256,7 @@ describe("AzurePlugin", () => { }) const ignored = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")), + ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("deployment")), modelID: ModelV2.ID.make("deployment"), package: ProviderV2.aisdk("test-provider"), }), @@ -280,7 +280,7 @@ describe("AzurePlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")), + ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")), modelID: ModelV2.ID.make("messages-deployment"), package: ProviderV2.aisdk("test-provider"), }), @@ -289,7 +289,7 @@ describe("AzurePlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")), + ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")), modelID: ModelV2.ID.make("language-deployment"), package: ProviderV2.aisdk("test-provider"), }), diff --git a/packages/core/test/plugin/provider-cerebras.test.ts b/packages/core/test/plugin/provider-cerebras.test.ts index eb5c4ec1bf54..6722f48996f5 100644 --- a/packages/core/test/plugin/provider-cerebras.test.ts +++ b/packages/core/test/plugin/provider-cerebras.test.ts @@ -65,7 +65,7 @@ describe("CerebrasPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty( + ...ModelV2.Info.default( ProviderV2.ID.make("custom-cerebras"), ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), ), @@ -88,7 +88,7 @@ describe("CerebrasPlugin", () => { yield* addPlugin() yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty( + ...ModelV2.Info.default( ProviderV2.ID.make("custom-cerebras"), ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), ), @@ -110,7 +110,7 @@ describe("CerebrasPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty( + ...ModelV2.Info.default( ProviderV2.ID.make("custom-cerebras"), ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), ), diff --git a/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts b/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts index cbd2b95c9eba..bd9d9e80cd27 100644 --- a/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts @@ -117,7 +117,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), @@ -139,7 +139,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), @@ -184,7 +184,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), @@ -214,7 +214,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), @@ -252,7 +252,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), @@ -284,7 +284,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), @@ -307,7 +307,7 @@ describe("CloudflareAIGatewayPlugin", () => { const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), @@ -331,7 +331,7 @@ describe("CloudflareAIGatewayPlugin", () => { const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), @@ -361,7 +361,7 @@ describe("CloudflareAIGatewayPlugin", () => { const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), @@ -385,7 +385,7 @@ describe("CloudflareAIGatewayPlugin", () => { const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty( + ...ModelV2.Info.default( ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("anthropic/claude-sonnet-4-5"), ), @@ -417,7 +417,7 @@ describe("CloudflareAIGatewayPlugin", () => { const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), diff --git a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts index d67b8d91cc2e..5f4e3bc27609 100644 --- a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts @@ -94,7 +94,7 @@ describe("CloudflareWorkersAIPlugin", () => { const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))) const sdk = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), modelID: ModelV2.ID.make("@cf/model"), package: provider.package, settings: provider.settings, @@ -138,7 +138,7 @@ describe("CloudflareWorkersAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), modelID: ModelV2.ID.make("@cf/model"), package: "aisdk:@ai-sdk/openai-compatible", settings: { baseURL: "https://proxy.example/v1" }, @@ -178,7 +178,7 @@ describe("CloudflareWorkersAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), modelID: ModelV2.ID.make("@cf/model"), package: "aisdk:@ai-sdk/openai-compatible", settings: { baseURL: "https://proxy.example/v1" }, @@ -207,7 +207,7 @@ describe("CloudflareWorkersAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), modelID: ModelV2.ID.make("@cf/model"), package: "aisdk:@ai-sdk/openai-compatible", settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1" }, @@ -233,7 +233,7 @@ describe("CloudflareWorkersAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("alias")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("alias")), modelID: ModelV2.ID.make("@cf/api-model"), package: "aisdk:test-provider", }), @@ -253,7 +253,7 @@ describe("CloudflareWorkersAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), modelID: ModelV2.ID.make("@cf/model"), package: "aisdk:@ai-sdk/anthropic", settings: { baseURL: "https://proxy.example/v1" }, diff --git a/packages/core/test/plugin/provider-dynamic.test.ts b/packages/core/test/plugin/provider-dynamic.test.ts index d0b1a8af5ace..341878e6ded4 100644 --- a/packages/core/test/plugin/provider-dynamic.test.ts +++ b/packages/core/test/plugin/provider-dynamic.test.ts @@ -53,7 +53,7 @@ describe("DynamicProviderPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")), modelID: ModelV2.ID.make("test-model"), package: ProviderV2.aisdk(fixtureProvider), }), @@ -72,7 +72,7 @@ describe("DynamicProviderPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")), modelID: ModelV2.ID.make("test-model"), package: ProviderV2.aisdk(fixtureProvider), }), @@ -90,7 +90,7 @@ describe("DynamicProviderPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("test-model")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("test-model")), modelID: ModelV2.ID.make("test-model"), package: ProviderV2.aisdk(fixtureProvider), }), @@ -107,7 +107,7 @@ describe("DynamicProviderPlugin", () => { yield* addPlugin(npmEntrypoint(fixtureProviderPath)) const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("npm-provider"), ModelV2.ID.make("test-model")), + ...ModelV2.Info.default(ProviderV2.ID.make("npm-provider"), ModelV2.ID.make("test-model")), modelID: ModelV2.ID.make("test-model"), package: "aisdk:fixture-provider", }), @@ -125,7 +125,7 @@ describe("DynamicProviderPlugin", () => { const exit = yield* aisdk .language( ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("missing-entrypoint"), ModelV2.ID.make("alias")), + ...ModelV2.Info.default(ProviderV2.ID.make("missing-entrypoint"), ModelV2.ID.make("alias")), modelID: ModelV2.ID.make("alias"), package: "aisdk:fixture-provider", }), @@ -143,7 +143,7 @@ describe("DynamicProviderPlugin", () => { const exit = yield* aisdk .language( ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("bad-import"), ModelV2.ID.make("alias")), + ...ModelV2.Info.default(ProviderV2.ID.make("bad-import"), ModelV2.ID.make("alias")), modelID: ModelV2.ID.make("alias"), package: "aisdk:file:///missing/provider-factory.js", }), @@ -163,7 +163,7 @@ describe("DynamicProviderPlugin", () => { const exit = yield* aisdk .language( ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("missing-factory"), ModelV2.ID.make("alias")), + ...ModelV2.Info.default(ProviderV2.ID.make("missing-factory"), ModelV2.ID.make("alias")), modelID: ModelV2.ID.make("alias"), package: "aisdk:fixture-provider", }), @@ -181,7 +181,7 @@ describe("DynamicProviderPlugin", () => { yield* addPlugin() const language = yield* aisdk.language( ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("alias")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("alias")), modelID: ModelV2.ID.make("test-model-api"), package: ProviderV2.aisdk(fixtureProvider), }), diff --git a/packages/core/test/plugin/provider-factory.test.ts b/packages/core/test/plugin/provider-factory.test.ts index a884fd0ce0fb..d51d4900b493 100644 --- a/packages/core/test/plugin/provider-factory.test.ts +++ b/packages/core/test/plugin/provider-factory.test.ts @@ -41,7 +41,7 @@ providers.forEach((item) => const host = yield* PluginHost.make(plugin) yield* item.plugin.effect(host) const model = ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make(item.id), modelID), + ...ModelV2.Info.default(ProviderV2.ID.make(item.id), modelID), modelID, package: ProviderV2.aisdk(item.package), }) diff --git a/packages/core/test/plugin/provider-github-copilot.test.ts b/packages/core/test/plugin/provider-github-copilot.test.ts index bb2443ae4121..b39e06ac7bef 100644 --- a/packages/core/test/plugin/provider-github-copilot.test.ts +++ b/packages/core/test/plugin/provider-github-copilot.test.ts @@ -99,7 +99,7 @@ describe("GithubCopilotPlugin", () => { yield* addPlugin() const ignored = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), package: "aisdk:test-provider", }), @@ -108,7 +108,7 @@ describe("GithubCopilotPlugin", () => { }) const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), package: "aisdk:test-provider", }), @@ -128,7 +128,7 @@ describe("GithubCopilotPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")), modelID: ModelV2.ID.make("claude-sonnet-4"), package: "aisdk:test-provider", }), @@ -147,7 +147,7 @@ describe("GithubCopilotPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")), modelID: ModelV2.ID.make("claude-sonnet-4"), package: "aisdk:test-provider", }), @@ -166,7 +166,7 @@ describe("GithubCopilotPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), package: "aisdk:test-provider", }), @@ -175,7 +175,7 @@ describe("GithubCopilotPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")), modelID: ModelV2.ID.make("gpt-5.1-codex"), package: "aisdk:test-provider", }), @@ -184,7 +184,7 @@ describe("GithubCopilotPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")), modelID: ModelV2.ID.make("gpt-4o"), package: "aisdk:test-provider", }), @@ -193,7 +193,7 @@ describe("GithubCopilotPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")), modelID: ModelV2.ID.make("gpt-5-mini"), package: "aisdk:test-provider", }), @@ -202,7 +202,7 @@ describe("GithubCopilotPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini-2025-08-07")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini-2025-08-07")), modelID: ModelV2.ID.make("gpt-5-mini-2025-08-07"), package: "aisdk:test-provider", }), @@ -227,7 +227,7 @@ describe("GithubCopilotPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("mai-code-1-flash-picker")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("mai-code-1-flash-picker")), modelID: ModelV2.ID.make("mai-code-1-flash-picker"), package: "aisdk:test-provider", settings: { endpoint: "responses" }, @@ -237,7 +237,7 @@ describe("GithubCopilotPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), package: "aisdk:test-provider", settings: { endpoint: "chat" }, @@ -257,7 +257,7 @@ describe("GithubCopilotPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")), modelID: ModelV2.ID.make("gpt-5"), package: "aisdk:test-provider", }), @@ -266,7 +266,7 @@ describe("GithubCopilotPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")), modelID: ModelV2.ID.make("gpt-5-mini"), package: "aisdk:test-provider", }), @@ -275,7 +275,7 @@ describe("GithubCopilotPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")), modelID: ModelV2.ID.make("claude-sonnet-4"), package: "aisdk:test-provider", }), @@ -324,7 +324,7 @@ describe("GithubCopilotPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), package: "aisdk:test-provider", }), diff --git a/packages/core/test/plugin/provider-gitlab.test.ts b/packages/core/test/plugin/provider-gitlab.test.ts index ae66a61aa81e..ac12ebfc9d60 100644 --- a/packages/core/test/plugin/provider-gitlab.test.ts +++ b/packages/core/test/plugin/provider-gitlab.test.ts @@ -69,7 +69,7 @@ describe("GitLabPlugin", () => { yield* addPlugin() yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), + ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), modelID: ModelV2.ID.make("claude"), package: "aisdk:test-provider", }), @@ -107,7 +107,7 @@ describe("GitLabPlugin", () => { yield* addPlugin() yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), + ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), modelID: ModelV2.ID.make("claude"), package: "aisdk:test-provider", }), @@ -133,7 +133,7 @@ describe("GitLabPlugin", () => { yield* addPlugin() yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), + ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), modelID: ModelV2.ID.make("claude"), package: "aisdk:test-provider", }), @@ -175,7 +175,7 @@ describe("GitLabPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), + ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), modelID: ModelV2.ID.make("claude"), package: "aisdk:test-provider", }), @@ -195,7 +195,7 @@ describe("GitLabPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), + ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), modelID: ModelV2.ID.make("duo-workflow-custom"), package: "aisdk:test-provider", headers: {}, @@ -229,7 +229,7 @@ describe("GitLabPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-exact")), + ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-exact")), modelID: ModelV2.ID.make("duo-workflow-exact"), package: "aisdk:test-provider", }), @@ -257,7 +257,7 @@ describe("GitLabPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), + ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), modelID: ModelV2.ID.make("duo-workflow-custom"), package: "aisdk:test-provider", headers: {}, @@ -284,7 +284,7 @@ describe("GitLabPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), + ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), modelID: ModelV2.ID.make("claude"), package: "aisdk:test-provider", headers: { h: "v" }, diff --git a/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts b/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts index 090abb945c29..3b6726d37303 100644 --- a/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts +++ b/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts @@ -116,7 +116,7 @@ describe("GoogleVertexAnthropicPlugin", () => { yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty( + ...ModelV2.Info.default( ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make("claude-sonnet-4-5"), ), @@ -143,7 +143,7 @@ describe("GoogleVertexAnthropicPlugin", () => { yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty( + ...ModelV2.Info.default( ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make("claude-sonnet-4-5"), ), @@ -167,7 +167,7 @@ describe("GoogleVertexAnthropicPlugin", () => { yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), modelID: ModelV2.ID.make("claude-sonnet-4-5"), package: "aisdk:test-provider", }), @@ -187,7 +187,7 @@ describe("GoogleVertexAnthropicPlugin", () => { yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), modelID: ModelV2.ID.make("claude-sonnet-4-5"), package: "aisdk:test-provider", }), @@ -206,7 +206,7 @@ describe("GoogleVertexAnthropicPlugin", () => { yield* addPlugin(GoogleVertexAnthropicPlugin) const sdkResult = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")), + ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")), modelID: ModelV2.ID.make(" claude-sonnet-4-5 "), package: "aisdk:test-provider", }), @@ -215,7 +215,7 @@ describe("GoogleVertexAnthropicPlugin", () => { }) const languageResult = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")), + ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")), modelID: ModelV2.ID.make(" claude-sonnet-4-5 "), package: "aisdk:test-provider", }), @@ -238,7 +238,7 @@ describe("GoogleVertexAnthropicPlugin", () => { yield* addPlugin(GoogleVertexAnthropicPlugin) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make(" claude-sonnet-4-5 ")), + ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make(" claude-sonnet-4-5 ")), modelID: ModelV2.ID.make(" claude-sonnet-4-5 "), package: "aisdk:test-provider", }), @@ -257,7 +257,7 @@ describe("GoogleVertexAnthropicPlugin", () => { yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), modelID: ModelV2.ID.make("claude-sonnet-4-5"), package: "aisdk:test-provider", }), diff --git a/packages/core/test/plugin/provider-google-vertex.test.ts b/packages/core/test/plugin/provider-google-vertex.test.ts index 635beba3c1c7..15949b49454d 100644 --- a/packages/core/test/plugin/provider-google-vertex.test.ts +++ b/packages/core/test/plugin/provider-google-vertex.test.ts @@ -172,7 +172,7 @@ describe("GoogleVertexPlugin", () => { const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))) yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), + ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), modelID: ModelV2.ID.make("gemini"), package: "aisdk:@ai-sdk/google-vertex", }), @@ -294,7 +294,7 @@ describe("GoogleVertexPlugin", () => { yield* addPlugin() yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), + ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), modelID: ModelV2.ID.make("gemini"), package: "aisdk:@ai-sdk/google-vertex", }), @@ -339,7 +339,7 @@ describe("GoogleVertexPlugin", () => { () => aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), + ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), modelID: ModelV2.ID.make("gemini"), package: "aisdk:@ai-sdk/openai-compatible", }), @@ -367,7 +367,7 @@ describe("GoogleVertexPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" gemini-2.5-pro ")), + ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" gemini-2.5-pro ")), modelID: ModelV2.ID.make(" gemini-2.5-pro "), package: "aisdk:test-provider", }), diff --git a/packages/core/test/plugin/provider-google.test.ts b/packages/core/test/plugin/provider-google.test.ts index d04e00e3b323..2dea12a2e662 100644 --- a/packages/core/test/plugin/provider-google.test.ts +++ b/packages/core/test/plugin/provider-google.test.ts @@ -26,7 +26,7 @@ describe("GooglePlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("gemini")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("gemini")), modelID: ModelV2.ID.make("gemini"), package: "aisdk:@ai-sdk/google", }), @@ -45,7 +45,7 @@ describe("GooglePlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini")), + ...ModelV2.Info.default(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini")), modelID: ModelV2.ID.make("gemini"), package: "aisdk:@ai-sdk/google", }), @@ -63,7 +63,7 @@ describe("GooglePlugin", () => { yield* addPlugin() const sdkEvent = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")), modelID: ModelV2.ID.make("gemini-api"), package: "aisdk:@ai-sdk/google", }), @@ -88,7 +88,7 @@ describe("GooglePlugin", () => { const resolved = yield* aisdk.model( ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")), modelID: ModelV2.ID.make("gemini-api"), package: "aisdk:@ai-sdk/google", settings: { apiKey: "test" }, diff --git a/packages/core/test/plugin/provider-openai-compatible.test.ts b/packages/core/test/plugin/provider-openai-compatible.test.ts index a954af9f00a2..e1cf1ed6c863 100644 --- a/packages/core/test/plugin/provider-openai-compatible.test.ts +++ b/packages/core/test/plugin/provider-openai-compatible.test.ts @@ -26,7 +26,7 @@ describe("OpenAICompatiblePlugin", () => { yield* addPlugin() const defaulted = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), modelID: ModelV2.ID.make("model"), package: "aisdk:test-provider", }), @@ -35,7 +35,7 @@ describe("OpenAICompatiblePlugin", () => { }) const disabled = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), modelID: ModelV2.ID.make("model"), package: "aisdk:test-provider", }), @@ -54,7 +54,7 @@ describe("OpenAICompatiblePlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), modelID: ModelV2.ID.make("model"), package: "aisdk:test-provider", }), @@ -78,7 +78,7 @@ describe("OpenAICompatiblePlugin", () => { ) yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("model")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("model")), modelID: ModelV2.ID.make("model"), package: "aisdk:test-provider", }), @@ -99,7 +99,7 @@ describe("OpenAICompatiblePlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("model")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("model")), modelID: ModelV2.ID.make("model"), package: "aisdk:test-provider", }), diff --git a/packages/core/test/plugin/provider-openai.test.ts b/packages/core/test/plugin/provider-openai.test.ts index cd0c943442f8..741966eebede 100644 --- a/packages/core/test/plugin/provider-openai.test.ts +++ b/packages/core/test/plugin/provider-openai.test.ts @@ -68,7 +68,7 @@ describe("OpenAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -86,7 +86,7 @@ describe("OpenAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -105,7 +105,7 @@ describe("OpenAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("alias")), + ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("alias")), modelID: ModelV2.ID.make("gpt-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -125,7 +125,7 @@ describe("OpenAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), package: ProviderV2.aisdk("test-provider"), }), diff --git a/packages/core/test/plugin/provider-opencode.test.ts b/packages/core/test/plugin/provider-opencode.test.ts index 1de22a03ddd4..f0b9fff8d7ae 100644 --- a/packages/core/test/plugin/provider-opencode.test.ts +++ b/packages/core/test/plugin/provider-opencode.test.ts @@ -293,7 +293,7 @@ describe("OpencodePlugin", () => { package: ProviderV2.aisdk("test-provider"), }) const model = ModelV2.Info.make({ - ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")), + ...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")), modelID: ModelV2.ID.make("paid"), package: ProviderV2.aisdk("test-provider"), cost: cost(1), @@ -320,7 +320,7 @@ describe("OpencodePlugin", () => { package: ProviderV2.aisdk("test-provider"), }) const model = ModelV2.Info.make({ - ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("free")), + ...ModelV2.Info.default(provider.id, ModelV2.ID.make("free")), modelID: ModelV2.ID.make("free"), package: ProviderV2.aisdk("test-provider"), cost: cost(0), @@ -347,7 +347,7 @@ describe("OpencodePlugin", () => { package: ProviderV2.aisdk("test-provider"), }) const model = ModelV2.Info.make({ - ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("output-only")), + ...ModelV2.Info.default(provider.id, ModelV2.ID.make("output-only")), modelID: ModelV2.ID.make("output-only"), package: ProviderV2.aisdk("test-provider"), cost: cost(0, 1), @@ -376,7 +376,7 @@ describe("OpencodePlugin", () => { package: ProviderV2.aisdk("test-provider"), }) const model = ModelV2.Info.make({ - ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")), + ...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")), modelID: ModelV2.ID.make("paid"), package: ProviderV2.aisdk("test-provider"), cost: cost(1), @@ -410,7 +410,7 @@ describe("OpencodePlugin", () => { package: ProviderV2.aisdk("test-provider"), }) const model = ModelV2.Info.make({ - ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")), + ...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")), modelID: ModelV2.ID.make("paid"), package: ProviderV2.aisdk("test-provider"), cost: cost(1), @@ -438,7 +438,7 @@ describe("OpencodePlugin", () => { settings: { apiKey: "configured" }, }) const model = ModelV2.Info.make({ - ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")), + ...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")), modelID: ModelV2.ID.make("paid"), package: ProviderV2.aisdk("test-provider"), cost: cost(1), @@ -468,7 +468,7 @@ describe("OpencodePlugin", () => { package: ProviderV2.aisdk("test-provider"), }) const model = ModelV2.Info.make({ - ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")), + ...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")), modelID: ModelV2.ID.make("paid"), package: ProviderV2.aisdk("test-provider"), cost: cost(1), diff --git a/packages/core/test/plugin/provider-openrouter.test.ts b/packages/core/test/plugin/provider-openrouter.test.ts index 21ee3ae216ab..63520e01d3f6 100644 --- a/packages/core/test/plugin/provider-openrouter.test.ts +++ b/packages/core/test/plugin/provider-openrouter.test.ts @@ -54,7 +54,7 @@ describe("OpenRouterPlugin", () => { const ignored = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -65,7 +65,7 @@ describe("OpenRouterPlugin", () => { const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: ProviderV2.aisdk("test-provider"), }), diff --git a/packages/core/test/plugin/provider-sap-ai-core.test.ts b/packages/core/test/plugin/provider-sap-ai-core.test.ts index 26dc3ac86cfe..09d99867f2ca 100644 --- a/packages/core/test/plugin/provider-sap-ai-core.test.ts +++ b/packages/core/test/plugin/provider-sap-ai-core.test.ts @@ -48,7 +48,7 @@ function withEnv(vars: Record, effect: () = function model(providerID: string) { return ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make(providerID), ModelV2.ID.make("sap-model")), + ...ModelV2.Info.default(ProviderV2.ID.make(providerID), ModelV2.ID.make("sap-model")), modelID: ModelV2.ID.make("sap-model"), package: ProviderV2.aisdk(fixtureProvider), }) diff --git a/packages/core/test/plugin/provider-snowflake-cortex.test.ts b/packages/core/test/plugin/provider-snowflake-cortex.test.ts index 92af6237566d..749f4c5520fe 100644 --- a/packages/core/test/plugin/provider-snowflake-cortex.test.ts +++ b/packages/core/test/plugin/provider-snowflake-cortex.test.ts @@ -58,7 +58,7 @@ describe("SnowflakeCortexPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-4")), + ...ModelV2.Info.default(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-4")), modelID: ModelV2.ID.make("gpt-4"), package: "aisdk:test-provider", }), @@ -77,7 +77,7 @@ describe("SnowflakeCortexPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), + ...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), modelID: ModelV2.ID.make("claude-sonnet-4-6"), package: "aisdk:test-provider", }), @@ -97,7 +97,7 @@ describe("SnowflakeCortexPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), + ...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), modelID: ModelV2.ID.make("claude-sonnet-4-6"), package: "aisdk:test-provider", }), @@ -121,7 +121,7 @@ describe("SnowflakeCortexPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), + ...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), modelID: ModelV2.ID.make("claude-sonnet-4-6"), package: "aisdk:test-provider", }), @@ -141,7 +141,7 @@ describe("SnowflakeCortexPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), + ...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), modelID: ModelV2.ID.make("claude-sonnet-4-6"), package: "aisdk:test-provider", }), @@ -165,7 +165,7 @@ describe("SnowflakeCortexPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), + ...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), modelID: ModelV2.ID.make("claude-sonnet-4-6"), package: "aisdk:test-provider", }), diff --git a/packages/core/test/plugin/provider-vercel.test.ts b/packages/core/test/plugin/provider-vercel.test.ts index 46d5fe25b80b..29820b714c56 100644 --- a/packages/core/test/plugin/provider-vercel.test.ts +++ b/packages/core/test/plugin/provider-vercel.test.ts @@ -59,7 +59,7 @@ describe("VercelPlugin", () => { yield* addPlugin() const event = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-vercel"), ModelV2.ID.make("v0-1.0-md")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom-vercel"), ModelV2.ID.make("v0-1.0-md")), modelID: ModelV2.ID.make("v0-1.0-md"), package: "aisdk:@ai-sdk/vercel", }), diff --git a/packages/core/test/plugin/provider-xai.test.ts b/packages/core/test/plugin/provider-xai.test.ts index 04bfc508f76c..4b3e672c1ed8 100644 --- a/packages/core/test/plugin/provider-xai.test.ts +++ b/packages/core/test/plugin/provider-xai.test.ts @@ -62,7 +62,7 @@ describe("XAIPlugin", () => { const ignored = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), + ...ModelV2.Info.default(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), modelID: ModelV2.ID.make("grok-4"), package: "aisdk:@ai-sdk/xai", }), @@ -72,7 +72,7 @@ describe("XAIPlugin", () => { const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), + ...ModelV2.Info.default(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), modelID: ModelV2.ID.make("grok-4"), package: "aisdk:@ai-sdk/xai", }), @@ -92,7 +92,7 @@ describe("XAIPlugin", () => { const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-xai"), ModelV2.ID.make("grok-4")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom-xai"), ModelV2.ID.make("grok-4")), modelID: ModelV2.ID.make("grok-4"), package: "aisdk:@ai-sdk/xai", }), @@ -112,7 +112,7 @@ describe("XAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("alias")), + ...ModelV2.Info.default(ProviderV2.ID.make("xai"), ModelV2.ID.make("alias")), modelID: ModelV2.ID.make("grok-4"), package: "aisdk:@ai-sdk/xai", }), @@ -133,7 +133,7 @@ describe("XAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("grok-4")), + ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("grok-4")), modelID: ModelV2.ID.make("grok-4"), package: "aisdk:@ai-sdk/xai", }), diff --git a/packages/core/test/shared-schema.test.ts b/packages/core/test/shared-schema.test.ts index 37b67c63a828..937cd3e1f044 100644 --- a/packages/core/test/shared-schema.test.ts +++ b/packages/core/test/shared-schema.test.ts @@ -170,8 +170,8 @@ test("Core reuses the canonical shared schemas", async () => { for (const [core, shared] of schemas) expect(core).toBe(shared) expect(Agent.Info.empty(Agent.ID.make("test"))).toEqual(AgentV2.Info.empty(AgentV2.ID.make("test"))) - expect(Model.Info.empty(Provider.ID.make("test"), Model.ID.make("model"))).toEqual( - ModelV2.Info.empty(ProviderV2.ID.make("test"), ModelV2.ID.make("model")), + expect(Model.Info.default(Provider.ID.make("test"), Model.ID.make("model"))).toEqual( + ModelV2.Info.default(ProviderV2.ID.make("test"), ModelV2.ID.make("model")), ) expect(Provider.Info.empty(Provider.ID.make("test"))).toEqual(ProviderV2.Info.empty(ProviderV2.ID.make("test"))) expect(Skill.Source.key(Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make("/tmp") }))).toBe( diff --git a/packages/docs/models.mdx b/packages/docs/models.mdx index 60fd63812adb..213bfac523e4 100644 --- a/packages/docs/models.mdx +++ b/packages/docs/models.mdx @@ -94,9 +94,10 @@ You can also map a friendly catalog ID to a different API model ID with `modelID } ``` -Here `openai/coding-default` is the selectable catalog reference, while `gpt-5.2` is sent to the provider. When adding a -model that is not already in the catalog, set accurate `capabilities` and `limit` values so OpenCode can expose tools and -enforce the correct context limits. Set `disabled: true` on a model entry to hide it from the available catalog. +Here `openai/coding-default` is the selectable catalog reference, while `gpt-5.2` is sent to the provider. A model that is +not already in the catalog defaults to tool support, text and image input, and text output. Set accurate `capabilities` +and `limit` values when those defaults do not match the model or OpenCode needs to enforce its context limits. Set +`disabled: true` on a model entry to hide it from the available catalog. OpenAI-compatible models that stream reasoning through a custom assistant-message field can set `compatibility.reasoningField`: @@ -193,8 +194,9 @@ For an OpenAI-compatible server, define a provider package, endpoint, and at lea } ``` -Use the server's real model name, limits, modalities, and tool support. OpenCode cannot infer these for a model you add -manually. If the endpoint requires a key, add `apiKey` to provider `settings` using an environment substitution such as +Use the server's real model name, limits, modalities, and tool support. OpenCode applies the custom-model capability +defaults described above but cannot infer the server's actual limits or whether those defaults are accurate. If the +endpoint requires a key, add `apiKey` to provider `settings` using an environment substitution such as `"apiKey": "{env:LOCAL_API_KEY}"`; do not commit secrets. ### Model references diff --git a/packages/schema/src/model.ts b/packages/schema/src/model.ts index 81caea654fc8..2f9d1dd7cc99 100644 --- a/packages/schema/src/model.ts +++ b/packages/schema/src/model.ts @@ -106,13 +106,13 @@ export const Info = Schema.Struct({ .annotate({ identifier: "Model.Info" }) .pipe( statics(() => ({ - empty: (providerID: Provider.ID, id: ID) => + default: (providerID: Provider.ID, id: ID) => ({ id, modelID: id, providerID, name: id, - capabilities: { tools: false, input: [], output: [] }, + capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, variants: [], time: { released: 0 }, cost: [], diff --git a/packages/schema/test/contract-hygiene.test.ts b/packages/schema/test/contract-hygiene.test.ts index c66287026902..46784ee27d56 100644 --- a/packages/schema/test/contract-hygiene.test.ts +++ b/packages/schema/test/contract-hygiene.test.ts @@ -83,7 +83,7 @@ describe("contract hygiene", () => { test("model defaults and provider overlays preserve public invariants", () => { const id = Model.ID.make("model") - expect(Model.Info.empty(Provider.ID.make("provider"), id)).toMatchObject({ modelID: id, variants: [] }) + expect(Model.Info.default(Provider.ID.make("provider"), id)).toMatchObject({ modelID: id, variants: [] }) expect(() => Schema.decodeUnknownSync(Provider.Info)({ id: "provider", From 52c98a4eeb927eefc07652abdd79eab1e1269e8f Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Thu, 23 Jul 2026 12:12:25 +0200 Subject: [PATCH 048/150] mini: add replay settings to cli config (#38487) --- packages/cli/src/commands/commands.ts | 6 +++--- packages/cli/src/commands/handlers/mini.ts | 4 ++-- packages/cli/test/mini.test.ts | 6 +++++- packages/tui/src/config/index.tsx | 6 ++++++ packages/tui/test/config-v2.test.tsx | 12 ++++++++++++ 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/commands/commands.ts b/packages/cli/src/commands/commands.ts index c0b914fc62b8..a3ae15113fa4 100644 --- a/packages/cli/src/commands/commands.ts +++ b/packages/cli/src/commands/commands.ts @@ -140,11 +140,11 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO Flag.withDefault(false), ), replay: Flag.boolean("replay").pipe( - Flag.withDescription("Replay session history on resume and after resize"), - Flag.withDefault(true), + Flag.withDescription("Restore session history on resume and resize (disable with --no-replay)"), + Flag.optional, ), replayLimit: Flag.integer("replay-limit").pipe( - Flag.withDescription("Cap visible replay to the newest N messages"), + Flag.withDescription("Limit replay to the newest N messages (default: 200)"), Flag.optional, ), model: Flag.string("model").pipe( diff --git a/packages/cli/src/commands/handlers/mini.ts b/packages/cli/src/commands/handlers/mini.ts index 4f8a16a35015..972c3bf962aa 100644 --- a/packages/cli/src/commands/handlers/mini.ts +++ b/packages/cli/src/commands/handlers/mini.ts @@ -28,8 +28,8 @@ export default Runtime.handler(Commands.commands.mini, (input) => model: Option.getOrUndefined(input.model), agent: Option.getOrUndefined(input.agent), prompt: Option.getOrUndefined(input.prompt), - replay: input.replay, - replayLimit: Option.getOrUndefined(input.replayLimit), + replay: Option.getOrUndefined(input.replay) ?? resolved.mini?.replay ?? true, + replayLimit: Option.getOrUndefined(input.replayLimit) ?? resolved.mini?.replay_limit, demo: input.demo, tuiConfig: resolved, config: { diff --git a/packages/cli/test/mini.test.ts b/packages/cli/test/mini.test.ts index 34cf2a2cd275..e9304d194fa8 100644 --- a/packages/cli/test/mini.test.ts +++ b/packages/cli/test/mini.test.ts @@ -214,11 +214,15 @@ describe("mini command", () => { expect(result.exitCode).toBe(0) expect(result.stdout).toContain("--server string") expect(result.stdout).toContain("--prompt string") + expect(result.stdout).toContain("--replay") + expect(result.stdout).toContain("disable with --no-replay") + expect(result.stdout).toContain("--replay-limit integer") + expect(result.stdout).toContain("Limit replay to the newest N messages (default: 200)") expect(result.stdout).not.toContain("SUBCOMMANDS") }) test("routes local and explicit-server invocations into mini", async () => { - for (const args of [["mini"], ["mini", "--server", "http://127.0.0.1:1"]]) { + for (const args of [["mini"], ["mini", "--no-replay"], ["mini", "--server", "http://127.0.0.1:1"]]) { const result = await cli(args) expect(result.exitCode).toBe(1) diff --git a/packages/tui/src/config/index.tsx b/packages/tui/src/config/index.tsx index 3d668183900f..e078eb5113ac 100644 --- a/packages/tui/src/config/index.tsx +++ b/packages/tui/src/config/index.tsx @@ -142,6 +142,12 @@ export const Info = Schema.Struct({ mono: Schema.optional(Schema.Boolean).annotate({ description: "Use monochrome ASCII output", }), + replay: Schema.optional(Schema.Boolean).annotate({ + description: "Restore session history on resume and terminal resize", + }), + replay_limit: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))).annotate({ + description: "Maximum number of newest messages restored during replay", + }), }), ).annotate({ description: "Mini transcript presentation settings" }), hints: Schema.optional( diff --git a/packages/tui/test/config-v2.test.tsx b/packages/tui/test/config-v2.test.tsx index 7a663cc11793..c349ea522430 100644 --- a/packages/tui/test/config-v2.test.tsx +++ b/packages/tui/test/config-v2.test.tsx @@ -1,13 +1,25 @@ /** @jsxImportSource @opentui/solid */ import { testRender } from "@opentui/solid" import { expect, test } from "bun:test" +import { Schema } from "effect" import { resolve, ConfigProvider, + Info, useConfig, type Interface, } from "../src/config" +test("validates mini replay settings", () => { + const decode = Schema.decodeUnknownSync(Info) + + expect(decode({ mini: { replay: false, replay_limit: 50 } })).toEqual({ + mini: { replay: false, replay_limit: 50 }, + }) + expect(() => decode({ mini: { replay_limit: 0 } })).toThrow() + expect(() => decode({ mini: { replay_limit: 1.5 } })).toThrow() +}) + test("resolves nested config and keybind defaults", () => { const config = resolve( { From 5b1321a8ca81bcedc19bfb7782472c35c5d77d38 Mon Sep 17 00:00:00 2001 From: James Long Date: Thu, 23 Jul 2026 10:00:51 -0400 Subject: [PATCH 049/150] feat(tui): add turn token usage diagnostics (#38398) --- packages/tui/src/component/devtools-bar.tsx | 13 ++- packages/tui/src/component/dialog-config.tsx | 8 -- packages/tui/src/config/index.tsx | 1 + packages/tui/src/routes/session/index.tsx | 114 ++++++++++++++++++- packages/tui/src/routes/session/rows.ts | 51 ++++++++- 5 files changed, 175 insertions(+), 12 deletions(-) diff --git a/packages/tui/src/component/devtools-bar.tsx b/packages/tui/src/component/devtools-bar.tsx index fc168260a203..03373110e226 100644 --- a/packages/tui/src/component/devtools-bar.tsx +++ b/packages/tui/src/component/devtools-bar.tsx @@ -59,6 +59,7 @@ export function DevToolsBar() { const canSwitchMode = () => supports(nextMode()) const runtime = createMemo(() => runtimeStatus(frontendSamples())) const timing = () => config.data.debug?.timing ?? false + const turnTokens = () => config.data.debug?.turn_tokens ?? false const offEscape = keymap.intercept( "key", @@ -352,6 +353,16 @@ export function DevToolsBar() { > {timing() ? "[x]" : "[ ]"} Time to first draw + + void config.update((draft) => { + draft.debug = { ...draft.debug, turn_tokens: !turnTokens() } + }) + } + hoverBackground + > + {turnTokens() ? "[x]" : "[ ]"} Turn token usage + {(group) => ( @@ -403,7 +414,7 @@ function PanelBox(props: ParentProps) { position="absolute" zIndex={2600} bottom={1} - left={0} + left={-1} width={42} paddingLeft={2} paddingRight={2} diff --git a/packages/tui/src/component/dialog-config.tsx b/packages/tui/src/component/dialog-config.tsx index ab84fdf6233f..8ff9db50c700 100644 --- a/packages/tui/src/component/dialog-config.tsx +++ b/packages/tui/src/component/dialog-config.tsx @@ -222,14 +222,6 @@ const settings: Setting[] = [ values: [false, true], labels: ["off", "on"], }, - { - title: "DevTools: Timing", - category: "Debug", - path: ["debug", "timing"], - default: true, - values: [false, true], - labels: ["off", "on"], - }, ] export function DialogConfig() { diff --git a/packages/tui/src/config/index.tsx b/packages/tui/src/config/index.tsx index e078eb5113ac..424e8dbc9a8f 100644 --- a/packages/tui/src/config/index.tsx +++ b/packages/tui/src/config/index.tsx @@ -159,6 +159,7 @@ export const Info = Schema.Struct({ Schema.Struct({ devtools: Schema.optional(Schema.Boolean).annotate({ description: "Show the DevTools debug bar" }), timing: Schema.optional(Schema.Boolean).annotate({ description: "Show time-to-first-draw diagnostics" }), + turn_tokens: Schema.optional(Schema.Boolean).annotate({ description: "Show per-turn token usage diagnostics" }), }), ).annotate({ description: "Debugging settings" }), animations: Schema.optional(Schema.Boolean).annotate({ description: "Enable interface animations" }), diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 69c9682ec158..e07947ba1c40 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -1029,11 +1029,23 @@ export function Session() { ) } -function SessionRowView(props: { +type SessionRowViewProps = { row: SessionRow message: (messageID: string) => SessionMessageInfo | undefined boundaryID?: string -}) { +} + +function SessionRowView(props: SessionRowViewProps) { + const config = useConfig() + const hidden = () => props.row.type === "turn-usage" && config.data.debug?.turn_tokens !== true + return ( + + + + ) +} + +function SessionRowContent(props: SessionRowViewProps) { return ( @@ -1072,11 +1084,109 @@ function SessionRowView(props: { )} + + {(row) => ( + + )} + ) } +function TurnTokenUsage(props: { + messageIDs: string[] + previousCacheRead?: number + message: (messageID: string) => SessionMessageInfo | undefined +}) { + const config = useConfig() + const { themeV2 } = useTheme() + const steps = createMemo(() => { + let previousCacheRead = props.previousCacheRead + return props.messageIDs.flatMap((messageID) => { + const message = props.message(messageID) + if (message?.type !== "assistant" || !message.tokens) return [] + const total = + message.tokens.input + + message.tokens.output + + message.tokens.reasoning + + message.tokens.cache.read + + message.tokens.cache.write + if (total === 0) return [] + const newTokens = total - message.tokens.cache.read + const cacheBust = + previousCacheRead !== undefined && message.tokens.cache.read < previousCacheRead + ? previousCacheRead - message.tokens.cache.read + : undefined + previousCacheRead = message.tokens.cache.read + return [ + { + finish: message.finish === "tool-calls" ? "tool-call" : (message.finish ?? "unknown"), + newTokens, + cached: message.tokens.cache.read, + total, + cacheBust, + }, + ] + }) + }) + const columns = createMemo(() => ({ + step: Math.max("Step".length, ...steps().map((item) => item.finish.length)), + newTokens: Math.max("New".length, ...steps().map((item) => item.newTokens.toLocaleString().length)), + cached: Math.max("Cached".length, ...steps().map((item) => item.cached.toLocaleString().length)), + total: Math.max("Total".length, ...steps().map((item) => item.total.toLocaleString().length)), + })) + return ( + 0}> + + + + ◈ + + + Tokens + + + + + {"Step".padEnd(columns().step + 2)} + {"New".padStart(columns().newTokens)} + {" "} + {"Cached".padStart(columns().cached)} + {" "} + {"Total".padStart(columns().total)} + + + + {(item) => ( + + + {item.finish.padEnd(columns().step + 2)} + + {item.newTokens.toLocaleString().padStart(columns().newTokens)} + + {" "} + {item.cached.toLocaleString().padStart(columns().cached)} + {" "} + {item.total.toLocaleString().padStart(columns().total)} + + + + ! Cache bust: {item.cacheBust?.toLocaleString()} fewer cached tokens than the previous step + + + + )} + + + + ) +} + function BackgroundToolHint(props: { messages: SessionMessageInfo[] }) { const { themeV2 } = useTheme() const shortcut = Keymap.useShortcut("session.background") diff --git a/packages/tui/src/routes/session/rows.ts b/packages/tui/src/routes/session/rows.ts index dc4a681bb9d1..837725faf82c 100644 --- a/packages/tui/src/routes/session/rows.ts +++ b/packages/tui/src/routes/session/rows.ts @@ -27,6 +27,7 @@ export type SessionRow = completed: boolean } | { type: "assistant-footer"; messageID: string } + | { type: "turn-usage"; messageIDs: string[]; previousCacheRead?: number } export function createSessionRows(sessionID: Accessor) { const data = useData() @@ -127,6 +128,26 @@ export function createSessionRows(sessionID: Accessor) { ), ) + createEffect( + on( + () => + data.session.message.list(sessionID()).flatMap((message) => + message.type === "assistant" + ? [ + { + id: message.id, + finish: message.finish, + error: message.error, + retry: message.retry, + tokens: message.tokens, + }, + ] + : [], + ), + () => setRows(reconcile(reduce())), + ), + ) + const appendMessage = (messageID: string) => setRows( produce((draft) => { @@ -260,6 +281,10 @@ export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new S const isInput = (message: SessionMessageInfo) => inputs.has(message.id) const pendingCompactions = messages.filter((message) => message.type === "compaction" && message.status === "running") const pending = new Set([...pendingCompactions.map((message) => message.id), ...inputs]) + const steps: string[] = [] + let previousCacheRead: number | undefined + let turnPreviousCacheRead: number | undefined + let measured = false return [ ...messages.filter((message) => !pending.has(message.id)), ...pendingCompactions, @@ -271,20 +296,42 @@ export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new S rows.push({ type: "message", messageID: message.id }) return rows } + if (steps.length === 0) turnPreviousCacheRead = previousCacheRead + steps.push(message.id) + if (message.tokens && tokenTotal(message.tokens) > 0) { + previousCacheRead = message.tokens.cache.read + measured = true + } const ordinals = { text: 0, reasoning: 0 } message.content.forEach((part) => { const partID = part.type === "tool" ? part.id : `${part.type}:${ordinals[part.type]++}` if ((part.type === "text" || part.type === "reasoning") && !part.text.trim()) return append(rows, { messageID: message.id, partID }, part) }) - if ((message.finish && !["tool-calls", "unknown"].includes(message.finish)) || message.error || message.retry) { + const terminal = (message.finish && !["tool-calls", "unknown"].includes(message.finish)) || message.error + if (terminal || message.retry) { completePrevious(rows) rows.push({ type: "assistant-footer", messageID: message.id }) } + if (terminal) { + if (measured) + rows.push({ + type: "turn-usage", + messageIDs: [...steps], + ...(turnPreviousCacheRead === undefined ? {} : { previousCacheRead: turnPreviousCacheRead }), + }) + steps.length = 0 + turnPreviousCacheRead = undefined + measured = false + } return rows }, []) } +function tokenTotal(tokens: NonNullable) { + return tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write +} + export function messageBoundaryIDs(rows: SessionRow[], messages: SessionMessageInfo[]) { const byID = new Map(messages.map((message) => [message.id, message])) const seen = new Set() @@ -309,6 +356,8 @@ function rowBoundaryMessageID(row: SessionRow, messages: Map Date: Thu, 23 Jul 2026 20:44:14 +0530 Subject: [PATCH 050/150] chore(cli): upgrade acp sdk (#38316) --- bun.lock | 4 ++-- packages/cli/package.json | 2 +- packages/cli/src/acp/agent.ts | 2 -- packages/cli/src/acp/event.ts | 7 ++----- packages/cli/src/acp/service.ts | 12 +----------- packages/cli/test/acp/event-behavior.test.ts | 1 - packages/cli/test/acp/event.test.ts | 4 +--- packages/cli/test/acp/service-directory.test.ts | 4 +--- packages/cli/test/acp/service-usage.test.ts | 5 ----- 9 files changed, 8 insertions(+), 33 deletions(-) diff --git a/bun.lock b/bun.lock index 3ea6f31f5dd8..72db1b9e9285 100644 --- a/bun.lock +++ b/bun.lock @@ -124,7 +124,7 @@ "opencode2": "./bin/opencode2.cjs", }, "dependencies": { - "@agentclientprotocol/sdk": "0.21.0", + "@agentclientprotocol/sdk": "1.2.1", "@effect/platform-node": "catalog:", "@opencode-ai/client": "workspace:*", "@opencode-ai/plugin": "workspace:*", @@ -1173,7 +1173,7 @@ "@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="], - "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.21.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-ONj+Q8qOdNQp5XbH5jnMwzT9IKZJsSN0p0lkceS4GtUtNOPVLpNzSS8gqQdGMKfBvA0ESbkL8BTaSN1Rc9miEw=="], + "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@1.2.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-jwYUdOQR7tc+Zfch53VL4JJyUNK/46q03uUTYb+PjECsmnNl94XFXOfYLJ8RBpMNidXd1rpOAVgb0vqD98xImA=="], "@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="], diff --git a/packages/cli/package.json b/packages/cli/package.json index acf8ec75dd4d..f848b562798b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -22,7 +22,7 @@ "typecheck": "tsgo --noEmit" }, "dependencies": { - "@agentclientprotocol/sdk": "0.21.0", + "@agentclientprotocol/sdk": "1.2.1", "@effect/platform-node": "catalog:", "@opencode-ai/client": "workspace:*", "@opencode-ai/plugin": "workspace:*", diff --git a/packages/cli/src/acp/agent.ts b/packages/cli/src/acp/agent.ts index cf8eb693f738..89ec88e8ca5f 100644 --- a/packages/cli/src/acp/agent.ts +++ b/packages/cli/src/acp/agent.ts @@ -13,7 +13,6 @@ import { type PromptRequest, type ResumeSessionRequest, type SetSessionConfigOptionRequest, - type SetSessionModelRequest, type SetSessionModeRequest, } from "@agentclientprotocol/sdk" import type { OpenCodeClient } from "@opencode-ai/client/promise" @@ -33,7 +32,6 @@ export function create(client: OpenCodeClient, connection: AgentSideConnection) unstable_forkSession: (params: ForkSessionRequest) => run(service.forkSession(params)), setSessionConfigOption: (params: SetSessionConfigOptionRequest) => run(service.setSessionConfigOption(params)), setSessionMode: (params: SetSessionModeRequest) => run(service.setSessionMode(params)), - unstable_setSessionModel: (params: SetSessionModelRequest) => run(service.setSessionModel(params)), prompt: (params: PromptRequest) => run(service.prompt(params)), cancel: (params: CancelNotification) => run(service.cancel(params)), } satisfies Agent diff --git a/packages/cli/src/acp/event.ts b/packages/cli/src/acp/event.ts index da8bfe168bf4..3b2d7bc23cd9 100644 --- a/packages/cli/src/acp/event.ts +++ b/packages/cli/src/acp/event.ts @@ -47,7 +47,6 @@ export async function streamTurn(input: { readonly sessionID: string readonly cwd: string readonly start: TurnStart - readonly userMessageID?: string | null readonly submit: (signal: AbortSignal) => Promise readonly control: TurnControl }): Promise { @@ -231,7 +230,7 @@ export async function streamTurn(input: { if (!started) { streamController.abort() await completed.catch(() => {}) - return response(undefined, undefined, "interrupted", true, undefined, input.userMessageID) + return response(undefined, undefined, "interrupted", true, undefined) } } const terminal = await completed @@ -246,7 +245,6 @@ export async function streamTurn(input: { terminal, control.cancelled, finish, - input.userMessageID, ) } catch (error) { streamController.abort() @@ -400,7 +398,6 @@ function response( terminal: "succeeded" | "failed" | "interrupted", cancelled: boolean, finish: SessionMessageAssistant["finish"], - messageID: string | null | undefined, ): PromptResponse { const error = assistant?.error ?? executionError if (error?.type === "provider.auth") throw new ACPError.AuthRequiredError() @@ -423,7 +420,7 @@ function response( } : undefined const stopReason = resolveStopReason({ terminal, cancelled, finish, error: error?.type }) - return { stopReason, ...(usage ? { usage } : {}), ...(messageID ? { userMessageId: messageID } : {}), _meta: {} } + return { stopReason, ...(usage ? { usage } : {}), _meta: {} } } function resolveStopReason(input: { diff --git a/packages/cli/src/acp/service.ts b/packages/cli/src/acp/service.ts index f20f31c9bcae..d9bf0ddc5b64 100644 --- a/packages/cli/src/acp/service.ts +++ b/packages/cli/src/acp/service.ts @@ -33,8 +33,6 @@ import type { ResumeSessionResponse, SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, - SetSessionModelRequest, - SetSessionModelResponse, SetSessionModeRequest, SetSessionModeResponse, } from "@agentclientprotocol/sdk" @@ -88,7 +86,6 @@ export interface Interface { forkSession(input: ForkSessionRequest): Promise setSessionConfigOption(input: SetSessionConfigOptionRequest): Promise setSessionMode(input: SetSessionModeRequest): Promise - setSessionModel(input: SetSessionModelRequest): Promise prompt(input: PromptRequest): Promise cancel(input: CancelNotification): Promise } @@ -270,13 +267,6 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti await selectMode(input.client, await requireSession(params.sessionId), params.modeId) return {} }, - setSessionModel: async (params) => { - const state = await requireSession(params.sessionId) - const selected = requireModel(state.catalog, params.modelId) - state.model = selected - await input.client.session.switchModel({ sessionID: state.id, model: selected }) - return {} - }, prompt: async (params) => { const state = await requireSession(params.sessionId) if (active.has(state.id)) { @@ -295,7 +285,6 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti sessionID: state.id, cwd: state.cwd, start: prepared.start, - userMessageID: params.messageId, control, submit: (signal) => submitPrompt(input.client, state, prepared, signal), }).finally(() => { @@ -479,6 +468,7 @@ async function registerMcpServers( function mcpConfig(server: McpServer) { if ("type" in server) { + if (server.type === "acp") throw new Error("MCP-over-ACP is not supported") return { type: "remote" as const, url: server.url, diff --git a/packages/cli/test/acp/event-behavior.test.ts b/packages/cli/test/acp/event-behavior.test.ts index 1f9935f5c8e3..46ecc552a44b 100644 --- a/packages/cli/test/acp/event-behavior.test.ts +++ b/packages/cli/test/acp/event-behavior.test.ts @@ -566,7 +566,6 @@ function turn(input: { sessionID: input.sessionID, cwd: "/workspace", start: { type: "input", id: input.inputID }, - userMessageID: `client_${input.inputID}`, control: { cancelled: false, admission: new AbortController() }, submit: (signal) => input.fixture.client.session.prompt({ sessionID: input.sessionID, id: input.inputID, text: "hello" }, { signal }), diff --git a/packages/cli/test/acp/event.test.ts b/packages/cli/test/acp/event.test.ts index e6d8db9deb9d..0075f5c913b0 100644 --- a/packages/cli/test/acp/event.test.ts +++ b/packages/cli/test/acp/event.test.ts @@ -85,7 +85,6 @@ test("acp prompt resolves after ordered turn updates", async () => { try { const id = "msg_prompt" - const userMessageID = "client-message" const response = await streamTurn({ client, connection: { @@ -97,7 +96,6 @@ test("acp prompt resolves after ordered turn updates", async () => { sessionID: "ses_test", cwd: "/workspace", start: { type: "input", id }, - userMessageID, control: { cancelled: false, admission: new AbortController() }, submit: () => client.session.prompt({ sessionID: "ses_test", id, text: "hi" }), }) @@ -112,7 +110,7 @@ test("acp prompt resolves after ordered turn updates", async () => { }, }, ]) - expect(response).toMatchObject({ stopReason: "end_turn", userMessageId: userMessageID, usage: { totalTokens: 2 } }) + expect(response).toMatchObject({ stopReason: "end_turn", usage: { totalTokens: 2 } }) } finally { events?.close() await server.stop(true) diff --git a/packages/cli/test/acp/service-directory.test.ts b/packages/cli/test/acp/service-directory.test.ts index 96906223721f..a3671753c921 100644 --- a/packages/cli/test/acp/service-directory.test.ts +++ b/packages/cli/test/acp/service-directory.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import type { McpServer, SessionConfigOption } from "@agentclientprotocol/sdk" -import { makeACPFixture, makeSession, secondModel, testModel } from "./service-fixture" +import { makeACPFixture, makeSession, secondModel } from "./service-fixture" describe("acp service directory behavior", () => { test("creates sessions from a catalog shared by concurrent callers in the same cwd", async () => { @@ -134,7 +134,6 @@ describe("acp service directory behavior", () => { configId: "mode", value: "plan", }) - await fixture.service.setSessionModel({ sessionId: session.sessionId, modelId: "test/test-model/high" }) await fixture.service.setSessionMode({ sessionId: session.sessionId, modeId: "build" }) expect(currentValue(selectedModel, "model")).toBe("test/second-model") @@ -148,7 +147,6 @@ describe("acp service directory behavior", () => { ).toEqual([ { model: { providerID: "test", id: secondModel.id } }, { model: { providerID: "test", id: secondModel.id, variant: "medium" } }, - { model: { providerID: "test", id: testModel.id, variant: "high" } }, ]) expect( fixture.requests diff --git a/packages/cli/test/acp/service-usage.test.ts b/packages/cli/test/acp/service-usage.test.ts index 1ad9eee7b77f..e7abb74073ca 100644 --- a/packages/cli/test/acp/service-usage.test.ts +++ b/packages/cli/test/acp/service-usage.test.ts @@ -45,17 +45,14 @@ describe("acp service prompt routing and usage", () => { const commandResult = await fixture.service.prompt({ sessionId: session.sessionId, - messageId: "client-command", prompt: [{ type: "text", text: "/review now" }], }) const skillResult = await fixture.service.prompt({ sessionId: session.sessionId, - messageId: "client-skill", prompt: [{ type: "text", text: "/verify" }], }) const compactResult = await fixture.service.prompt({ sessionId: session.sessionId, - messageId: "client-compact", prompt: [{ type: "text", text: "/compact" }], }) @@ -154,13 +151,11 @@ describe("acp service prompt routing and usage", () => { const response = await fixture.service.prompt({ sessionId: session.sessionId, - messageId: "client-message", prompt: [{ type: "text", text: "hello" }], }) expect(response).toEqual({ stopReason: "end_turn", - userMessageId: "client-message", usage: { inputTokens: 100, outputTokens: 40, From 833dd2ed7f9dc9845528997c378a1fb50bc423df Mon Sep 17 00:00:00 2001 From: James Long Date: Thu, 23 Jul 2026 11:22:00 -0400 Subject: [PATCH 051/150] refactor(tui): simplify turn usage reduction (#38514) --- packages/tui/src/routes/session/index.tsx | 10 --- packages/tui/src/routes/session/rows.ts | 76 ++++++++++++----------- packages/tui/test/cli/tui/data.test.tsx | 18 ++++-- 3 files changed, 51 insertions(+), 53 deletions(-) diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index e07947ba1c40..9b03a404225a 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -1036,16 +1036,6 @@ type SessionRowViewProps = { } function SessionRowView(props: SessionRowViewProps) { - const config = useConfig() - const hidden = () => props.row.type === "turn-usage" && config.data.debug?.turn_tokens !== true - return ( - - - - ) -} - -function SessionRowContent(props: SessionRowViewProps) { return ( diff --git a/packages/tui/src/routes/session/rows.ts b/packages/tui/src/routes/session/rows.ts index 837725faf82c..c6b0d744c91d 100644 --- a/packages/tui/src/routes/session/rows.ts +++ b/packages/tui/src/routes/session/rows.ts @@ -1,6 +1,7 @@ import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client" import { createEffect, on, onCleanup, type Accessor } from "solid-js" import { createStore, produce, reconcile } from "solid-js/store" +import { useConfig } from "../../config" import { useData } from "../../context/data" import { useClient } from "../../context/client" @@ -32,14 +33,20 @@ export type SessionRow = export function createSessionRows(sessionID: Accessor) { const data = useData() const client = useClient() + const config = useConfig() const [rows, setRows] = createStore([]) const revertBoundary = () => data.session.get(sessionID())?.revert?.messageID + const turnTokens = () => config.data.debug?.turn_tokens === true function reduce() { const messages = data.session.message.list(sessionID()) const inputs = new Set(data.session.input.list(sessionID())) const boundary = revertBoundary() - const rows = reduceSessionRows(boundary ? messages.filter((message) => message.id < boundary) : messages, inputs) + const rows = reduceSessionRows( + boundary ? messages.filter((message) => message.id < boundary) : messages, + inputs, + turnTokens(), + ) partitionPending(rows, pendingPermissions()) const position = rows.findIndex((row) => row.type === "message" && inputs.has(row.messageID)) rows.splice( @@ -129,23 +136,7 @@ export function createSessionRows(sessionID: Accessor) { ) createEffect( - on( - () => - data.session.message.list(sessionID()).flatMap((message) => - message.type === "assistant" - ? [ - { - id: message.id, - finish: message.finish, - error: message.error, - retry: message.retry, - tokens: message.tokens, - }, - ] - : [], - ), - () => setRows(reconcile(reduce())), - ), + on(turnTokens, () => setRows(reconcile(reduce()))), ) const appendMessage = (messageID: string) => @@ -267,9 +258,12 @@ export function createSessionRows(sessionID: Accessor) { data.on("session.step.ended", (event) => { if (event.data.sessionID !== sessionID() || ["tool-calls", "unknown"].includes(event.data.finish)) return appendFooter(event.data.assistantMessageID) + if (turnTokens()) setRows(reconcile(reduce())) }), data.on("session.step.failed", (event) => { - if (event.data.sessionID === sessionID()) appendFooter(event.data.assistantMessageID) + if (event.data.sessionID !== sessionID()) return + appendFooter(event.data.assistantMessageID) + if (turnTokens()) setRows(reconcile(reduce())) }), ] onCleanup(() => subscriptions.forEach((unsubscribe) => unsubscribe())) @@ -277,14 +271,17 @@ export function createSessionRows(sessionID: Accessor) { return rows } -export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new Set()) { +export function reduceSessionRows( + messages: SessionMessageInfo[], + inputs = new Set(), + turnTokens = false, +) { const isInput = (message: SessionMessageInfo) => inputs.has(message.id) const pendingCompactions = messages.filter((message) => message.type === "compaction" && message.status === "running") const pending = new Set([...pendingCompactions.map((message) => message.id), ...inputs]) - const steps: string[] = [] - let previousCacheRead: number | undefined - let turnPreviousCacheRead: number | undefined - let measured = false + const usage = turnTokens + ? { steps: [] as SessionMessageAssistant[], previousTurnCacheRead: undefined as number | undefined } + : undefined return [ ...messages.filter((message) => !pending.has(message.id)), ...pendingCompactions, @@ -296,12 +293,7 @@ export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new S rows.push({ type: "message", messageID: message.id }) return rows } - if (steps.length === 0) turnPreviousCacheRead = previousCacheRead - steps.push(message.id) - if (message.tokens && tokenTotal(message.tokens) > 0) { - previousCacheRead = message.tokens.cache.read - measured = true - } + usage?.steps.push(message) const ordinals = { text: 0, reasoning: 0 } message.content.forEach((part) => { const partID = part.type === "tool" ? part.id : `${part.type}:${ordinals[part.type]++}` @@ -313,21 +305,31 @@ export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new S completePrevious(rows) rows.push({ type: "assistant-footer", messageID: message.id }) } - if (terminal) { - if (measured) + if (terminal && usage) { + const stepsWithUsage = usage.steps.filter(hasTokenUsage) + const last = stepsWithUsage.at(-1) + if (last) { rows.push({ type: "turn-usage", - messageIDs: [...steps], - ...(turnPreviousCacheRead === undefined ? {} : { previousCacheRead: turnPreviousCacheRead }), + messageIDs: stepsWithUsage.map((step) => step.id), + ...(usage.previousTurnCacheRead === undefined + ? {} + : { previousCacheRead: usage.previousTurnCacheRead }), }) - steps.length = 0 - turnPreviousCacheRead = undefined - measured = false + usage.previousTurnCacheRead = last.tokens.cache.read + } + usage.steps.length = 0 } return rows }, []) } +function hasTokenUsage( + message: SessionMessageAssistant, +): message is SessionMessageAssistant & { tokens: NonNullable } { + return message.tokens !== undefined && tokenTotal(message.tokens) > 0 +} + function tokenTotal(tokens: NonNullable) { return tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write } diff --git a/packages/tui/test/cli/tui/data.test.tsx b/packages/tui/test/cli/tui/data.test.tsx index 88b2175f261c..e7e6146f4b5d 100644 --- a/packages/tui/test/cli/tui/data.test.tsx +++ b/packages/tui/test/cli/tui/data.test.tsx @@ -5,12 +5,14 @@ import type { OpenCodeEvent } from "@opencode-ai/client" import { SessionMessage } from "@opencode-ai/core/session/message" import { EventV2 } from "@opencode-ai/core/event" import { createEffect, onMount, type ParentProps } from "solid-js" +import { ConfigProvider } from "../../../src/config" import { ClientProvider, useClient } from "../../../src/context/client" import { DataProvider as DataProviderBase, useData } from "../../../src/context/data" import { LocationProvider, useLocation } from "../../../src/context/location" import { createSessionRows, type SessionRow } from "../../../src/routes/session/rows" import { createApi, createEventStream, createFetch, directory, json } from "../../fixture/tui-client" import { TestTuiContexts } from "../../fixture/tui-environment" +import { createTuiResolvedConfig } from "../../fixture/tui-runtime" const formFields = [{ key: "authorization", type: "external", url: "https://example.com" }] satisfies [ { @@ -32,14 +34,18 @@ function emitEvent(events: ReturnType, event: OpenCode events.emit({ ...event, location: { directory } }) } +const config = createTuiResolvedConfig() + function DataProvider(props: ParentProps) { return ( - - - - {props.children} - - + + + + + {props.children} + + + ) } From 466b75b19d8deea761593c207d398f618ec710da Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Thu, 23 Jul 2026 21:13:46 +0530 Subject: [PATCH 052/150] feat(cli): expand acp v1 support (#38325) --- packages/cli/src/acp/agent.ts | 2 ++ packages/cli/src/acp/event.ts | 2 ++ packages/cli/src/acp/permission.ts | 3 ++- packages/cli/src/acp/service.ts | 19 +++++++++++-- packages/cli/test/acp/event-behavior.test.ts | 3 +++ packages/cli/test/acp/event.test.ts | 1 + .../acp/initialize-auth.subprocess.test.ts | 1 + .../cli/test/acp/lifecycle.subprocess.test.ts | 15 +++++++++++ .../cli/test/acp/permission-behavior.test.ts | 23 ++++++++++++++++ .../cli/test/acp/service-lifecycle.test.ts | 27 +++++++++++++++++++ 10 files changed, 93 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/acp/agent.ts b/packages/cli/src/acp/agent.ts index 89ec88e8ca5f..c01ec940186d 100644 --- a/packages/cli/src/acp/agent.ts +++ b/packages/cli/src/acp/agent.ts @@ -5,6 +5,7 @@ import { type AuthenticateRequest, type CancelNotification, type CloseSessionRequest, + type DeleteSessionRequest, type ForkSessionRequest, type InitializeRequest, type ListSessionsRequest, @@ -27,6 +28,7 @@ export function create(client: OpenCodeClient, connection: AgentSideConnection) newSession: (params: NewSessionRequest) => run(service.newSession(params)), loadSession: (params: LoadSessionRequest) => run(service.loadSession(params)), listSessions: (params: ListSessionsRequest) => run(service.listSessions(params)), + deleteSession: (params: DeleteSessionRequest) => run(service.deleteSession(params)), resumeSession: (params: ResumeSessionRequest) => run(service.resumeSession(params)), closeSession: (params: CloseSessionRequest) => run(service.closeSession(params)), unstable_forkSession: (params: ForkSessionRequest) => run(service.forkSession(params)), diff --git a/packages/cli/src/acp/event.ts b/packages/cli/src/acp/event.ts index 3b2d7bc23cd9..15513538300c 100644 --- a/packages/cli/src/acp/event.ts +++ b/packages/cli/src/acp/event.ts @@ -47,6 +47,7 @@ export async function streamTurn(input: { readonly sessionID: string readonly cwd: string readonly start: TurnStart + readonly writeTextFile: boolean readonly submit: (signal: AbortSignal) => Promise readonly control: TurnControl }): Promise { @@ -169,6 +170,7 @@ export async function streamTurn(input: { tools.delete(event.data.callID) await syncEditedFiles({ connection: input.connection, + writeTextFile: input.writeTextFile, sessionID: input.sessionID, cwd: input.cwd, toolName: current.name, diff --git a/packages/cli/src/acp/permission.ts b/packages/cli/src/acp/permission.ts index 71086bf90959..3c1d92957e42 100644 --- a/packages/cli/src/acp/permission.ts +++ b/packages/cli/src/acp/permission.ts @@ -53,13 +53,14 @@ export async function replyPermission(input: { export async function syncEditedFiles(input: { readonly connection: Partial> + readonly writeTextFile: boolean readonly sessionID: string readonly cwd: string readonly toolName: string readonly toolInput: ToolInput readonly structured: Readonly> }) { - if (!input.connection.writeTextFile || toToolKind(input.toolName) !== "edit") return + if (!input.writeTextFile || !input.connection.writeTextFile || toToolKind(input.toolName) !== "edit") return const files = Array.isArray(input.structured.files) ? input.structured.files.flatMap((file): string[] => { if (!file || typeof file !== "object") return [] diff --git a/packages/cli/src/acp/service.ts b/packages/cli/src/acp/service.ts index d9bf0ddc5b64..963b5c3ed47e 100644 --- a/packages/cli/src/acp/service.ts +++ b/packages/cli/src/acp/service.ts @@ -16,6 +16,8 @@ import type { CancelNotification, CloseSessionRequest, CloseSessionResponse, + DeleteSessionRequest, + DeleteSessionResponse, ForkSessionRequest, ForkSessionResponse, InitializeRequest, @@ -45,7 +47,8 @@ import { ACPError } from "./error" export const AuthMethodID = "opencode-login" -type Connection = Pick +type Connection = Pick & + Partial> type Catalog = { readonly providers: ConfigOptionProvider[] @@ -81,6 +84,7 @@ export interface Interface { newSession(input: NewSessionRequest): Promise loadSession(input: LoadSessionRequest): Promise listSessions(input: ListSessionsRequest): Promise + deleteSession(input: DeleteSessionRequest): Promise resumeSession(input: ResumeSessionRequest): Promise closeSession(input: CloseSessionRequest): Promise forkSession(input: ForkSessionRequest): Promise @@ -95,6 +99,7 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti const catalogs = new Map>() const registeredMcp = new Map>() const active = new Map() + const capabilities = { writeTextFile: false } const catalog = (cwd: string) => { const cached = catalogs.get(cwd) @@ -154,6 +159,7 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti return { initialize: async (params) => { + capabilities.writeTextFile = params.clientCapabilities?.fs?.writeTextFile === true const authMethod: AuthMethod = { description: "Run `opencode auth login` in the terminal", name: "Login with opencode", @@ -170,7 +176,7 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti loadSession: true, mcpCapabilities: { http: true, sse: false }, promptCapabilities: { embeddedContext: true, image: true }, - sessionCapabilities: { close: {}, fork: {}, list: {}, resume: {} }, + sessionCapabilities: { close: {}, delete: {}, fork: {}, list: {}, resume: {} }, }, authMethods: [authMethod], agentInfo: { name: "OpenCode", version: OPENCODE_VERSION }, @@ -213,6 +219,14 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti ...(page.cursor.next ? { nextCursor: page.cursor.next } : {}), } }, + deleteSession: async (params) => { + await input.client.session.remove({ sessionID: params.sessionId }).catch((error) => { + if (!isSessionNotFoundError(error)) throw error + }) + sessions.delete(params.sessionId) + registeredMcp.delete(params.sessionId) + return {} + }, resumeSession: async (params) => { const session = await getSession(input.client, params.sessionId) const state = await attach(session, session.location.directory, params.mcpServers ?? []) @@ -285,6 +299,7 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti sessionID: state.id, cwd: state.cwd, start: prepared.start, + writeTextFile: capabilities.writeTextFile, control, submit: (signal) => submitPrompt(input.client, state, prepared, signal), }).finally(() => { diff --git a/packages/cli/test/acp/event-behavior.test.ts b/packages/cli/test/acp/event-behavior.test.ts index 46ecc552a44b..ded7b7020aa3 100644 --- a/packages/cli/test/acp/event-behavior.test.ts +++ b/packages/cli/test/acp/event-behavior.test.ts @@ -439,6 +439,7 @@ describe("acp event behavior", () => { sessionID: "ses_cancel", cwd: "/workspace", start: { type: "input", id: "input_cancel" }, + writeTextFile: false, control, submit: async (signal) => { await fixture.client.session.prompt( @@ -481,6 +482,7 @@ describe("acp event behavior", () => { sessionID: "ses_cancel_admission", cwd: "/workspace", start: { type: "input", id: "input_cancel_admission" }, + writeTextFile: false, control, submit: (signal) => fixture.client.session.prompt( @@ -566,6 +568,7 @@ function turn(input: { sessionID: input.sessionID, cwd: "/workspace", start: { type: "input", id: input.inputID }, + writeTextFile: false, control: { cancelled: false, admission: new AbortController() }, submit: (signal) => input.fixture.client.session.prompt({ sessionID: input.sessionID, id: input.inputID, text: "hello" }, { signal }), diff --git a/packages/cli/test/acp/event.test.ts b/packages/cli/test/acp/event.test.ts index 0075f5c913b0..575705b294ee 100644 --- a/packages/cli/test/acp/event.test.ts +++ b/packages/cli/test/acp/event.test.ts @@ -96,6 +96,7 @@ test("acp prompt resolves after ordered turn updates", async () => { sessionID: "ses_test", cwd: "/workspace", start: { type: "input", id }, + writeTextFile: false, control: { cancelled: false, admission: new AbortController() }, submit: () => client.session.prompt({ sessionID: "ses_test", id, text: "hi" }), }) diff --git a/packages/cli/test/acp/initialize-auth.subprocess.test.ts b/packages/cli/test/acp/initialize-auth.subprocess.test.ts index 2303b5b3a136..904ceadd5a38 100644 --- a/packages/cli/test/acp/initialize-auth.subprocess.test.ts +++ b/packages/cli/test/acp/initialize-auth.subprocess.test.ts @@ -14,6 +14,7 @@ describe("acp initialize/auth subprocess", () => { expect(initialized.agentCapabilities?.mcpCapabilities?.sse).toBe(false) expect(initialized.agentCapabilities?.loadSession).toBe(true) expect(initialized.agentCapabilities?.sessionCapabilities?.close).toEqual({}) + expect(initialized.agentCapabilities?.sessionCapabilities?.delete).toEqual({}) expect(initialized.agentCapabilities?.sessionCapabilities?.fork).toEqual({}) expect(initialized.agentCapabilities?.sessionCapabilities?.list).toEqual({}) expect(initialized.agentCapabilities?.sessionCapabilities?.resume).toEqual({}) diff --git a/packages/cli/test/acp/lifecycle.subprocess.test.ts b/packages/cli/test/acp/lifecycle.subprocess.test.ts index ffd0332620ed..4ae855a7f386 100644 --- a/packages/cli/test/acp/lifecycle.subprocess.test.ts +++ b/packages/cli/test/acp/lifecycle.subprocess.test.ts @@ -1,5 +1,6 @@ import type { CloseSessionResponse, + DeleteSessionResponse, ListSessionsResponse, LoadSessionResponse, ResumeSessionResponse, @@ -60,6 +61,20 @@ describe("acp lifecycle subprocess", () => { expect(listed.sessions.some((item) => item.sessionId === session.sessionId)).toBe(true) }, 60_000) + test("delete capability and delete request", async () => { + await using fixture = await createAcpFixture() + const acp = fixture.spawn() + const initialized = await initialize(acp) + expect(initialized.agentCapabilities?.sessionCapabilities?.delete).toEqual({}) + const session = await newSession(acp, fixture.home) + + expect( + expectOk(await acp.request("session/delete", { sessionId: session.sessionId })), + ).toEqual({}) + const listed = expectOk(await acp.request("session/list", { cwd: fixture.home })) + expect(listed.sessions.some((item) => item.sessionId === session.sessionId)).toBe(false) + }, 60_000) + test("resume capability advertisement", async () => { await using fixture = await createAcpFixture() const initialized = await initialize(fixture.spawn()) diff --git a/packages/cli/test/acp/permission-behavior.test.ts b/packages/cli/test/acp/permission-behavior.test.ts index ad8ccd128a26..355ff5bb410b 100644 --- a/packages/cli/test/acp/permission-behavior.test.ts +++ b/packages/cli/test/acp/permission-behavior.test.ts @@ -4,6 +4,7 @@ import fs from "node:fs/promises" import os from "node:os" import path from "node:path" import { streamTurn } from "../../src/acp/event" +import { syncEditedFiles } from "../../src/acp/permission" import { createSseFixture, durableEvent, ephemeralEvent, withTimeout } from "./sse-fixture" type SessionUpdateParams = Parameters[0] @@ -12,6 +13,27 @@ type Connection = Pick describe("acp permission behavior", () => { + test("does not sync edits when writeTextFile was not advertised", async () => { + const writes: Parameters[0][] = [] + + await syncEditedFiles({ + connection: { + writeTextFile: async (input) => { + writes.push(input) + return {} + }, + }, + writeTextFile: false, + sessionID: "ses_no_write", + cwd: "/workspace", + toolName: "edit", + toolInput: { filePath: "/workspace/file.ts" }, + structured: {}, + }) + + expect(writes).toEqual([]) + }) + test("forwards allow-once and allow-always selections to the generated client", async () => { const permissionRequests: RequestPermissionRequest[] = [] const fixture = createSseFixture({ @@ -465,6 +487,7 @@ function startTurn(fixture: Fixture, connection: Connection, sessionID: string, sessionID, cwd, start: { type: "input", id: inputID }, + writeTextFile: true, control: { cancelled: false, admission: new AbortController() }, submit: (signal) => fixture.client.session.prompt({ sessionID, id: inputID, text: "hello" }, { signal }), }) diff --git a/packages/cli/test/acp/service-lifecycle.test.ts b/packages/cli/test/acp/service-lifecycle.test.ts index 2c4992b8e5b2..35a46a0ee2f6 100644 --- a/packages/cli/test/acp/service-lifecycle.test.ts +++ b/packages/cli/test/acp/service-lifecycle.test.ts @@ -225,6 +225,33 @@ describe("acp service lifecycle", () => { "/api/session/missing/interrupt", ]) }) + + test("deletes sessions from backing and local storage", async () => { + await using fixture = makeACPFixture({ + fetch(request) { + if (request.method === "POST" && request.path === "/api/session") { + return Response.json({ data: makeSession("ses_delete") }) + } + if (request.method === "DELETE" && request.path === "/api/session/ses_delete") { + return new Response(null, { status: 204 }) + } + return undefined + }, + }) + const session = await fixture.service.newSession({ cwd: "/workspace", mcpServers: [] }) + + expect(await fixture.service.deleteSession({ sessionId: session.sessionId })).toEqual({}) + expect(fixture.requests).toContainEqual({ + method: "DELETE", + path: "/api/session/ses_delete", + query: {}, + body: undefined, + }) + const missing = await fixture.service + .setSessionConfigOption({ sessionId: session.sessionId, configId: "effort", value: "high" }) + .catch((error: unknown) => error) + expect(missing).toMatchObject({ _tag: "ACPSessionNotFoundError", sessionId: session.sessionId }) + }) }) function currentValue(result: { readonly configOptions?: readonly SessionConfigOption[] | null }, id: string) { From 8f3465c951a024028a92adc9a283038a085f967f Mon Sep 17 00:00:00 2001 From: James Long Date: Thu, 23 Jul 2026 12:35:29 -0400 Subject: [PATCH 053/150] refactor(tui): load native V2 themes (#38430) --- packages/docs/script/generate-theme-tokens.ts | 6 +- packages/tui/src/app.tsx | 2 + .../tui/src/component/theme-error-toast.tsx | 20 ++++ packages/tui/src/context/theme.tsx | 92 ++++++++++++------ packages/tui/src/mini/theme.ts | 6 +- packages/tui/src/theme/index.ts | 74 +++++++++++---- packages/tui/src/theme/resolve.ts | 8 +- packages/tui/src/theme/v1.ts | 4 +- packages/tui/src/theme/v2/defaults.ts | 4 +- packages/tui/src/theme/v2/index.ts | 2 +- packages/tui/src/theme/v2/resolve.ts | 20 +--- packages/tui/src/theme/v2/schema.ts | 4 +- packages/tui/src/theme/v2/select.ts | 32 +++---- packages/tui/src/theme/v2/v1-migrate.ts | 8 +- packages/tui/test/cli/tui/theme-mode.test.tsx | 87 ++++++++++++++++- packages/tui/test/theme.test.ts | 94 ++++++++++++++++++- packages/tui/test/theme/v2/resolve.test.ts | 56 ++++++----- packages/tui/test/theme/v2/select.test.ts | 28 +++--- packages/tui/test/theme/v2/types.test.ts | 14 +-- packages/tui/test/theme/v2/v1-migrate.test.ts | 10 +- 20 files changed, 410 insertions(+), 161 deletions(-) create mode 100644 packages/tui/src/component/theme-error-toast.tsx diff --git a/packages/docs/script/generate-theme-tokens.ts b/packages/docs/script/generate-theme-tokens.ts index 0c441075f962..25f614d119b3 100644 --- a/packages/docs/script/generate-theme-tokens.ts +++ b/packages/docs/script/generate-theme-tokens.ts @@ -2,7 +2,7 @@ import { Schema, SchemaAST } from "effect" import { format } from "prettier" -import { ThemeDefinition, ThemeFile } from "../../tui/src/theme/v2/schema" +import { ThemeDefinition, ThemeDocument } from "../../tui/src/theme/v2/schema" const target = import.meta.dir + "/../snippets/generated/theme-tokens.mdx" const root = requireObject(ThemeDefinition.ast) @@ -52,8 +52,8 @@ const example = { default: "#101014", }, }, -} satisfies ThemeFile -Schema.decodeUnknownSync(ThemeFile)(example) +} satisfies ThemeDocument +Schema.decodeUnknownSync(ThemeDocument)(example) const output = await format( `{/* Generated by packages/docs/script/generate-theme-tokens.ts. Do not edit. */} diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 5e600b65e845..91b286e7986d 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -65,6 +65,7 @@ import { DialogThemeList } from "./component/dialog-theme-list" import { DialogHelp } from "./ui/dialog-help" import { DialogAgent } from "./component/dialog-agent" import { DialogSessionList } from "./component/dialog-session-list" +import { ThemeErrorToast } from "./component/theme-error-toast" import { ThemeProvider, useTheme } from "./context/theme" import { Home } from "./routes/home" import { Session } from "./routes/session" @@ -337,6 +338,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { + diff --git a/packages/tui/src/component/theme-error-toast.tsx b/packages/tui/src/component/theme-error-toast.tsx new file mode 100644 index 000000000000..f10a11bdc942 --- /dev/null +++ b/packages/tui/src/component/theme-error-toast.tsx @@ -0,0 +1,20 @@ +import { onCleanup } from "solid-js" +import { useTheme } from "../context/theme" +import { useToast } from "../ui/toast" + +export function ThemeErrorToast() { + const theme = useTheme() + const toast = useToast() + + onCleanup( + theme.onError(({ name, error }) => + toast.show({ + variant: "error", + title: `Failed to load theme: ${name}`, + message: error.message, + }), + ), + ) + + return null +} diff --git a/packages/tui/src/context/theme.tsx b/packages/tui/src/context/theme.tsx index 16ffdeaccbee..9b53e8d07a2a 100644 --- a/packages/tui/src/context/theme.tsx +++ b/packages/tui/src/context/theme.tsx @@ -5,21 +5,20 @@ import { addTheme, allThemes, hasTheme, - isTheme, + parseTheme, selectedForeground, setCustomThemes, setSystemTheme, subscribeThemes, upsertTheme, type Theme, - type ThemeJson, + type ThemeDocumentSource, } from "../theme" import { generateSyntax } from "../theme/v2/syntax" import { generateSystem, terminalMode } from "../theme/system" import { discoverThemes, themeDirectories } from "../theme/discovery" import { createComponentTheme, type ComponentTheme } from "../theme/v2/component" -import { resolveThemeFile } from "../theme/v2/resolve" -import { migrateV1 } from "../theme/v2/v1-migrate" +import { resolveThemeDocument } from "../theme/v2/resolve" import { themeModes } from "../theme/v2/select" import { createEffect, createMemo, onCleanup, onMount, type Accessor, type ParentProps } from "solid-js" import { createStore, produce } from "solid-js/store" @@ -29,6 +28,36 @@ import { Global } from "@opencode-ai/util/global" import { DevTools } from "../devtools" const themePerformance = DevTools.register({ id: "theme-performance", title: "Theme performance" }) +export type ThemeError = { name: string; error: Error } +type ThemeErrorHandler = (event: ThemeError) => void + +function createThemeErrors() { + let handler: ThemeErrorHandler | undefined + let pending: ThemeError | undefined + + return { + emit(name: string, cause: unknown) { + const event = { name, error: cause instanceof Error ? cause : new Error(String(cause)) } + if (handler) { + handler(event) + return + } + pending = event + }, + onError(next: ThemeErrorHandler) { + handler = next + if (pending) { + next(pending) + pending = undefined + } + return () => { + if (handler === next) handler = undefined + } + }, + } +} + +const themeErrors = createThemeErrors() export type ThemeSource = Readonly<{ discover(): Promise> @@ -61,7 +90,7 @@ export { const THEME_REFRESH_DELAYS = [250, 1000] as const type State = { - themes: Record + themes: Record mode: "dark" | "light" lock: "dark" | "light" | undefined active: string @@ -84,6 +113,7 @@ type ThemeService = { unlock(): void setMode(mode?: "dark" | "light", persist?: boolean): boolean set(theme: string): boolean + onError(handler: ThemeErrorHandler): () => void readonly ready: boolean } @@ -139,12 +169,7 @@ const themeContext = createSimpleContext({ return themes .discover() .then((themes) => { - setCustomThemes( - Object.entries(themes).reduce>((result, [name, theme]) => { - if (isTheme(theme)) result[name] = theme - return result - }, {}), - ) + setCustomThemes(themes) }) .catch(() => setStore("active", "opencode")) } @@ -269,30 +294,26 @@ const themeContext = createSimpleContext({ }) const initStarted = performance.now() - const source = createMemo(() => store.themes[store.active] ?? store.themes.opencode) - const sourceName = createMemo(() => (store.themes[store.active] ? store.active : "opencode")) - const file = createMemo(() => migrateV1(source())) - const modes = createMemo(() => themeModes(file())) - const mode = () => { - const supported = modes() - if (supported.includes(store.mode)) return store.mode - return supported[0] ?? store.mode - } - const valuesV2 = createMemo(() => resolveThemeFile(file(), mode(), sourceName())) + const selected = createMemo(() => { + const name = store.themes[store.active] ? store.active : "opencode" + try { + return loadTheme(store.themes[name], name, store.mode) + } catch (error) { + if (name === "opencode") throw error + themeErrors.emit(name, error) + setStore("active", "opencode") + return loadTheme(store.themes.opencode, "opencode", store.mode) + } + }) + const modes = () => selected().modes + const mode = () => selected().mode + const valuesV2 = () => selected().theme valuesV2() themePerformance.set("Init", `${(performance.now() - initStarted).toFixed(2)} ms`) const themeV2 = createComponentTheme(valuesV2, mode) const contextsV2 = { - elevated: createComponentTheme(() => { - const theme = valuesV2().contexts["@context:elevated"] - if (!theme) throw new Error("Theme context is not defined: elevated") - return theme - }, mode), - overlay: createComponentTheme(() => { - const theme = valuesV2().contexts["@context:overlay"] - if (!theme) throw new Error("Theme context is not defined: overlay") - return theme - }, mode), + elevated: createComponentTheme(() => valuesV2().contexts["@context:elevated"] ?? valuesV2(), mode), + overlay: createComponentTheme(() => valuesV2().contexts["@context:overlay"] ?? valuesV2(), mode), } createEffect(() => renderer.setBackgroundColor(valuesV2().background.default)) @@ -331,6 +352,7 @@ const themeContext = createSimpleContext({ .catch(() => {}) return true }, + onError: themeErrors.onError, get ready() { return store.ready }, @@ -354,6 +376,14 @@ export function ThemeContextProvider(props: ParentProps<{ context: ContextName } ) } + +function loadTheme(source: ThemeDocumentSource, name: string, requested: "dark" | "light") { + const document = parseTheme(source, name) + const modes = themeModes(document) + const mode = modes.includes(requested) ? requested : (modes[0] ?? requested) + return { modes, mode, theme: resolveThemeDocument(document, mode) } +} + export function createSyntaxStyleMemo(factory: () => SyntaxStyle) { const renderer = useRenderer() const retained = new Set() diff --git a/packages/tui/src/mini/theme.ts b/packages/tui/src/mini/theme.ts index 736eed08bb2f..3c5afed16e6a 100644 --- a/packages/tui/src/mini/theme.ts +++ b/packages/tui/src/mini/theme.ts @@ -10,7 +10,7 @@ import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui" import { ansiToRgba } from "../theme/color" import { resolveThemeColors } from "../theme/resolve" import { terminalMode } from "../theme/system" -import type { ThemeJson } from "../theme/v1" +import type { ThemeV1Json } from "../theme/v1" import type { EntryKind, RunTuiConfig } from "./types" type Tone = { @@ -184,7 +184,7 @@ function splashShadow(indexed: RGBA[], base: RGBA, overlay: RGBA, value: number) return nearestIndexed(indexed, mixed) } -export function resolveTheme(theme: ThemeJson, pick: "dark" | "light"): TuiThemeCurrent { +export function resolveTheme(theme: ThemeV1Json, pick: "dark" | "light"): TuiThemeCurrent { const resolved = resolveThemeColors(theme, pick, (code) => RGBA.fromIndex(code, ansiToRgba(code))) return { ...resolved.theme, @@ -246,7 +246,7 @@ function generateMutedTextColor(bg: RGBA, isDark: boolean, map: (rgba: RGBA) => return map(RGBA.fromInts(gray, gray, gray)) } -export function generateSystem(colors: TerminalColors, pick: "dark" | "light"): ThemeJson { +export function generateSystem(colors: TerminalColors, pick: "dark" | "light"): ThemeV1Json { const bg_snapshot = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0]!) const fg_snapshot = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7]!) const bg = RGBA.defaultBackground(bg_snapshot) diff --git a/packages/tui/src/theme/index.ts b/packages/tui/src/theme/index.ts index ef522b43c283..d128a75539fc 100644 --- a/packages/tui/src/theme/index.ts +++ b/packages/tui/src/theme/index.ts @@ -1,16 +1,25 @@ +import { Schema } from "effect" import { resolveThemeColors } from "./resolve" -import { DEFAULT_THEMES, type Theme, type ThemeJson } from "./v1" +import { DEFAULT_THEMES, type Theme, type ThemeV1Json } from "./v1" +import { resolveThemeDocument, themeDecodeError } from "./v2/resolve" +import { ThemeDocument } from "./v2/schema" +import { migrateV1 } from "./v2/v1-migrate" -export { DEFAULT_THEMES, generateSyntax, selectedForeground, type Theme, type ThemeJson } from "./v1" +export { DEFAULT_THEMES, generateSyntax, selectedForeground, type Theme, type ThemeV1Json } from "./v1" +export { resolveThemeDocument, type ThemeDocument } -const pluginThemes: Record = {} -let customThemes: Record = {} -let systemTheme: ThemeJson | undefined -const listeners = new Set<(themes: Record) => void>() +export type ThemeDocumentSource = Record + +const pluginThemes: Record = {} +let customThemes: Record = {} +let systemTheme: ThemeDocumentSource | undefined +const listeners = new Set<(themes: Record) => void>() +const parsed = new WeakMap() +const decodeThemeDocument = Schema.decodeUnknownSync(ThemeDocument) function listThemes() { // Priority: defaults < plugin installs < custom files < generated system. - const themes = { + const themes: Record = { ...DEFAULT_THEMES, ...pluginThemes, ...customThemes, @@ -31,23 +40,40 @@ export function allThemes() { return listThemes() } -export function isTheme(theme: unknown): theme is ThemeJson { - if (typeof theme !== "object" || theme === null || Array.isArray(theme)) return false - const value = Reflect.get(theme, "theme") - return typeof value === "object" && value !== null && !Array.isArray(value) +export function isThemeSource(source: unknown): source is ThemeDocumentSource { + if (typeof source !== "object" || source === null || Array.isArray(source)) return false + return "theme" in source || "version" in source +} + +export function parseTheme(source: ThemeDocumentSource, name = "theme") { + const cached = parsed.get(source) + if (cached) return cached + + const version = source.version ?? 1 + const document = + version === 1 + ? migrateV1(source as ThemeV1Json) + : version === 2 + ? decodeV2Theme(source, name) + : unsupportedThemeVersion(version) + + parsed.set(source, document) + return document } -export function subscribeThemes(listener: (themes: Record) => void) { +export function subscribeThemes(listener: (themes: Record) => void) { listeners.add(listener) return () => listeners.delete(listener) } -export function setCustomThemes(themes: Record) { - customThemes = themes +export function setCustomThemes(themes: Record) { + customThemes = Object.fromEntries( + Object.entries(themes).filter((entry): entry is [string, ThemeDocumentSource] => isThemeSource(entry[1])), + ) syncThemes() } -export function setSystemTheme(theme: ThemeJson | undefined) { +export function setSystemTheme(theme: ThemeDocumentSource | undefined) { systemTheme = theme syncThemes() } @@ -59,7 +85,7 @@ export function hasTheme(name: string) { export function addTheme(name: string, theme: unknown) { if (!name) return false - if (!isTheme(theme)) return false + if (!isThemeSource(theme)) return false if (hasTheme(name)) return false pluginThemes[name] = theme syncThemes() @@ -68,7 +94,7 @@ export function addTheme(name: string, theme: unknown) { export function upsertTheme(name: string, theme: unknown) { if (!name) return false - if (!isTheme(theme)) return false + if (!isThemeSource(theme)) return false if (customThemes[name] !== undefined) { customThemes[name] = theme } else { @@ -78,7 +104,7 @@ export function upsertTheme(name: string, theme: unknown) { return true } -export function resolveTheme(theme: ThemeJson, mode: "dark" | "light"): Theme { +export function resolveTheme(theme: ThemeV1Json, mode: "dark" | "light"): Theme { const resolved = resolveThemeColors(theme, mode) return { ...resolved.theme, @@ -86,3 +112,15 @@ export function resolveTheme(theme: ThemeJson, mode: "dark" | "light"): Theme { thinkingOpacity: resolved.thinkingOpacity, } } + +function decodeV2Theme(source: ThemeDocumentSource, name: string) { + try { + return decodeThemeDocument(source) + } catch (error) { + throw themeDecodeError(error, name) + } +} + +function unsupportedThemeVersion(version: unknown): never { + throw new Error(`Unsupported theme version: ${String(version)}`) +} diff --git a/packages/tui/src/theme/resolve.ts b/packages/tui/src/theme/resolve.ts index b8cf36866c44..6d028e8fa95b 100644 --- a/packages/tui/src/theme/resolve.ts +++ b/packages/tui/src/theme/resolve.ts @@ -1,9 +1,9 @@ import { RGBA } from "@opentui/core" import { ansiToRgba } from "./color" -import type { ColorValue, Theme, ThemeColor, ThemeJson } from "./v1" +import type { ColorValue, Theme, ThemeColor, ThemeV1Json } from "./v1" export function resolveThemeColors( - theme: ThemeJson, + theme: ThemeV1Json, mode: "dark" | "light", resolveAnsi: (code: number) => RGBA = ansiToRgba, ) { @@ -43,7 +43,9 @@ export function resolveThemeColors( ? resolveColor(theme.theme.selectedListItemText!) : resolved.background!, backgroundMenu: - theme.theme.backgroundMenu === undefined ? resolved.backgroundElement! : resolveColor(theme.theme.backgroundMenu), + theme.theme.backgroundMenu === undefined + ? resolved.backgroundElement! + : resolveColor(theme.theme.backgroundMenu), } satisfies Omit, hasSelectedListItemText, thinkingOpacity: theme.theme.thinkingOpacity ?? 0.6, diff --git a/packages/tui/src/theme/v1.ts b/packages/tui/src/theme/v1.ts index 483882e274a0..5cf1df1b9f28 100644 --- a/packages/tui/src/theme/v1.ts +++ b/packages/tui/src/theme/v1.ts @@ -98,7 +98,7 @@ export type Variant = { light: HexColor | RefName } export type ColorValue = HexColor | RefName | Variant | RGBA | number -export type ThemeJson = { +export type ThemeV1Json = { $schema?: string defs?: Record theme: Omit, "selectedListItemText" | "backgroundMenu"> & { @@ -108,7 +108,7 @@ export type ThemeJson = { } } -export const DEFAULT_THEMES: Record = { +export const DEFAULT_THEMES: Record = { aura, ayu, catppuccin, diff --git a/packages/tui/src/theme/v2/defaults.ts b/packages/tui/src/theme/v2/defaults.ts index 666f45014cfa..e9eb352a07ff 100644 --- a/packages/tui/src/theme/v2/defaults.ts +++ b/packages/tui/src/theme/v2/defaults.ts @@ -1,4 +1,4 @@ -import type { HueName, ThemeFile } from "./schema" +import type { HueName, ThemeDocument } from "./schema" export const DEFAULT_CATEGORICAL = [ "blue", @@ -437,4 +437,4 @@ export const DEFAULT_THEME = { }, }, }, -} satisfies ThemeFile +} satisfies ThemeDocument diff --git a/packages/tui/src/theme/v2/index.ts b/packages/tui/src/theme/v2/index.ts index 953112da50dd..b6cfbb98f524 100644 --- a/packages/tui/src/theme/v2/index.ts +++ b/packages/tui/src/theme/v2/index.ts @@ -16,7 +16,7 @@ export { SyntaxDefinition, SyntaxToken, ThemeDefinition, - ThemeFile, + ThemeDocument, type BackgroundDefinition, type DiffDefinition, type FileThemeDefinition, diff --git a/packages/tui/src/theme/v2/resolve.ts b/packages/tui/src/theme/v2/resolve.ts index 74d06e8f1ada..500f853dbd82 100644 --- a/packages/tui/src/theme/v2/resolve.ts +++ b/packages/tui/src/theme/v2/resolve.ts @@ -11,7 +11,7 @@ import { HueAlias, HueStep, ThemeDefinition, - ThemeFile, + ThemeDocument, } from "./schema" import type { ActionStateKey, @@ -26,7 +26,6 @@ import type { import { selectTheme, selectThemeMode } from "./select" const decodeThemeDefinitionSchema = Schema.decodeUnknownSync(ThemeDefinition) -const decodeThemeFileSchema = Schema.decodeUnknownSync(ThemeFile) function decodeThemeDefinition(input: unknown) { try { @@ -36,27 +35,18 @@ function decodeThemeDefinition(input: unknown) { } } -function decodeThemeFile(input: unknown, name: string) { - try { - return decodeThemeFileSchema(input) - } catch (error) { - throw themeDecodeError(error, name) - } -} - -function themeDecodeError(error: unknown, name: string) { +export function themeDecodeError(error: unknown, name: string) { const message = Schema.isSchemaError(error) ? error.message : String(error) const value = /got ("[^"]*"|\S+)/.exec(message)?.[1] ?? "value" return new Error(`Invalid theme: ${name} ${value} is an invalid value`, { cause: error }) } -export function resolveThemeFile(file: ThemeFile, mode?: "light" | "dark", name = "theme") { - const decoded = decodeThemeFile(file, name) - const selected = selectThemeMode(decoded, mode) +export function resolveThemeDocument(document: ThemeDocument, mode?: "light" | "dark") { + const selected = selectThemeMode(document, mode) const definition = selected.expanded ? selected.theme : expandTheme(selected.theme) const defaults = expandTheme(selectTheme(DEFAULT_THEME, selected.mode)) const core = expandTokens(fallback()) - const merged = decoded.standalone ? mergeTheme(core, definition) : mergeTheme(core, defaults, definition) + const merged = document.standalone ? mergeTheme(core, definition) : mergeTheme(core, defaults, definition) if (!merged["hue"]) throw new Error("Standalone themes must provide hues") return resolveExpandedTheme({ ...merged, diff --git a/packages/tui/src/theme/v2/schema.ts b/packages/tui/src/theme/v2/schema.ts index 76a33f7a0999..2256a3a1d64c 100644 --- a/packages/tui/src/theme/v2/schema.ts +++ b/packages/tui/src/theme/v2/schema.ts @@ -251,8 +251,8 @@ const FileMetadata = { version: Schema.Literal(2), standalone: Schema.optional(Schema.Boolean), } -export const ThemeFile = Schema.Union([ +export const ThemeDocument = Schema.Union([ Schema.Struct({ ...FileMetadata, light: ModeDefinition, dark: Schema.optional(ModeDefinition) }), Schema.Struct({ ...FileMetadata, light: Schema.optional(ModeDefinition), dark: ModeDefinition }), ]) -export type ThemeFile = Schema.Schema.Type +export type ThemeDocument = Schema.Schema.Type diff --git a/packages/tui/src/theme/v2/select.ts b/packages/tui/src/theme/v2/select.ts index c763179f53af..17d47488fa14 100644 --- a/packages/tui/src/theme/v2/select.ts +++ b/packages/tui/src/theme/v2/select.ts @@ -5,45 +5,45 @@ import type { Mode, ModeDefinition, ThemeDefinition, - ThemeFile, + ThemeDocument, } from "./index" export function selectTheme( - file: ThemeFile & { light: ThemeDefinition; dark: ThemeDefinition }, + document: ThemeDocument & { light: ThemeDefinition; dark: ThemeDefinition }, mode?: Mode, ): ThemeDefinition -export function selectTheme(file: ThemeFile, mode?: Mode): FileThemeDefinition -export function selectTheme(file: ThemeFile, mode?: Mode) { - return selectThemeMode(file, mode).theme +export function selectTheme(document: ThemeDocument, mode?: Mode): FileThemeDefinition +export function selectTheme(document: ThemeDocument, mode?: Mode) { + return selectThemeMode(document, mode).theme } export function selectThemeMode( - file: ThemeFile, + document: ThemeDocument, mode: Mode = "light", ): { theme: FileThemeDefinition; mode: Mode; expanded: boolean } { - const modes = themeModes(file) + const modes = themeModes(document) const selectedMode = modes.includes(mode) ? mode : modes[0] - const selected = file[selectedMode] + const selected = document[selectedMode] if (!selected) throw new Error("Theme must provide at least one mode") - if (merges(file.light) && merges(file.dark)) throw new Error("Light and dark themes cannot both merge modes") + if (merges(document.light) && merges(document.dark)) throw new Error("Light and dark themes cannot both merge modes") if (!merges(selected)) return { theme: selected, mode: selectedMode, expanded: false } const otherMode = selectedMode === "light" ? "dark" : "light" - const other = file[otherMode] + const other = document[otherMode] if (!other) throw new Error(`The ${selectedMode} theme cannot merge without a ${otherMode} theme`) const merged = mergeTheme(expandTheme(other), expandTheme(selected)) if (!merged["hue"]) throw new Error(`The ${otherMode} theme must provide hues when ${selectedMode} merges modes`) return { theme: merged as FileThemeDefinition, mode: selectedMode, expanded: true } } -export function themeModes(file: ThemeFile): readonly Mode[] { - if (merges(file.light) && !file.dark) throw new Error("The light theme cannot merge without a dark theme") - if (merges(file.dark) && !file.light) throw new Error("The dark theme cannot merge without a light theme") - return (["light", "dark"] as const).filter((mode) => file[mode] !== undefined) +export function themeModes(document: ThemeDocument): readonly Mode[] { + if (merges(document.light) && !document.dark) throw new Error("The light theme cannot merge without a dark theme") + if (merges(document.dark) && !document.light) throw new Error("The dark theme cannot merge without a light theme") + return (["light", "dark"] as const).filter((mode) => document[mode] !== undefined) } -export function supportsThemeMode(file: ThemeFile, mode: Mode) { - return themeModes(file).includes(mode) +export function supportsThemeMode(document: ThemeDocument, mode: Mode) { + return themeModes(document).includes(mode) } function merges(definition: ModeDefinition | undefined): definition is MergeModeDefinition { diff --git a/packages/tui/src/theme/v2/v1-migrate.ts b/packages/tui/src/theme/v2/v1-migrate.ts index 42e7c31b962d..4fdea0d135ee 100644 --- a/packages/tui/src/theme/v2/v1-migrate.ts +++ b/packages/tui/src/theme/v2/v1-migrate.ts @@ -1,8 +1,8 @@ import { RGBA } from "@opentui/core" import { oklchToHex, rgbToOklch } from "@opencode-ai/ui/theme/color" -import type { Theme, ThemeJson } from "../index" +import type { Theme, ThemeV1Json } from "../v1" import { DEFAULT_CATEGORICAL, DEFAULT_THEME } from "./defaults" -import type { FileThemeDefinition, Mode, ThemeFile } from "./index" +import type { FileThemeDefinition, Mode, ThemeDocument } from "./index" import { HueStep } from "./schema" type ThemeColor = Exclude @@ -14,7 +14,7 @@ const categoricalTokens: readonly V1HueToken[] = ["secondary", "accent", "succes const minimumChroma = 0.03 const lightThreshold = 0.6 -export function migrateV1(theme: ThemeJson): ThemeFile { +export function migrateV1(theme: ThemeV1Json): ThemeDocument { const light = resolveV1(theme, "light") const dark = resolveV1(theme, "dark") if (light.background.a > 0 && dark.background.a > 0 && light.background.equals(dark.background)) { @@ -234,7 +234,7 @@ function ambiguous(color: RGBA, chroma = toOklch(color).c) { return color.toInts()[3] === 0 || chroma < minimumChroma } -function resolveV1(theme: ThemeJson, mode: "dark" | "light"): Theme { +function resolveV1(theme: ThemeV1Json, mode: "dark" | "light"): Theme { const defs = theme.defs ?? {} function resolveColor(value: unknown, chain: string[] = []): RGBA { diff --git a/packages/tui/test/cli/tui/theme-mode.test.tsx b/packages/tui/test/cli/tui/theme-mode.test.tsx index cb195e384bb1..0b8123f05c7a 100644 --- a/packages/tui/test/cli/tui/theme-mode.test.tsx +++ b/packages/tui/test/cli/tui/theme-mode.test.tsx @@ -1,10 +1,13 @@ /** @jsxImportSource @opentui/solid */ import { testRender } from "@opentui/solid" import { expect, test } from "bun:test" +import { RGBA } from "@opentui/core" import { createTuiResolvedConfig } from "../../fixture/tui-runtime" import { DEFAULT_THEMES } from "../../../src/theme" +import { DEFAULT_THEME } from "../../../src/theme/v2/defaults" +import { selectTheme } from "../../../src/theme/v2/select" import { ConfigProvider } from "../../../src/config" -import { ThemeProvider, useTheme } from "../../../src/context/theme" +import { ThemeProvider, useTheme, type ThemeError } from "../../../src/context/theme" async function wait(fn: () => boolean) { const started = Date.now() @@ -24,6 +27,7 @@ test("uses an available mode while retaining the pinned preference", async () => const darkOnly = structuredClone(DEFAULT_THEMES.opencode) darkOnly.theme.background = "#111111" darkOnly.theme.text = "#eeeeee" + const native = { version: 2, dark: { text: { default: "#abcdef" } } } as const let theme: ReturnType | undefined function Probe() { @@ -42,7 +46,7 @@ test("uses an available mode while retaining the pinned preference", async () => Promise.resolve({ "light-only": lightOnly, "dark-only": darkOnly, dual }) }} + source={{ discover: () => Promise.resolve({ "light-only": lightOnly, "dark-only": darkOnly, dual, native }) }} > @@ -66,6 +70,85 @@ test("uses an available mode while retaining the pinned preference", async () => expect(current().set("dual")).toBeTrue() await wait(() => current().mode() === "dark") expect(current().modes()).toEqual(["light", "dark"]) + expect(current().set("native")).toBeTrue() + await wait(() => current().selected === "native") + expect(current().modes()).toEqual(["dark"]) + expect(current().themeV2.text.default.equals(RGBA.fromHex("#abcdef"))).toBeTrue() + } finally { + app.renderer.destroy() + } +}) + +test.each([ + ["schema", { version: 2, light: { categorical: [] } }], + ["mode merging", { version: 2, light: { mergeMode: true } }], + ["token reference", { version: 2, light: { text: { default: "$missing" } } }], +] as const)("falls back to OpenCode when configured V2 theme %s is invalid", async (_label, source) => { + let theme: ReturnType | undefined + let failure: ThemeError | undefined + let unsubscribe: (() => void) | undefined + + function Probe() { + const value = useTheme() + theme = value + unsubscribe = value.onError((error) => (failure = error)) + return {value.selected} + } + + const app = await testRender( + () => ( + + Promise.resolve({ invalid: source }) }}> + + + + ), + { width: 20, height: 2 }, + ) + app.renderer.start() + + try { + await wait(() => theme?.ready === true) + expect(theme?.selected).toBe("opencode") + expect(failure?.name).toBe("invalid") + expect(failure?.error).toBeInstanceOf(Error) + expect(failure?.error.message.length).toBeGreaterThan(0) + } finally { + unsubscribe?.() + app.renderer.destroy() + } +}) + +test("contextual themes fall back to a standalone theme's base view", async () => { + const standalone = { + version: 2, + standalone: true, + dark: { hue: selectTheme(DEFAULT_THEME, "dark").hue }, + } as const + let theme: ReturnType | undefined + + function Probe() { + theme = useTheme() + return {theme.selected} + } + + const app = await testRender( + () => ( + + Promise.resolve({ standalone }) }}> + + + + ), + { width: 20, height: 2 }, + ) + app.renderer.start() + + try { + await wait(() => theme?.ready === true) + if (!theme) throw new Error("Theme provider is not mounted") + expect(theme.contextual("elevated").themeV2.text.default).toBe(theme.themeV2.text.default) + expect(theme.contextual("overlay").themeV2.background.default).toBe(theme.themeV2.background.default) } finally { app.renderer.destroy() } diff --git a/packages/tui/test/theme.test.ts b/packages/tui/test/theme.test.ts index b2208f42c491..05d13d8601ef 100644 --- a/packages/tui/test/theme.test.ts +++ b/packages/tui/test/theme.test.ts @@ -2,7 +2,16 @@ import { expect, test } from "bun:test" import { mkdir, writeFile } from "node:fs/promises" import path from "node:path" import type { TerminalColors } from "@opentui/core" -import { DEFAULT_THEMES, addTheme, allThemes, hasTheme, resolveTheme } from "../src/theme" +import { + DEFAULT_THEMES, + addTheme, + allThemes, + hasTheme, + parseTheme, + resolveTheme, + setCustomThemes, + upsertTheme, +} from "../src/theme" import { discoverThemes, themeDirectories } from "../src/theme/discovery" import { terminalMode } from "../src/theme/system" import { tmpdir } from "./fixture/fixture" @@ -10,7 +19,7 @@ import { tmpdir } from "./fixture/fixture" test("addTheme writes into module theme store", () => { const name = `plugin-theme-${Date.now()}` expect(addTheme(name, DEFAULT_THEMES.opencode)).toBe(true) - expect(allThemes()[name]).toBeDefined() + expect(allThemes()[name]).toBe(DEFAULT_THEMES.opencode) }) test("addTheme keeps first theme for duplicate names", () => { @@ -22,15 +31,92 @@ test("addTheme keeps first theme for duplicate names", () => { expect(addTheme(name, one)).toBe(true) expect(addTheme(name, two)).toBe(false) - expect(allThemes()[name]!.theme.primary).toBe("#101010") + expect(allThemes()[name]).toBe(one) }) -test("addTheme ignores entries without a theme object", () => { +test("addTheme ignores values without a V1 theme or version", () => { const name = `plugin-theme-invalid-${Date.now()}` expect(addTheme(name, { defs: { a: "#ffffff" } })).toBe(false) + expect(addTheme(name, { light: {} })).toBe(false) expect(allThemes()[name]).toBeUndefined() }) +test("addTheme defers validation of versioned sources", () => { + const name = `plugin-theme-versioned-${Date.now()}` + expect(addTheme(name, { version: 2 })).toBe(true) + expect(() => parseTheme(allThemes()[name]!, name)).toThrow(`Invalid theme: ${name}`) +}) + +test("parseTheme delegates malformed V1 sources and rejects unknown versions", () => { + expect(() => parseTheme({})).toThrow() + expect(() => parseTheme({ version: 3 })).toThrow("Unsupported theme version: 3") +}) + +test("parses unversioned and explicit V1 themes lazily once", () => { + const unversioned = structuredClone(DEFAULT_THEMES.opencode) + const explicit = { ...structuredClone(DEFAULT_THEMES.opencode), version: 1 } + const first = parseTheme(unversioned, "unversioned") + const second = parseTheme(explicit, "explicit") + + expect(first.version).toBe(2) + expect(second.version).toBe(2) + expect(parseTheme(unversioned, "unversioned")).toBe(first) + expect(parseTheme(explicit, "explicit")).toBe(second) +}) + +test("decodes native V2 themes lazily once", () => { + const name = `plugin-theme-v2-${Date.now()}` + const source = { version: 2, light: { categorical: ["red"] } } as const + + expect(addTheme(name, source)).toBe(true) + expect(allThemes()[name]).toBe(source) + const document = parseTheme(allThemes()[name]!, name) + expect(document.light?.categorical).toEqual(["red"]) + expect(parseTheme(allThemes()[name]!, name)).toBe(document) +}) + +test("defers invalid V2 errors until parsing", () => { + const name = `plugin-theme-invalid-v2-${Date.now()}` + expect(addTheme(name, { version: 2, light: { categorical: [] } })).toBe(true) + expect(() => parseTheme(allThemes()[name]!, name)).toThrow(`Invalid theme: ${name}`) +}) + +test("defers invalid V1 errors until parsing", () => { + const name = `plugin-theme-invalid-v1-${Date.now()}` + const source = structuredClone(DEFAULT_THEMES.opencode) + source.defs = { ...source.defs, one: "two", two: "one" } + source.theme.primary = "one" + + expect(addTheme(name, source)).toBe(true) + expect(() => parseTheme(allThemes()[name]!, name)).toThrow("Circular color reference") +}) + +test("replacement sources receive independent parse caches", () => { + const name = `plugin-theme-replace-${Date.now()}` + const first = structuredClone(DEFAULT_THEMES.opencode) + const second = structuredClone(DEFAULT_THEMES.opencode) + second.theme.primary = "#123456" + + expect(addTheme(name, first)).toBe(true) + const previous = parseTheme(allThemes()[name]!, name) + expect(upsertTheme(name, second)).toBe(true) + const next = parseTheme(allThemes()[name]!, name) + expect(next).not.toBe(previous) + expect(parseTheme(allThemes()[name]!, name)).toBe(next) +}) + +test("custom themes retain precedence over plugin themes", () => { + const name = `plugin-theme-precedence-${Date.now()}` + const plugin = structuredClone(DEFAULT_THEMES.opencode) + const custom = structuredClone(DEFAULT_THEMES.opencode) + + expect(addTheme(name, plugin)).toBe(true) + setCustomThemes({ [name]: custom }) + expect(allThemes()[name]).toBe(custom) + setCustomThemes({}) + expect(allThemes()[name]).toBe(plugin) +}) + test("hasTheme checks theme presence", () => { const name = `plugin-theme-has-${Date.now()}` expect(hasTheme(name)).toBe(false) diff --git a/packages/tui/test/theme/v2/resolve.test.ts b/packages/tui/test/theme/v2/resolve.test.ts index e91a79d2b20a..9d42b4034136 100644 --- a/packages/tui/test/theme/v2/resolve.test.ts +++ b/packages/tui/test/theme/v2/resolve.test.ts @@ -1,16 +1,21 @@ import { expect, test } from "bun:test" import { RGBA } from "@opentui/core" +import { parseTheme, type ThemeDocumentSource } from "../../../src/theme" import { DEFAULT_THEME } from "../../../src/theme/v2/defaults" -import type { ThemeDefinition } from "../../../src/theme/v2" -import { resolveTheme, resolveThemeFile } from "../../../src/theme/v2/resolve" +import type { Mode, ThemeDefinition } from "../../../src/theme/v2" +import { resolveTheme, resolveThemeDocument } from "../../../src/theme/v2/resolve" import { selectTheme } from "../../../src/theme/v2/select" const light = selectTheme(DEFAULT_THEME, "light") const dark = selectTheme(DEFAULT_THEME, "dark") -test("resolves one-mode files with defaults for the available mode", () => { - const resolvedLight = resolveThemeFile({ version: 2, light: {} }, "dark") - const resolvedDark = resolveThemeFile({ version: 2, dark: {} }, "light") +function resolveSource(source: ThemeDocumentSource, mode?: Mode, name?: string) { + return resolveThemeDocument(parseTheme(source, name), mode) +} + +test("resolves one-mode documents with defaults for the available mode", () => { + const resolvedLight = resolveSource({ version: 2, light: {} }, "dark") + const resolvedDark = resolveSource({ version: 2, dark: {} }, "light") expect(resolvedLight.background.default.equals(resolveTheme(light).background.default)).toBeTrue() expect(resolvedDark.background.default.equals(resolveTheme(dark).background.default)).toBeTrue() @@ -18,26 +23,19 @@ test("resolves one-mode files with defaults for the available mode", () => { expect(resolvedDark.categorical.length).toBeGreaterThan(0) }) -test("rejects theme files without a mode", () => { - // @ts-expect-error Runtime decoding also enforces the at-least-one-mode invariant. - expect(() => resolveThemeFile({ version: 2 })).toThrow("Invalid theme") +test("rejects theme documents without a mode", () => { + expect(() => resolveSource({ version: 2 })).toThrow("Invalid theme") }) test("validates and resolves categorical hues in configured order", () => { - const theme = resolveThemeFile({ version: 2, light: { categorical: ["accent", "red", "interactive"] } }, "light") + const theme = resolveSource({ version: 2, light: { categorical: ["accent", "red", "interactive"] } }, "light") expect(theme.categorical[0]).toBe(theme.hue.accent) expect(theme.categorical[1]).toBe(theme.hue.red) expect(theme.categorical[2]).toBe(theme.hue.interactive) expect(theme.contexts["@context:elevated"]?.categorical).toBe(theme.categorical) - expect(() => resolveThemeFile({ version: 2, light: { categorical: [] } }, "light")).toThrow("Invalid theme") - expect(() => - resolveThemeFile( - // @ts-expect-error Runtime decoding rejects unknown categorical hue names. - { version: 2, light: { categorical: ["magenta"] } }, - "light", - ), - ).toThrow("Invalid theme") + expect(() => resolveSource({ version: 2, light: { categorical: [] } }, "light")).toThrow("Invalid theme") + expect(() => resolveSource({ version: 2, light: { categorical: ["magenta"] } }, "light")).toThrow("Invalid theme") }) test("uses the default categorical order for direct definitions", () => { @@ -93,7 +91,7 @@ test("resolves base hue aliases and rejects circular hue aliases", () => { ...light, hue: { ...light.hue, blue: "$hue.red", purple: "$hue.blue" }, }) - const overridden = resolveThemeFile({ version: 2, light: { hue: { blue: "$hue.red" } }, dark: {} }, "light") + const overridden = resolveSource({ version: 2, light: { hue: { blue: "$hue.red" } }, dark: {} }, "light") expect(aliased.hue.blue).not.toBe(aliased.hue.red) expect(aliased.hue.blue[500].equals(aliased.hue.red[500])).toBeTrue() @@ -131,8 +129,8 @@ test("steps by hue source when adjacent colors have equal values", () => { expect(theme.increase(theme.hue.neutral[300])).toBe(theme.hue.neutral[400]) }) -test("merges partial files with the selected OpenCode defaults", () => { - const theme = resolveThemeFile( +test("merges partial documents with the selected OpenCode defaults", () => { + const theme = resolveSource( { version: 2, light: { @@ -150,7 +148,7 @@ test("merges partial files with the selected OpenCode defaults", () => { }) test("expands user structural fallbacks before merging defaults", () => { - const expanded = resolveThemeFile( + const expanded = resolveSource( { version: 2, light: { @@ -161,7 +159,7 @@ test("expands user structural fallbacks before merging defaults", () => { }, "light", ) - const isolatedState = resolveThemeFile( + const isolatedState = resolveSource( { version: 2, light: { @@ -181,9 +179,9 @@ test("expands user structural fallbacks before merging defaults", () => { }) test("standalone themes skip OpenCode defaults and use the red core fallback", () => { - const file = { version: 2, standalone: true, light: { hue: light.hue }, dark: { hue: dark.hue } } as const - const lightTheme = resolveThemeFile(file, "light") - const darkTheme = resolveThemeFile(file, "dark") + const document = { version: 2, standalone: true, light: { hue: light.hue }, dark: { hue: dark.hue } } as const + const lightTheme = resolveSource(document, "light") + const darkTheme = resolveSource(document, "dark") expect(lightTheme.text.default.toInts()).toEqual([255, 0, 0, 255]) expect(lightTheme.background.default.toInts()).toEqual([255, 0, 0, 255]) @@ -192,7 +190,7 @@ test("standalone themes skip OpenCode defaults and use the red core fallback", ( }) test("uses defaults for the selected mode when it merges the other mode", () => { - const theme = resolveThemeFile( + const theme = resolveSource( { version: 2, light: { hue: light.hue, background: { default: "#123456" } }, @@ -217,7 +215,7 @@ test("resolves matched action variants and states", () => { }) test("resolves elevated hover surfaces from direct colors", () => { - const theme = resolveThemeFile( + const theme = resolveSource( { version: 2, light: { background: { surface: { offset: "#123456", overlay: "#234567" } } }, @@ -231,7 +229,7 @@ test("resolves elevated hover surfaces from direct colors", () => { }) test("resolves transparent colors", () => { - const theme = resolveThemeFile({ + const theme = resolveSource({ version: 2, light: { background: { formfield: { default: "transparent" } } }, dark: { background: { formfield: { default: "transparent" } } }, @@ -241,7 +239,7 @@ test("resolves transparent colors", () => { test("reports theme decoding failures as native errors", () => { expect(() => - resolveThemeFile( + resolveSource( { version: 2, light: { text: { default: "opaque" } }, diff --git a/packages/tui/test/theme/v2/select.test.ts b/packages/tui/test/theme/v2/select.test.ts index 792321de13a5..00f0bd437dee 100644 --- a/packages/tui/test/theme/v2/select.test.ts +++ b/packages/tui/test/theme/v2/select.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test" -import type { HueDefinition, ThemeDefinition, ThemeFile } from "../../../src/theme/v2" +import type { HueDefinition, ThemeDefinition, ThemeDocument } from "../../../src/theme/v2" import { selectTheme, selectThemeMode, supportsThemeMode, themeModes } from "../../../src/theme/v2/select" const hue = {} as HueDefinition @@ -11,20 +11,20 @@ const dark = { } satisfies ThemeDefinition test("requires and selects independent light and dark themes", () => { - const file = { version: 2, light, dark } satisfies ThemeFile - expect(selectTheme(file)).toBe(light) - expect(selectTheme(file, "light")).toBe(light) - expect(selectTheme(file, "dark")).toBe(dark) - expect(selectThemeMode(file, "dark").mode).toBe("dark") + const document = { version: 2, light, dark } satisfies ThemeDocument + expect(selectTheme(document)).toBe(light) + expect(selectTheme(document, "light")).toBe(light) + expect(selectTheme(document, "dark")).toBe(dark) + expect(selectThemeMode(document, "dark").mode).toBe("dark") }) test("merges an expanded mode override over the other mode", () => { - const file = { + const document = { version: 2, light, dark: { mergeMode: true, text: { default: "#ffffff" } }, - } satisfies ThemeFile - const selected = selectTheme(file, "dark") + } satisfies ThemeDocument + const selected = selectTheme(document, "dark") expect(selected.hue).toBeDefined() expect(selected.text?.default).toBe("#ffffff") @@ -41,8 +41,8 @@ test("replaces categorical order in a merge mode", () => { }) test("selects the available mode when the requested mode is missing", () => { - const lightOnly = { version: 2, light } satisfies ThemeFile - const darkOnly = { version: 2, dark } satisfies ThemeFile + const lightOnly = { version: 2, light } satisfies ThemeDocument + const darkOnly = { version: 2, dark } satisfies ThemeDocument expect(themeModes(lightOnly)).toEqual(["light"]) expect(themeModes(darkOnly)).toEqual(["dark"]) @@ -62,10 +62,10 @@ test("rejects a merge mode without its base mode", () => { }) test("rejects mutual mode merging", () => { - const file = { + const document = { version: 2, light: { mergeMode: true }, dark: { mergeMode: true }, - } satisfies ThemeFile - expect(() => selectTheme(file)).toThrow("cannot both merge") + } satisfies ThemeDocument + expect(() => selectTheme(document)).toThrow("cannot both merge") }) diff --git a/packages/tui/test/theme/v2/types.test.ts b/packages/tui/test/theme/v2/types.test.ts index a44322d71a20..f98a013f5ef1 100644 --- a/packages/tui/test/theme/v2/types.test.ts +++ b/packages/tui/test/theme/v2/types.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test" -import type { BackgroundDefinition, TextDefinition, ThemeDefinition, ThemeFile } from "../../../src/theme/v2" +import type { BackgroundDefinition, TextDefinition, ThemeDefinition, ThemeDocument } from "../../../src/theme/v2" const text = { default: "$hue.neutral.900", @@ -51,11 +51,11 @@ const definition = { "@context:overlay": { background: { default: "$hue.neutral.300" } }, } satisfies ThemeDefinition -const file = { version: 2, light: definition, dark: definition } satisfies ThemeFile -const lightOnly = { version: 2, light: definition } satisfies ThemeFile -const darkOnly = { version: 2, dark: definition } satisfies ThemeFile -// @ts-expect-error A theme file must provide at least one mode. -const empty = { version: 2 } satisfies ThemeFile +const document = { version: 2, light: definition, dark: definition } satisfies ThemeDocument +const lightOnly = { version: 2, light: definition } satisfies ThemeDocument +const darkOnly = { version: 2, dark: definition } satisfies ThemeDocument +// @ts-expect-error A theme document must provide at least one mode. +const empty = { version: 2 } satisfies ThemeDocument test("supports property-first definitions, variants, states, and contexts", () => { expect(text.action.primary.$hovered).toBe("$hue.neutral.200") @@ -68,7 +68,7 @@ test("supports property-first definitions, variants, states, and contexts", () = expect(definition["@context:elevated"].text?.default).toBe("$hue.neutral.800") expect(definition["@context:overlay"].background?.default).toBe("$hue.neutral.300") expect(definition.categorical).toEqual(["blue", "accent"]) - expect(file.light).toBe(definition) + expect(document.light).toBe(definition) expect(lightOnly.light).toBe(definition) expect(darkOnly.dark).toBe(definition) expect(empty.version).toBe(2) diff --git a/packages/tui/test/theme/v2/v1-migrate.test.ts b/packages/tui/test/theme/v2/v1-migrate.test.ts index 84c3639b09f6..ced1ae4800ac 100644 --- a/packages/tui/test/theme/v2/v1-migrate.test.ts +++ b/packages/tui/test/theme/v2/v1-migrate.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "bun:test" import { DEFAULT_THEMES, resolveTheme as resolveV1 } from "../../../src/theme" -import { resolveThemeFile } from "../../../src/theme/v2/resolve" +import { resolveThemeDocument } from "../../../src/theme/v2/resolve" import { selectThemeMode, themeModes } from "../../../src/theme/v2/select" import { migrateV1 } from "../../../src/theme/v2/v1-migrate" import { DEFAULT_CATEGORICAL, DEFAULT_THEME } from "../../../src/theme/v2/defaults" @@ -9,7 +9,7 @@ test("migrates resolved V1 modes into literal V2 tokens", () => { const migrated = migrateV1(DEFAULT_THEMES.opencode) if (!migrated.light || !migrated.dark) throw new Error("Expected both modes") const legacy = resolveV1(DEFAULT_THEMES.opencode, "light") - const resolved = resolveThemeFile(migrated, "light") + const resolved = resolveThemeDocument(migrated, "light") expect(migrated.standalone).toBeTrue() expect(migrated.light.categorical?.length).toBeGreaterThan(0) @@ -83,8 +83,8 @@ test("infers chromatic hues, anchors light and dark colors, and aliases ambiguou expect(migrated.light.hue?.purple).toBe("$hue.gray") expect(migrated.light.hue?.accent).toBe("$hue.gray") expect(migrated.light.hue?.interactive).toBe("$hue.gray") - expect(() => resolveThemeFile(migrated, "light")).not.toThrow() - expect(() => resolveThemeFile(migrated, "dark")).not.toThrow() + expect(() => resolveThemeDocument(migrated, "light")).not.toThrow() + expect(() => resolveThemeDocument(migrated, "dark")).not.toThrow() }) test("orders categorical hues by V1 semantic color mapping", () => { @@ -185,7 +185,7 @@ test("migrates every built-in V1 theme in its supported modes", () => { for (const source of Object.values(DEFAULT_THEMES)) { const migrated = migrateV1(source) for (const mode of themeModes(migrated)) { - expect(resolveThemeFile(migrated, mode).text.default).toBeDefined() + expect(resolveThemeDocument(migrated, mode).text.default).toBeDefined() } } }) From 74e92f73e034dfcfbd092ae12d8ff2ea2300414e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 12:32:33 -0500 Subject: [PATCH 054/150] refactor(ai): remove unused response format (#38540) Co-authored-by: Aiden Cline --- packages/ai/src/llm.ts | 2 +- packages/ai/src/schema/messages.ts | 9 --------- packages/core/src/aisdk.ts | 7 ------- 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/packages/ai/src/llm.ts b/packages/ai/src/llm.ts index e4781d8608b0..ecdf30ae47d8 100644 --- a/packages/ai/src/llm.ts +++ b/packages/ai/src/llm.ts @@ -81,7 +81,7 @@ const GENERATE_OBJECT_TOOL_NAME = "generate_object" const GENERATE_OBJECT_TOOL_DESCRIPTION = "Return the structured result by calling this tool." -type GenerateObjectBase = Omit +type GenerateObjectBase = Omit export class GenerateObjectResponse { constructor( diff --git a/packages/ai/src/schema/messages.ts b/packages/ai/src/schema/messages.ts index 4a9de3a735e4..e6617ddc9ed8 100644 --- a/packages/ai/src/schema/messages.ts +++ b/packages/ai/src/schema/messages.ts @@ -261,13 +261,6 @@ export namespace ToolChoice { } } -export const ResponseFormat = Schema.Union([ - Schema.Struct({ type: Schema.Literal("text") }), - Schema.Struct({ type: Schema.Literal("json"), schema: JsonSchema }), - Schema.Struct({ type: Schema.Literal("tool"), tool: ToolDefinition }), -]).pipe(Schema.toTaggedUnion("type")) -export type ResponseFormat = Schema.Schema.Type - export class LLMRequest extends Schema.Class("LLM.Request")({ id: Schema.optional(Schema.String), model: ModelSchema, @@ -278,7 +271,6 @@ export class LLMRequest extends Schema.Class("LLM.Request")({ generation: Schema.optional(GenerationOptions), providerOptions: Schema.optional(ProviderOptions), http: Schema.optional(HttpOptions), - responseFormat: Schema.optional(ResponseFormat), cache: Schema.optional(CachePolicy), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }) {} @@ -296,7 +288,6 @@ export namespace LLMRequest { generation: request.generation, providerOptions: request.providerOptions, http: request.http, - responseFormat: request.responseFormat, cache: request.cache, metadata: request.metadata, }) diff --git a/packages/core/src/aisdk.ts b/packages/core/src/aisdk.ts index 6d749b8526f4..33a4d670af32 100644 --- a/packages/core/src/aisdk.ts +++ b/packages/core/src/aisdk.ts @@ -422,7 +422,6 @@ function callOptions(request: LLMRequest): LanguageModelV3CallOptions { presencePenalty: request.generation?.presencePenalty, frequencyPenalty: request.generation?.frequencyPenalty, seed: request.generation?.seed, - responseFormat: responseFormat(request), tools: request.tools.map(tool), toolChoice: toolChoice(request.toolChoice), headers: request.http?.headers, @@ -527,12 +526,6 @@ function toolChoice(input: LLMRequest["toolChoice"]): LanguageModelV3ToolChoice return { type: input.type } } -function responseFormat(request: LLMRequest): LanguageModelV3CallOptions["responseFormat"] { - if (request.responseFormat?.type === "json") - return { type: "json", schema: request.responseFormat.schema as JSONSchema7 } - if (request.responseFormat) return { type: "text" } -} - function providerOptions(input: LLMRequest["providerOptions"]): SharedV3ProviderOptions | undefined { if (!input) return undefined return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsonObject(value)])) From ad596fb42bfbf29a2585babbae1e987c54240eea Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 12:34:24 -0500 Subject: [PATCH 055/150] chore(core): upgrade fff to 0.10.1 (#38545) Co-authored-by: Aiden Cline --- bun.lock | 22 +++++++++--------- package.json | 1 - packages/core/package.json | 2 +- patches/@ff-labs%2Ffff-bun@0.9.3.patch | 31 -------------------------- 4 files changed, 13 insertions(+), 43 deletions(-) delete mode 100644 patches/@ff-labs%2Ffff-bun@0.9.3.patch diff --git a/bun.lock b/bun.lock index 72db1b9e9285..24e3c97c4ed2 100644 --- a/bun.lock +++ b/bun.lock @@ -362,7 +362,7 @@ "@aws-sdk/credential-providers": "3.1057.0", "@effect/platform-node": "catalog:", "@effect/sql-sqlite-bun": "catalog:", - "@ff-labs/fff-bun": "0.9.4", + "@ff-labs/fff-bun": "0.10.1", "@lydell/node-pty": "catalog:", "@modelcontextprotocol/sdk": "1.29.0", "@opencode-ai/ai": "workspace:*", @@ -1641,23 +1641,25 @@ "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], - "@ff-labs/fff-bin-darwin-arm64": ["@ff-labs/fff-bin-darwin-arm64@0.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xyivu2xB++O5xXDx5Qm50JsU2aXt8YgXlGVhH/HE7UMYDrE6L6f1RYdYs8Y0bn0D3D0+bFBrN5ELPszK9E4Wbw=="], + "@ff-labs/fff-bin-android-arm64": ["@ff-labs/fff-bin-android-arm64@0.10.1", "", { "os": "android", "cpu": "arm64" }, "sha512-6Bsaa6yKEd2HV1M2WtqSYhoZucKYffIUms6GYoPN48QHP8hZgO8GXJ85/JDxKlkCJsN2hw1ROiwQK0+xUHYhFQ=="], - "@ff-labs/fff-bin-darwin-x64": ["@ff-labs/fff-bin-darwin-x64@0.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xLooAhCnTDCipPSMMZz7kGF3lhRHx6aP5fb6DJ0Ipyw/w/UWJb+xITJFszUl/QnIBoJ/qjDc93/FZMo1dk6gVA=="], + "@ff-labs/fff-bin-darwin-arm64": ["@ff-labs/fff-bin-darwin-arm64@0.10.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7yUP+56sG3UTrLg7eepOD16yM2dgiD7g6Ase2XWQB7oXwLV7mBMylPKIli2pP3kHzfw9K+BS3WAwHKHJ2QhxYw=="], - "@ff-labs/fff-bin-linux-arm64-gnu": ["@ff-labs/fff-bin-linux-arm64-gnu@0.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-m5+8vA+1veaUUWonwva1WsU6m1HRm8CpYUzr06KDB65mewlmPbqz7+Fh7hjEfiD8C4mHVHe6RysULvAH1yhsdw=="], + "@ff-labs/fff-bin-darwin-x64": ["@ff-labs/fff-bin-darwin-x64@0.10.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-9zb+P1xtyqu/jVklm5RKF2zm9doRRsBNbkF/a8S4aSqSJoNlQR8ZF7C129fzOLfffVAjjgcO2l8oJgxOzHYiwQ=="], - "@ff-labs/fff-bin-linux-arm64-musl": ["@ff-labs/fff-bin-linux-arm64-musl@0.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-EMeWm7CSTVkizy4ZEzUkLDP024tVcbCUthduuIhekFQRDsiaAze0YboIylWb9HBHJCZlCCoZrWAl4nnJbsX7AA=="], + "@ff-labs/fff-bin-linux-arm64-gnu": ["@ff-labs/fff-bin-linux-arm64-gnu@0.10.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-KTwr9CUfCJv0vtG2xG+nMxDae/2NJY2/oVmtgSvTnH9baI6JprqsFGphbukx7mdW/QMPwBiYtoO8ZGoC7i92jA=="], - "@ff-labs/fff-bin-linux-x64-gnu": ["@ff-labs/fff-bin-linux-x64-gnu@0.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-pglE0uLkhnlE6bStXqfgUjYTSj+2sVwXaPfoA0QksidAsQor6NRt8004mygzC9DPubgHq5B9QezPfEwigKaP9Q=="], + "@ff-labs/fff-bin-linux-arm64-musl": ["@ff-labs/fff-bin-linux-arm64-musl@0.10.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-oznmSpV+zjiAPiwqbA4y+SAsMaYvDhGllHiT9L4ebdQnSOfcQzlUwvo45OhBPwHBt47tIois4bkF/MITlDk54A=="], - "@ff-labs/fff-bin-linux-x64-musl": ["@ff-labs/fff-bin-linux-x64-musl@0.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-VNKxgl8qs3aTfXViX7lqRK1aLu311h8dtBFqG4Scv+9Oi7WprybUp5L7IZ8sxKERaDAaiJMXHodXa1c90QdK8w=="], + "@ff-labs/fff-bin-linux-x64-gnu": ["@ff-labs/fff-bin-linux-x64-gnu@0.10.1", "", { "os": "linux", "cpu": "x64" }, "sha512-KDpl8lwSEOauP/6FSJIvnARzE+ILm2rVIRQAio9dc5nn56EvlsryBWry0c5V0Tbw1PV0lNrZ+bzcIiPuTacvzw=="], - "@ff-labs/fff-bin-win32-arm64": ["@ff-labs/fff-bin-win32-arm64@0.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-uFEt0aNL54vQxq1ivjxRuo+thnhS4wLqa4INl4VXnXJUmwB42XXxD+gsj7vzhBLLx4cFf0aWgy/+TVDR8yjZtQ=="], + "@ff-labs/fff-bin-linux-x64-musl": ["@ff-labs/fff-bin-linux-x64-musl@0.10.1", "", { "os": "linux", "cpu": "x64" }, "sha512-mZCpojVtGNDr/wdCUEAHdfrl6qORvKHv3Pw35TaOdshnG4wocoA5bBKTGj/zmAU11o0GMK4V+wUhdNrgqoE10w=="], - "@ff-labs/fff-bin-win32-x64": ["@ff-labs/fff-bin-win32-x64@0.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-Yd2Eyxj+slWv+0QDW9/xBpu9FXq+hwD0rXQD5184/88d+xwWCLKhEP2w8I6OO9XCg+kLT79UJb+k0WwXUtBtMw=="], + "@ff-labs/fff-bin-win32-arm64": ["@ff-labs/fff-bin-win32-arm64@0.10.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-SbT76ETXC5AgV9J8sVDozKG8wzrrxsOn8lbBprlOtx+O5V5EYmS1W7BlsatTHy6ddQ3uU5oOYO04PWZBuP6wXg=="], - "@ff-labs/fff-bun": ["@ff-labs/fff-bun@0.9.4", "", { "optionalDependencies": { "@ff-labs/fff-bin-darwin-arm64": "0.9.4", "@ff-labs/fff-bin-darwin-x64": "0.9.4", "@ff-labs/fff-bin-linux-arm64-gnu": "0.9.4", "@ff-labs/fff-bin-linux-arm64-musl": "0.9.4", "@ff-labs/fff-bin-linux-x64-gnu": "0.9.4", "@ff-labs/fff-bin-linux-x64-musl": "0.9.4", "@ff-labs/fff-bin-win32-arm64": "0.9.4", "@ff-labs/fff-bin-win32-x64": "0.9.4" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-7HUraaK/g5dStAnuKAuzsXVOQvqqX0ylo5G+DxYwsCjCDc42bjoEAAHqz/3Sn3raUNw97KMoz87XR9QyrLEfVw=="], + "@ff-labs/fff-bin-win32-x64": ["@ff-labs/fff-bin-win32-x64@0.10.1", "", { "os": "win32", "cpu": "x64" }, "sha512-dcpHUCBEZoXKQCdKa3bADfgcNyeyfM8tatXrILqIFYi9GL8E4GnzKx1rGkgP5ukVtuj0WuoJyyqSvGjpJPIT8w=="], + + "@ff-labs/fff-bun": ["@ff-labs/fff-bun@0.10.1", "", { "optionalDependencies": { "@ff-labs/fff-bin-android-arm64": "0.10.1", "@ff-labs/fff-bin-darwin-arm64": "0.10.1", "@ff-labs/fff-bin-darwin-x64": "0.10.1", "@ff-labs/fff-bin-linux-arm64-gnu": "0.10.1", "@ff-labs/fff-bin-linux-arm64-musl": "0.10.1", "@ff-labs/fff-bin-linux-x64-gnu": "0.10.1", "@ff-labs/fff-bin-linux-x64-musl": "0.10.1", "@ff-labs/fff-bin-win32-arm64": "0.10.1", "@ff-labs/fff-bin-win32-x64": "0.10.1" }, "os": [ "!aix", "!sunos", "!freebsd", "!openbsd", ], "cpu": [ "x64", "arm64", ] }, "sha512-9oUCxypGbf2q3vNfKZ31wdzt5KqjhA9S6TwQaFol/j1lkSHVGvtIa3RvdGHPs1UUuvR/MO7b6pHj054BR9bXPQ=="], "@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="], diff --git a/package.json b/package.json index 7bb37f946f4c..2110c4d9af0c 100644 --- a/package.json +++ b/package.json @@ -155,7 +155,6 @@ "@types/node": "catalog:" }, "patchedDependencies": { - "@ff-labs/fff-bun@0.9.3": "patches/@ff-labs%2Ffff-bun@0.9.3.patch", "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", diff --git a/packages/core/package.json b/packages/core/package.json index cf54590d52e2..7756c08d7e04 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -91,7 +91,7 @@ "@effect/sql-sqlite-bun": "catalog:", "@lydell/node-pty": "catalog:", "@modelcontextprotocol/sdk": "1.29.0", - "@ff-labs/fff-bun": "0.9.4", + "@ff-labs/fff-bun": "0.10.1", "@opencode-ai/codemode": "workspace:*", "@opencode-ai/effect-drizzle-sqlite": "workspace:*", "@opencode-ai/effect-sqlite-node": "workspace:*", diff --git a/patches/@ff-labs%2Ffff-bun@0.9.3.patch b/patches/@ff-labs%2Ffff-bun@0.9.3.patch deleted file mode 100644 index 23a7dd54fb15..000000000000 --- a/patches/@ff-labs%2Ffff-bun@0.9.3.patch +++ /dev/null @@ -1,31 +0,0 @@ -diff --git a/src/download.ts b/src/download.ts -index 3454256..6dca25a 100644 ---- a/src/download.ts -+++ b/src/download.ts -@@ -7,7 +7,7 @@ - */ - -+declare const FFF_LIBC: "gnu" | "musl"; - import { existsSync } from "node:fs"; --import { createRequire } from "node:module"; - import { dirname, join } from "node:path"; - import { fileURLToPath } from "node:url"; - import { getLibFilename, getNpmPackageName } from "./platform"; -@@ -54,14 +54,10 @@ export function binaryExists(): boolean { - * in the same directory. - */ - function resolveFromNpmPackage(): string | null { -- const packageName = getNpmPackageName(); -- - try { -- // Use createRequire to resolve the platform package's location -- const require = createRequire(join(getPackageDir(), "package.json")); -- const packageJsonPath = require.resolve(`${packageName}/package.json`); -- const packageDir = dirname(packageJsonPath); -- const binaryPath = join(packageDir, getLibFilename()); -+ const binaryPath = require( -+ `@ff-labs/fff-bin-${process.platform === "linux" ? `linux-${process.arch}-${typeof FFF_LIBC === "string" ? FFF_LIBC : getNpmPackageName().endsWith("musl") ? "musl" : "gnu"}` : `${process.platform}-${process.arch}`}/${process.platform === "win32" ? "fff_c.dll" : process.platform === "darwin" ? "libfff_c.dylib" : "libfff_c.so"}`, -+ ); - - if (existsSync(binaryPath)) { - return binaryPath; From 360e7b412d64ac6ad8bd63415dbf68af0a47c60a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:37:16 -0400 Subject: [PATCH 056/150] feat(tui): expose debug settings (#38546) Co-authored-by: James Long --- packages/tui/src/component/dialog-config.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/tui/src/component/dialog-config.tsx b/packages/tui/src/component/dialog-config.tsx index 8ff9db50c700..1ca585ec1372 100644 --- a/packages/tui/src/component/dialog-config.tsx +++ b/packages/tui/src/component/dialog-config.tsx @@ -222,6 +222,14 @@ const settings: Setting[] = [ values: [false, true], labels: ["off", "on"], }, + { + title: "Turn token usage", + category: "Debug", + path: ["debug", "turn_tokens"], + default: false, + values: [false, true], + labels: ["off", "on"], + }, ] export function DialogConfig() { From 18fccac6ff8b6e27786869f2d03d86e1b6b1ac1a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:59:51 -0400 Subject: [PATCH 057/150] fix(tui): preserve first message in new sessions (#38542) Co-authored-by: Kit Langton Co-authored-by: James Long <17031+jlongster@users.noreply.github.com> --- packages/tui/src/context/data.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index ddf3823e756c..a5a18ff077f1 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -298,6 +298,12 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ case "session.created": result.session.invalidate(event.data.sessionID) void result.session.sync(event.data.sessionID) + // Band-aid: a newly created session starts empty, so live events can be its source of truth. + // Fetching pending inputs and projected messages separately lets promotion move an input between snapshots, + // causing both requests to miss it and overwrite event-built state. Skip those racy initial reads until + // hydration can load pending and projected messages atomically. + sync.complete(`session.pending:${event.data.sessionID}`) + sync.complete(`session.message:${event.data.sessionID}`) break case "session.deleted": removeSession(event.data.sessionID) From 2c814120c7f918237812510655fed067ccb902a3 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:52:20 -0500 Subject: [PATCH 058/150] fix(ai): keep tools when Anthropic tool_choice is none (#38553) --- .../ai/src/protocols/anthropic-messages.ts | 10 ++++--- .../test/provider/anthropic-messages.test.ts | 28 +++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index f5b9ea67d9f7..cc03dfa65b6a 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -159,7 +159,7 @@ const AnthropicTool = Schema.Struct({ type AnthropicTool = Schema.Schema.Type const AnthropicToolChoice = Schema.Union([ - Schema.Struct({ type: Schema.Literals(["auto", "any"]) }), + Schema.Struct({ type: Schema.Literals(["auto", "any", "none"]) }), Schema.Struct({ type: Schema.tag("tool"), name: Schema.String }), ]) @@ -297,7 +297,7 @@ const lowerTool = (breakpoints: Cache.Breakpoints, tool: ToolDefinition, inputSc const lowerToolChoice = (toolChoice: NonNullable) => ProviderShared.matchToolChoice("Anthropic Messages", toolChoice, { auto: () => ({ type: "auto" as const }), - none: () => undefined, + none: () => ({ type: "none" as const }), required: () => ({ type: "any" as const }), tool: (name) => ({ type: "tool" as const, name }), }) @@ -542,7 +542,6 @@ const outputConfig = (request: LLMRequest) => { } const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) { - const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined const generation = request.generation const toolSchemaCompatibility = request.model.compatibility?.toolSchema const outputLimit = request.model.defaults?.limits?.output ?? request.model.route.defaults.limits?.output ?? 4096 @@ -551,7 +550,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques // over-mark we keep their tool hints and shed the message-tail ones first. const breakpoints = Cache.newBreakpoints(ANTHROPIC_BREAKPOINT_CAP) const tools = - request.tools.length === 0 || request.toolChoice?.type === "none" + request.tools.length === 0 ? undefined : request.tools.map((tool) => lowerTool( @@ -560,6 +559,9 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility), ), ) + // Anthropic rejects tool_choice when tools are absent; "none" is only meaningful with tools present. + const toolChoice = + tools === undefined || !request.toolChoice ? undefined : yield* lowerToolChoice(request.toolChoice) const system = request.system.length === 0 ? undefined diff --git a/packages/ai/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts index 30b3c1b6b861..da8bb342876a 100644 --- a/packages/ai/test/provider/anthropic-messages.test.ts +++ b/packages/ai/test/provider/anthropic-messages.test.ts @@ -235,6 +235,34 @@ describe("Anthropic Messages route", () => { }), ) + it.effect("keeps tools and sends tool_choice none", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + id: "req_tool_choice_none", + model, + tools: [{ name: "lookup", description: "Look things up", inputSchema: { type: "object", properties: {} } }], + messages: [ + Message.user("What is the weather?"), + Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: { query: "weather" } })]), + Message.tool({ id: "call_1", name: "lookup", result: { forecast: "sunny" } }), + ], + toolChoice: "none", + cache: "none", + }), + ) + + expect(prepared.body.tools).toEqual([ + { + name: "lookup", + description: "Look things up", + input_schema: { type: "object", properties: {} }, + }, + ]) + expect(prepared.body.tool_choice).toEqual({ type: "none" }) + }), + ) + // Regression: read tool results must stay structured so base64 media data is // not JSON-stringified into `tool_result.content`. it.effect("lowers media tool-result content as structured blocks", () => From 2a9f8e3a2cb78d28f345e5f45f9ba1c222068e8f Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:58:02 +0000 Subject: [PATCH 059/150] fix(tui): manage focus in devtools panels (#38555) Co-authored-by: James Long <17031+jlongster@users.noreply.github.com> --- packages/tui/src/component/devtools-bar.tsx | 25 +++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/tui/src/component/devtools-bar.tsx b/packages/tui/src/component/devtools-bar.tsx index 03373110e226..45c031069c2b 100644 --- a/packages/tui/src/component/devtools-bar.tsx +++ b/packages/tui/src/component/devtools-bar.tsx @@ -1,4 +1,4 @@ -import { TextAttributes } from "@opentui/core" +import { TextAttributes, type Renderable } from "@opentui/core" import { TimeToFirstDraw, useRenderer, useTerminalDimensions } from "@opentui/solid" import { open } from "node:fs/promises" import { tmpdir } from "node:os" @@ -33,6 +33,7 @@ export function DevToolsBar() { const plugins = usePlugin() const theme = useTheme() const keymap = Keymap.use() + const renderer = useRenderer() const dimensions = useTerminalDimensions() const { themeV2, mode, supports, setMode } = theme const elevatedTheme = theme.contextual("elevated").themeV2 @@ -41,6 +42,7 @@ export function DevToolsBar() { const [dumpPath, setDumpPath] = createSignal() const [dumpError, setDumpError] = createSignal() const [frontendSamples, setFrontendSamples] = createSignal([]) + let focus: Renderable | null const connected = createMemo(() => client.connection.status() === "connected") const serverIndicator = createMemo(() => connectionIndicator(client.connection.status(), client.connection.attempt())) const themePerformance = createMemo( @@ -54,7 +56,22 @@ export function DevToolsBar() { address: info.urls[0] ? new URL(info.urls[0]).host : "Unknown", } }) - const toggle = (next: Panel) => setPanel((current) => (current === next ? undefined : next)) + const close = () => { + setPanel() + setTimeout(() => { + if (panel() || !focus || focus.isDestroyed) return + focus.focus() + focus = null + }, 1) + } + const toggle = (next: Panel) => { + if (panel() === next) return close() + if (!panel()) { + focus = renderer.currentFocusedRenderable + focus?.blur() + } + setPanel(next) + } const nextMode = () => (mode() === "dark" ? "light" : "dark") const canSwitchMode = () => supports(nextMode()) const runtime = createMemo(() => runtimeStatus(frontendSamples())) @@ -67,7 +84,7 @@ export function DevToolsBar() { if (!panel() || event.name !== "escape") return event.preventDefault() event.stopPropagation() - setPanel() + close() }, { priority: 10 }, ) @@ -205,7 +222,7 @@ export function DevToolsBar() { width={dimensions().width} height={Math.max(0, dimensions().height - 1)} backgroundColor="transparent" - onMouseUp={() => setPanel()} + onMouseUp={close} /> toggle("server")}> From 193f6be99ca3e70f8ca001a7eb4f66cf78383fe9 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 23 Jul 2026 14:09:25 -0500 Subject: [PATCH 060/150] fix(ai): keep tools when Gemini tool choice is none (#38556) --- packages/ai/src/protocols/gemini.ts | 6 +++--- packages/ai/test/provider/gemini.test.ts | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/ai/src/protocols/gemini.ts b/packages/ai/src/protocols/gemini.ts index 6cba2bc7f9b3..57ad06603721 100644 --- a/packages/ai/src/protocols/gemini.ts +++ b/packages/ai/src/protocols/gemini.ts @@ -313,7 +313,7 @@ const thinkingConfig = (request: LLMRequest) => { } const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMRequest) { - const toolsEnabled = request.tools.length > 0 && request.toolChoice?.type !== "none" + const hasTools = request.tools.length > 0 const generation = request.generation const toolSchemaCompatibility = request.model.compatibility?.toolSchema const generationConfig = { @@ -329,7 +329,7 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMReque contents: yield* lowerMessages(request), systemInstruction: request.system.length === 0 ? undefined : { parts: [{ text: ProviderShared.joinText(request.system) }] }, - tools: toolsEnabled + tools: hasTools ? [ { functionDeclarations: request.tools.map((tool) => @@ -338,7 +338,7 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMReque }, ] : undefined, - toolConfig: toolsEnabled && request.toolChoice ? yield* lowerToolConfig(request.toolChoice) : undefined, + toolConfig: hasTools && request.toolChoice ? yield* lowerToolConfig(request.toolChoice) : undefined, generationConfig: Object.values(generationConfig).some((value) => value !== undefined) ? generationConfig : undefined, diff --git a/packages/ai/test/provider/gemini.test.ts b/packages/ai/test/provider/gemini.test.ts index 5195d372c901..50b5dbb1d253 100644 --- a/packages/ai/test/provider/gemini.test.ts +++ b/packages/ai/test/provider/gemini.test.ts @@ -233,11 +233,11 @@ describe("Gemini route", () => { }), ) - it.effect("omits tools when tool choice is none", () => + it.effect("keeps tools and sends function calling mode NONE", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( LLM.request({ - id: "req_no_tools", + id: "req_tool_choice_none", model, prompt: "Say hello.", tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], @@ -245,8 +245,10 @@ describe("Gemini route", () => { }), ) - expect(prepared.body).toEqual({ + expect(prepared.body).toMatchObject({ contents: [{ role: "user", parts: [{ text: "Say hello." }] }], + tools: [{ functionDeclarations: [{ name: "lookup", description: "Lookup data" }] }], + toolConfig: { functionCallingConfig: { mode: "NONE" } }, }) }), ) From 8cac010bacc9ddd374c900d6a7c98aafc1113f1f Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 23 Jul 2026 14:12:23 -0500 Subject: [PATCH 061/150] fix(core): stop forcing toolChoice none on session.generate (#38557) --- packages/core/src/session/generate-node.ts | 1 - packages/core/test/session-generate.test.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/src/session/generate-node.ts b/packages/core/src/session/generate-node.ts index f26e2847c2f1..96804fc57f74 100644 --- a/packages/core/src/session/generate-node.ts +++ b/packages/core/src/session/generate-node.ts @@ -74,7 +74,6 @@ export const layer = Layer.effect( system: contextEvent.system, messages: contextEvent.messages, tools: hookedTools, - toolChoice: "none", }), ) yield* Effect.logInfo("session generation usage diagnostic", { usage: response.usage }) diff --git a/packages/core/test/session-generate.test.ts b/packages/core/test/session-generate.test.ts index 98e7aedc0c7c..e78f41bdac0f 100644 --- a/packages/core/test/session-generate.test.ts +++ b/packages/core/test/session-generate.test.ts @@ -301,7 +301,7 @@ it.effect("generates from fresh settled Session context without durable mutation ), ).toEqual(["Settled partial answer"]) expect(requests[0]?.tools).toMatchObject([{ name: "lookup", description: "Hooked lookup" }]) - expect(requests[0]?.toolChoice).toMatchObject({ type: "none" }) + expect(requests[0]?.toolChoice).toBeUndefined() expect(yield* durableState(db, sessionID)).toEqual(before) }), ) From 79c1544072739f58d2e80f8c3e83b203aa92d827 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 23 Jul 2026 17:13:31 -0400 Subject: [PATCH 062/150] refactor(tools): unify tool APIs and result handling (#38367) --- .changeset/canonical-tool-results.md | 8 + .../ai/src/protocols/anthropic-messages.ts | 9 +- packages/ai/src/protocols/bedrock-converse.ts | 9 +- .../test/provider/anthropic-messages.test.ts | 9 +- .../ai/test/provider/bedrock-converse.test.ts | 30 ++ packages/cli/src/acp/event.ts | 23 +- packages/cli/src/acp/permission.ts | 6 +- packages/cli/src/acp/tool.ts | 37 +- .../cli/src/node/plugin-runtime.promise.ts | 2 + packages/cli/src/run/noninteractive.ts | 48 +-- packages/cli/test/acp/event-behavior.test.ts | 29 +- .../cli/test/acp/permission-behavior.test.ts | 6 +- packages/cli/test/acp/tool.test.ts | 63 +-- packages/cli/test/import-boundaries.test.ts | 7 +- packages/cli/test/run/noninteractive.test.ts | 25 +- packages/cli/vite.node.config.ts | 12 +- .../client/src/promise/generated/types.ts | 59 ++- packages/codemode/README.md | 8 +- packages/codemode/interpreter-support.md | 2 +- packages/codemode/src/interpreter/execute.ts | 2 +- packages/codemode/src/interpreter/runtime.ts | 13 +- packages/codemode/src/openapi/index.ts | 10 +- packages/codemode/src/openapi/types.ts | 4 +- packages/codemode/src/tool-runtime.ts | 72 ++-- packages/codemode/src/tool-schema.ts | 50 +-- packages/codemode/src/tool.ts | 24 +- packages/codemode/src/tools.ts | 4 +- packages/codemode/test/callbacks.test.ts | 2 +- packages/codemode/test/codemode.test.ts | 74 ++-- packages/codemode/test/enumeration.test.ts | 2 +- packages/codemode/test/openapi.test.ts | 149 ++++--- packages/codemode/test/promise.test.ts | 16 +- packages/codemode/test/signature.test.ts | 14 +- packages/codemode/test/stdlib.test.ts | 4 +- packages/codemode/test/tool-paths.test.ts | 10 +- packages/core/src/codemode.ts | 39 +- packages/core/src/database/migration.gen.ts | 1 + .../20260722170000_canonical_tool_results.ts | 123 ++++++ packages/core/src/plugin/host.ts | 42 +- packages/core/src/plugin/promise.ts | 22 +- packages/core/src/session/generate-node.ts | 9 +- packages/core/src/session/message-updater.ts | 18 +- packages/core/src/session/model-request.ts | 43 +- packages/core/src/session/runner/llm.ts | 25 +- .../src/session/runner/publish-llm-event.ts | 103 +++-- .../core/src/session/runner/to-llm-message.ts | 40 +- packages/core/src/session/to-session-error.ts | 9 +- packages/core/src/tool-output-store.ts | 36 +- packages/core/src/tool/AGENTS.md | 18 +- packages/core/src/tool/edit.ts | 18 +- packages/core/src/tool/execute.ts | 101 +++-- packages/core/src/tool/glob.ts | 22 +- packages/core/src/tool/grep.ts | 28 +- packages/core/src/tool/hooks.ts | 27 +- packages/core/src/tool/mcp.ts | 146 +++---- packages/core/src/tool/patch.ts | 17 +- packages/core/src/tool/question.ts | 10 +- packages/core/src/tool/read.ts | 28 +- packages/core/src/tool/registry.ts | 276 +++++++------ packages/core/src/tool/shell.ts | 62 +-- packages/core/src/tool/skill.ts | 16 +- packages/core/src/tool/subagent.ts | 18 +- packages/core/src/tool/tool.ts | 90 +++- packages/core/src/tool/tools.ts | 4 +- packages/core/src/tool/webfetch.ts | 10 +- packages/core/src/tool/websearch.ts | 10 +- packages/core/src/tool/write.ts | 9 +- packages/core/test/codemode.test.ts | 16 +- packages/core/test/database-migration.test.ts | 203 +++++++++ packages/core/test/lib/tool.ts | 11 +- packages/core/test/mcp.test.ts | 94 ++++- packages/core/test/plugin.test.ts | 53 ++- packages/core/test/plugin/promise.test.ts | 41 +- packages/core/test/session-generate.test.ts | 4 +- .../core/test/session-instructions.test.ts | 14 +- .../core/test/session-runner-message.test.ts | 41 +- .../test/session-runner-tool-events.test.ts | 90 ++-- .../test/session-runner-tool-registry.test.ts | 330 ++++++++------- packages/core/test/session-runner.test.ts | 386 ++++++++++-------- .../core/test/session-tool-progress.test.ts | 20 +- packages/core/test/tool-edit.test.ts | 86 ++-- packages/core/test/tool-execute.test.ts | 116 ++++-- packages/core/test/tool-output-store.test.ts | 76 +--- packages/core/test/tool-patch.test.ts | 171 ++++---- packages/core/test/tool-question.test.ts | 31 +- packages/core/test/tool-read.test.ts | 145 ++++--- packages/core/test/tool-search.test.ts | 25 +- packages/core/test/tool-shell.test.ts | 120 +++--- packages/core/test/tool-skill.test.ts | 34 +- packages/core/test/tool-subagent.test.ts | 72 ++-- packages/core/test/tool-webfetch.test.ts | 83 ++-- packages/core/test/tool-websearch.test.ts | 32 +- packages/core/test/tool-write.test.ts | 58 +-- packages/docs/build/plugins.mdx | 75 ++-- .../plugin/src/v2/effect/internal/tool.ts | 315 ++++++++++++++ packages/plugin/src/v2/effect/tool.ts | 316 +------------- packages/plugin/src/v2/promise/README.md | 20 +- .../plugin/src/v2/promise/internal/tool.ts | 64 +++ packages/plugin/src/v2/promise/tool.ts | 50 +-- packages/plugin/test/tool.test.ts | 69 ++-- packages/schema/src/session-event.ts | 29 +- packages/schema/src/session-message.ts | 13 +- packages/schema/test/event-manifest.test.ts | 4 +- packages/sdk-next/src/tool.ts | 2 +- packages/sdk-next/test/embedded.test.ts | 6 +- .../src/backend/simulated-provider.ts | 24 +- packages/simulation/src/protocol/index.ts | 5 +- packages/simulation/test/protocol.test.ts | 2 +- .../test/simulated-provider.test.ts | 110 ++--- packages/tui/src/context/data.tsx | 14 +- packages/tui/src/mini/demo.ts | 13 +- packages/tui/src/mini/permission.shared.ts | 2 +- packages/tui/src/mini/stream-v2.subagent.ts | 45 +- packages/tui/src/mini/stream-v2.transport.ts | 20 +- packages/tui/src/mini/stream.ts | 2 +- packages/tui/src/mini/tool.public.ts | 3 +- packages/tui/src/mini/tool.ts | 49 ++- packages/tui/src/routes/session/index.tsx | 80 +++- .../tui/src/routes/session/permission.tsx | 10 +- packages/tui/src/util/permission.ts | 4 +- packages/tui/src/util/tool-display.ts | 18 +- packages/tui/test/cli/tui/data.test.tsx | 11 +- packages/tui/test/mini/entry.body.test.ts | 41 +- .../tui/test/mini/permission.shared.test.ts | 9 +- .../tui/test/mini/scrollback.surface.test.ts | 30 +- .../tui/test/mini/stream-v2.transport.test.ts | 82 ++-- packages/tui/test/mini/tool.test.ts | 40 +- packages/tui/test/util/tool-display.test.ts | 16 +- packages/www/content/docs/build/plugins.mdx | 73 ++-- specs/v2/README.md | 2 +- specs/v2/schema-changelog.md | 15 +- specs/v2/session.md | 4 +- specs/v2/tools.md | 71 ++-- 133 files changed, 3602 insertions(+), 2770 deletions(-) create mode 100644 .changeset/canonical-tool-results.md create mode 100644 packages/core/src/database/migration/20260722170000_canonical_tool_results.ts create mode 100644 packages/plugin/src/v2/effect/internal/tool.ts create mode 100644 packages/plugin/src/v2/promise/internal/tool.ts diff --git a/.changeset/canonical-tool-results.md b/.changeset/canonical-tool-results.md new file mode 100644 index 000000000000..b484966033d4 --- /dev/null +++ b/.changeset/canonical-tool-results.md @@ -0,0 +1,8 @@ +--- +"@opencode-ai/plugin": minor +"@opencode-ai/sdk": minor +"@opencode-ai/client": minor +"@opencode-ai/protocol": minor +--- + +Replace the V2 tool result model with one canonical representation per fact. Tools lose `structured`, projection callbacks, the `Structured` generic, and the exported `Tool.settle` interpreter; tool responses carry schema-validated `output`, model-visible `content`, and optional compact JSON `metadata`. Code Mode receives the validated encoded output. Durable tool success stores non-empty model content plus optional metadata; failure stores one error plus the final bounded partial snapshot. Progress carries metadata only, while `execute.after` hooks receive the canonical terminal outcome and managed `outputPaths`. A one-time migration rewrites existing projected tool rows and moves provider-hosted result payloads into provider-owned result state. diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index cc03dfa65b6a..fc4ce9ab5393 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -330,7 +330,10 @@ const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult const wireType = serverToolResultType(part.name) if (!wireType) return yield* invalid(`Anthropic Messages does not know how to round-trip server tool result for ${part.name}`) - return { type: wireType, tool_use_id: part.id, content: part.result.value } satisfies AnthropicServerToolResultBlock + // Prefer the provider-owned replay payload; fall back to the result value for + // histories constructed directly from provider events. + const payload = part.providerMetadata?.anthropic?.["result"] ?? part.result.value + return { type: wireType, tool_use_id: part.id, content: payload } satisfies AnthropicServerToolResultBlock }) const lowerMedia = Effect.fn("AnthropicMessages.lowerMedia")(function* (part: MediaPart) { @@ -682,7 +685,9 @@ const serverToolResultEvent = (block: NonNullable 0 && request.toolChoice?.type !== "none" - ? { tools: lowerTools(request.model.compatibility?.toolSchema, breakpoints, request.tools), toolChoice } + request.tools.length > 0 + ? { + tools: lowerTools(request.model.compatibility?.toolSchema, breakpoints, request.tools), + // Converse has no native "none". Keep definitions stable for prompt + // caching and omit only the unsupported choice. + toolChoice, + } : undefined const system = request.system.length === 0 ? undefined : lowerSystem(breakpoints, request.system) const messages = yield* lowerMessages(request, breakpoints) diff --git a/packages/ai/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts index da8bb342876a..0b319f573b87 100644 --- a/packages/ai/test/provider/anthropic-messages.test.ts +++ b/packages/ai/test/provider/anthropic-messages.test.ts @@ -664,7 +664,14 @@ describe("Anthropic Messages route", () => { name: "web_search", result: { type: "json", value: [{ type: "web_search_result", url: "https://example.com", title: "Example" }] }, providerExecuted: true, - providerMetadata: { anthropic: { blockType: "web_search_tool_result" } }, + // The complete payload rides in provider metadata as irreducible replay + // state for later stateless requests. + providerMetadata: { + anthropic: { + blockType: "web_search_tool_result", + result: [{ type: "web_search_result", url: "https://example.com", title: "Example" }], + }, + }, }) expect(response.text).toBe("Found it.") expect(response.events.at(-1)).toMatchObject({ type: "finish", reason: "stop" }) diff --git a/packages/ai/test/provider/bedrock-converse.test.ts b/packages/ai/test/provider/bedrock-converse.test.ts index 776032966c84..a7280e09cd9f 100644 --- a/packages/ai/test/provider/bedrock-converse.test.ts +++ b/packages/ai/test/provider/bedrock-converse.test.ts @@ -154,6 +154,36 @@ describe("Bedrock Converse route", () => { }), ) + it.effect("keeps tools and omits the unsupported choice when tool choice is none", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.updateRequest(baseRequest, { + tools: [ + { + name: "lookup", + description: "Lookup data", + inputSchema: { type: "object", properties: { query: { type: "string" } } }, + }, + ], + toolChoice: ToolChoice.make({ type: "none" }), + }), + ) + + expect(prepared.body.toolConfig).toMatchObject({ + tools: [ + { + toolSpec: { + name: "lookup", + description: "Lookup data", + inputSchema: { json: { type: "object", properties: { query: { type: "string" } } } }, + }, + }, + ], + }) + expect(prepared.body.toolConfig?.toolChoice).toBeUndefined() + }), + ) + it.effect("lowers assistant tool-call + tool-result message history", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( diff --git a/packages/cli/src/acp/event.ts b/packages/cli/src/acp/event.ts index 15513538300c..187b660141ca 100644 --- a/packages/cli/src/acp/event.ts +++ b/packages/cli/src/acp/event.ts @@ -28,7 +28,7 @@ export type TurnControl = { type ToolState = { readonly name: string input: ToolInput - structured: Record + metadata: Record content: ToolContent } @@ -38,7 +38,7 @@ export type TurnStart = | { readonly type: "compaction"; readonly id: string } function emptyToolState(): ToolState { - return { name: "tool", input: {}, structured: {}, content: [] } + return { name: "tool", input: {}, metadata: {}, content: [] } } export async function streamTurn(input: { @@ -120,7 +120,7 @@ export async function streamTurn(input: { } if (event.type === "session.tool.input.started") { assistantMessageID = event.data.assistantMessageID - tools.set(event.data.callID, { name: event.data.name, input: {}, structured: {}, content: [] }) + tools.set(event.data.callID, { name: event.data.name, input: {}, metadata: {}, content: [] }) await update({ sessionUpdate: "tool_call", ...pendingToolCall({ @@ -151,15 +151,13 @@ export async function streamTurn(input: { if (event.type === "session.tool.progress") { const current = tools.get(event.data.callID) if (!current) continue - current.structured = event.data.structured - current.content = event.data.content + current.metadata = event.data.metadata await update({ sessionUpdate: "tool_call_update", ...runningToolUpdate({ toolCallId: event.data.callID, toolName: current.name, state: { input: current.input }, - content: current.content, cwd: input.cwd, }), }) @@ -175,7 +173,7 @@ export async function streamTurn(input: { cwd: input.cwd, toolName: current.name, toolInput: current.input, - structured: event.data.structured, + metadata: event.data.metadata ?? {}, }).catch(() => {}) await update({ sessionUpdate: "tool_call_update", @@ -183,9 +181,8 @@ export async function streamTurn(input: { toolCallId: event.data.callID, toolName: current.name, input: current.input, - structured: event.data.structured, + metadata: event.data.metadata, content: event.data.content, - result: event.data.result, }), }) continue @@ -199,7 +196,7 @@ export async function streamTurn(input: { toolCallId: event.data.callID, toolName: current.name, input: current.input, - structured: event.data.metadata ?? current.structured, + metadata: event.data.metadata ?? current.metadata, content: event.data.content ?? current.content, error: event.data.error.message, cwd: input.cwd, @@ -342,9 +339,8 @@ async function replayMessage( toolCallId: part.id, toolName: part.name, input: part.state.input, - structured: part.state.structured, + metadata: part.state.metadata, content: part.state.content, - result: part.state.result, }), }, }) @@ -358,7 +354,6 @@ async function replayMessage( toolCallId: part.id, toolName: part.name, state: { input: part.state.input }, - content: part.state.content, cwd, }), }, @@ -373,7 +368,7 @@ async function replayMessage( toolCallId: part.id, toolName: part.name, input: part.state.input, - structured: part.state.structured, + metadata: part.state.metadata, content: part.state.content, error: part.state.error.message, cwd, diff --git a/packages/cli/src/acp/permission.ts b/packages/cli/src/acp/permission.ts index 3c1d92957e42..87f6e68d06b1 100644 --- a/packages/cli/src/acp/permission.ts +++ b/packages/cli/src/acp/permission.ts @@ -58,11 +58,11 @@ export async function syncEditedFiles(input: { readonly cwd: string readonly toolName: string readonly toolInput: ToolInput - readonly structured: Readonly> + readonly metadata: Readonly> }) { if (!input.writeTextFile || !input.connection.writeTextFile || toToolKind(input.toolName) !== "edit") return - const files = Array.isArray(input.structured.files) - ? input.structured.files.flatMap((file): string[] => { + const files = Array.isArray(input.metadata.files) + ? input.metadata.files.flatMap((file): string[] => { if (!file || typeof file !== "object") return [] const path = Reflect.get(file, "file") return typeof path === "string" ? [path] : [] diff --git a/packages/cli/src/acp/tool.ts b/packages/cli/src/acp/tool.ts index 468512f892b7..8937712f11b5 100644 --- a/packages/cli/src/acp/tool.ts +++ b/packages/cli/src/acp/tool.ts @@ -1,5 +1,6 @@ import { isAbsolute, resolve } from "node:path" import type { ToolCall, ToolCallContent, ToolCallLocation, ToolCallUpdate, ToolKind } from "@agentclientprotocol/sdk" +import { readDisplayText } from "@opencode-ai/tui/mini/tool" export type ToolInput = Record export type ToolContent = ReadonlyArray< @@ -100,11 +101,12 @@ export function completedToolUpdate(input: { readonly toolName: string readonly input: ToolInput readonly content: ToolContent - readonly structured: Readonly> - readonly result?: unknown + readonly metadata?: Readonly> }): ToolCallUpdate { const normalized = toolContent(input.content) - const read = input.toolName.toLocaleLowerCase() === "read" ? readDisplayText(input.structured) : undefined + // Read's model content is a JSON page envelope; show the clean text instead. + const firstText = input.content.find((part) => part.type === "text") + const read = input.toolName.toLocaleLowerCase() === "read" && firstText ? readDisplayText(firstText.text) : undefined const images = normalized.filter((part) => part.type === "content" && part.content.type === "image") const primary = read === undefined @@ -128,8 +130,7 @@ export function completedToolUpdate(input: { status: "completed", content: [...primary, ...diff, ...images], rawOutput: { - structured: input.structured, - ...(input.result === undefined ? {} : { result: input.result }), + ...(input.metadata === undefined ? {} : { metadata: input.metadata }), }, } } @@ -138,8 +139,8 @@ export function errorToolUpdate(input: { readonly toolCallId: string readonly toolName: string readonly input: ToolInput - readonly content: ToolContent - readonly structured: Readonly> + readonly content?: ToolContent + readonly metadata?: Readonly> readonly error: string readonly cwd?: string }): ToolCallUpdate { @@ -150,8 +151,11 @@ export function errorToolUpdate(input: { title: toolTitle(input.toolName, input.input, undefined), locations: toLocations(input.toolName, input.input, input.cwd), rawInput: rawInput(input.toolName, input.input, input.cwd), - content: [...toolContent(input.content), { type: "content", content: { type: "text", text: input.error } }], - rawOutput: { structured: input.structured, error: input.error }, + content: [...toolContent(input.content ?? []), { type: "content", content: { type: "text", text: input.error } }], + rawOutput: { + ...(input.metadata === undefined ? {} : { metadata: input.metadata }), + error: input.error, + }, } } @@ -164,21 +168,6 @@ function toolContent(content: ToolContent): ToolCallContent[] { }) } -function readDisplayText(structured: Readonly>) { - if (typeof structured.content === "string") { - if (structured.type === "text-page" || structured.encoding === "utf8") return structured.content - } - if (!Array.isArray(structured.entries)) return undefined - return structured.entries - .flatMap((entry): string[] => { - if (typeof entry === "string") return [entry] - if (!entry || typeof entry !== "object") return [] - const path = Reflect.get(entry, "path") - return typeof path === "string" ? [path] : [] - }) - .join("\n") -} - function toolTitle(toolName: string, input: ToolInput, fallback: string | undefined) { if (isShell(toolName)) return stringValue(input.command) ?? stringValue(input.cmd) ?? fallback ?? toolName return fallback || toolName diff --git a/packages/cli/src/node/plugin-runtime.promise.ts b/packages/cli/src/node/plugin-runtime.promise.ts index 93e20b87bb1c..49c46512328d 100644 --- a/packages/cli/src/node/plugin-runtime.promise.ts +++ b/packages/cli/src/node/plugin-runtime.promise.ts @@ -10,6 +10,7 @@ import { Reference, Skill, } from "@opencode-ai/plugin/v2" +import { Tool } from "@opencode-ai/plugin/v2/tool" const key = Symbol.for("opencode.plugin.v2.promise") ;(globalThis as typeof globalThis & { [key]?: unknown })[key] = { @@ -23,4 +24,5 @@ const key = Symbol.for("opencode.plugin.v2.promise") Provider, Reference, Skill, + Tool, } diff --git a/packages/cli/src/run/noninteractive.ts b/packages/cli/src/run/noninteractive.ts index 41859d3b64fc..824d228c6a52 100644 --- a/packages/cli/src/run/noninteractive.ts +++ b/packages/cli/src/run/noninteractive.ts @@ -10,7 +10,7 @@ import type { import { SessionMessage } from "@opencode-ai/schema/session-message" import { EOL } from "node:os" import { readFile } from "node:fs/promises" -import { toolOutputText, type MiniToolPart } from "@opencode-ai/tui/mini/tool" +import { nonEmptyToolContent, toolOutputText, type MiniToolPart } from "@opencode-ai/tui/mini/tool" import { UI } from "./ui" type Model = { @@ -55,7 +55,7 @@ type ToolState = StartedPart & { raw?: string provider?: unknown providerState?: SessionMessageAssistantTool["providerState"] - structured: Record + metadata: Record content: LLMToolContent[] } @@ -306,7 +306,7 @@ export async function runNonInteractivePrompt(input: Input) { assistantMessageID: event.data.assistantMessageID, tool: event.data.name, input: {}, - structured: {}, + metadata: {}, content: [], }) continue @@ -334,7 +334,7 @@ export async function runNonInteractivePrompt(input: Input) { raw: current?.raw, provider: { executed: event.data.executed, state: event.data.state }, providerState: event.data.state, - structured: {}, + metadata: {}, content: [], }) continue @@ -342,8 +342,7 @@ export async function runNonInteractivePrompt(input: Input) { if (event.type === "session.tool.progress") { const current = tools.get(toolKey(event.data.assistantMessageID, event.data.callID)) if (current) { - current.structured = event.data.structured - current.content = event.data.content + current.metadata = event.data.metadata } continue } @@ -360,9 +359,8 @@ export async function runNonInteractivePrompt(input: Input) { state: { status: "completed", input: current.input, - structured: event.data.structured, + metadata: event.data.metadata, content: event.data.content, - result: event.data.result, }, time: { created: current.timestamp, ran: current.timestamp, completed: time }, } @@ -379,9 +377,8 @@ export async function runNonInteractivePrompt(input: Input) { output: toolOutputText(current.tool, event.data.content), title: current.tool, metadata: { - structured: event.data.structured, + metadata: event.data.metadata, content: event.data.content, - result: event.data.result, providerCall: current.provider, providerResult: { executed: event.data.executed, state: event.data.resultState }, rawInput: current.raw, @@ -398,8 +395,8 @@ export async function runNonInteractivePrompt(input: Input) { const key = toolKey(event.data.assistantMessageID, event.data.callID) const current = tools.get(key) ?? fallbackTool(event) const error = event.data.error.message - const structured = event.data.metadata ?? current.structured - const content = event.data.content ?? current.content + const metadata = event.data.metadata ?? current.metadata + const content = event.data.content ?? nonEmptyToolContent(current.content) const tool: SessionMessageAssistantTool = { type: "tool", id: event.data.callID, @@ -410,10 +407,9 @@ export async function runNonInteractivePrompt(input: Input) { state: { status: "error", input: current.input, - structured, + metadata, content, error: event.data.error, - result: event.data.result, }, time: { created: current.timestamp, ran: current.timestamp, completed: time }, } @@ -429,7 +425,6 @@ export async function runNonInteractivePrompt(input: Input) { input: current.input, error, metadata: { - result: event.data.result, providerCall: current.provider, providerResult: { executed: event.data.executed, state: event.data.resultState }, rawInput: current.raw, @@ -441,15 +436,14 @@ export async function runNonInteractivePrompt(input: Input) { renderedTools.add(key) if (input.compatibility === "v1" && (permissionRejected || formCancelled)) continue if (!emit("tool_use", time, { part })) { - if (toolOutputText(current.tool, content).trim()) + if (content && toolOutputText(current.tool, content).trim()) await input.renderTool({ ...tool, state: { status: "completed", input: current.input, - structured, + metadata, content, - result: event.data.result, }, }) await input.renderToolError(tool) @@ -597,14 +591,14 @@ export async function runNonInteractivePrompt(input: Input) { input: item.state.input, output: toolOutputText(item.name, item.state.content), title: item.name, - metadata: { structured: item.state.structured, content: item.state.content, result: item.state.result }, + metadata: { metadata: item.state.metadata, content: item.state.content }, time: { start: item.time.ran ?? item.time.created, end: item.time.completed ?? timestamp }, } : { status: "error", input: item.state.input, error: item.state.error.message, - metadata: { structured: item.state.structured, content: item.state.content, result: item.state.result }, + metadata: { metadata: item.state.metadata, content: item.state.content }, time: { start: item.time.ran ?? item.time.created, end: item.time.completed ?? timestamp }, }, } @@ -614,8 +608,16 @@ export async function runNonInteractivePrompt(input: Input) { await input.renderTool(item) continue } - if (toolOutputText(item.name, item.state.content).trim()) { - await input.renderTool({ ...item, state: { ...item.state, status: "completed" } }) + if (item.state.content && toolOutputText(item.name, item.state.content).trim()) { + await input.renderTool({ + ...item, + state: { + status: "completed", + input: item.state.input, + metadata: item.state.metadata, + content: item.state.content, + }, + }) } await input.renderToolError(item) UI.error(item.state.error.message) @@ -792,7 +794,7 @@ function fallbackTool(event: { assistantMessageID: event.data.assistantMessageID, tool: "tool", input: {}, - structured: {}, + metadata: {}, content: [], } } diff --git a/packages/cli/test/acp/event-behavior.test.ts b/packages/cli/test/acp/event-behavior.test.ts index ded7b7020aa3..42be5c452061 100644 --- a/packages/cli/test/acp/event-behavior.test.ts +++ b/packages/cli/test/acp/event-behavior.test.ts @@ -218,8 +218,7 @@ describe("acp event behavior", () => { sessionID: "ses_tools", assistantMessageID: "msg_tools", callID: "call_ok", - structured: { phase: 1 }, - content: [{ type: "text", text: "working" }], + metadata: { phase: 1 }, }), ) send( @@ -227,9 +226,8 @@ describe("acp event behavior", () => { sessionID: "ses_tools", assistantMessageID: "msg_tools", callID: "call_ok", - structured: { exit: 0 }, + metadata: { exit: 0 }, content: [{ type: "text", text: "done" }], - result: { code: 0 }, executed: true, }), ) @@ -255,8 +253,7 @@ describe("acp event behavior", () => { sessionID: "ses_tools", assistantMessageID: "msg_tools", callID: "call_fail", - structured: { bytes: 0 }, - content: [{ type: "text", text: "opening" }], + metadata: { bytes: 0 }, }), ) send( @@ -313,12 +310,10 @@ describe("acp event behavior", () => { locations: [{ path: resolve("/workspace", "sub") }], rawInput: { command: "printf done", workdir: "sub" }, }) - expect(updates[2]?.update).toMatchObject({ - content: [{ type: "content", content: { type: "text", text: "working" } }], - }) + expect(updates[2]?.update).not.toHaveProperty("content") expect(updates[3]?.update).toMatchObject({ content: [{ type: "content", content: { type: "text", text: "done" } }], - rawOutput: { structured: { exit: 0 }, result: { code: 0 } }, + rawOutput: { metadata: { exit: 0 } }, }) expect(updates[7]?.update).toMatchObject({ kind: "read", @@ -327,7 +322,7 @@ describe("acp event behavior", () => { { type: "content", content: { type: "text", text: "opening" } }, { type: "content", content: { type: "text", text: "not found" } }, ], - rawOutput: { structured: { bytes: 0 }, error: "not found" }, + rawOutput: { metadata: { bytes: 0 }, error: "not found" }, }) expect(response.stopReason).toBe("end_turn") } finally { @@ -379,7 +374,7 @@ describe("acp event behavior", () => { { type: "content", content: { type: "text", text: "done" } }, { type: "content", content: { type: "image", mimeType: "image/png", data: "AAAA" } }, ], - rawOutput: { structured: { exit: 0 }, result: { code: 0 } }, + rawOutput: { metadata: { exit: 0 } }, }) expect(updates[8]?.update).toMatchObject({ toolCallId: "call_running", @@ -618,12 +613,11 @@ function replayFixtureMessages(): SessionMessageInfo[] { state: { status: "completed", input: { command: "printf done" }, - structured: { exit: 0 }, + metadata: { exit: 0 }, content: [ { type: "text", text: "done" }, { type: "file", uri: "data:image/png;base64,AAAA", mime: "image/png", name: "image.png" }, ], - result: { code: 0 }, }, }, { @@ -634,8 +628,7 @@ function replayFixtureMessages(): SessionMessageInfo[] { state: { status: "running", input: { command: "pwd" }, - structured: {}, - content: [{ type: "text", text: "/workspace" }], + metadata: {}, }, }, { @@ -646,7 +639,7 @@ function replayFixtureMessages(): SessionMessageInfo[] { state: { status: "error", input: { filePath: "/workspace/missing.ts" }, - structured: { bytes: 0 }, + metadata: { bytes: 0 }, content: [{ type: "text", text: "partial" }], error: { type: "tool.error", message: "failed hard" }, }, @@ -679,7 +672,7 @@ function replayToolMessage(id: string) { state: { status: "completed", input: { command: "printf done" }, - structured: { exit: 0 }, + metadata: { exit: 0 }, content: [{ type: "text", text: "done" }], }, }, diff --git a/packages/cli/test/acp/permission-behavior.test.ts b/packages/cli/test/acp/permission-behavior.test.ts index 355ff5bb410b..df7de586fddc 100644 --- a/packages/cli/test/acp/permission-behavior.test.ts +++ b/packages/cli/test/acp/permission-behavior.test.ts @@ -28,7 +28,7 @@ describe("acp permission behavior", () => { cwd: "/workspace", toolName: "edit", toolInput: { filePath: "/workspace/file.ts" }, - structured: {}, + metadata: {}, }) expect(writes).toEqual([]) @@ -193,7 +193,7 @@ describe("acp permission behavior", () => { sessionID: "ses_edit", assistantMessageID: "msg_edit", callID: "call_edit", - structured: { files: [{ file: "file.ts" }], replacements: 1 }, + metadata: { files: [{ file: "file.ts" }], replacements: 1 }, content: [{ type: "text", text: "edited" }], executed: true, }), @@ -286,7 +286,7 @@ describe("acp permission behavior", () => { sessionID: "ses_patch", assistantMessageID: "msg_patch", callID: "call_patch", - structured: { files: [{ file: "first.ts" }, { file: "second.ts" }] }, + metadata: { files: [{ file: "first.ts" }, { file: "second.ts" }] }, content: [{ type: "text", text: "patched" }], executed: true, }), diff --git a/packages/cli/test/acp/tool.test.ts b/packages/cli/test/acp/tool.test.ts index da90fb7c053b..0095b57ce1cb 100644 --- a/packages/cli/test/acp/tool.test.ts +++ b/packages/cli/test/acp/tool.test.ts @@ -63,7 +63,7 @@ describe("acp tools", () => { { type: "file", mime: "image/png", name: "image.png", uri: `data:image/png;base64,${image}` }, { type: "file", mime: "text/plain", name: "note.txt", uri: "data:text/plain;base64,bm90ZQ==" }, ], - structured: {}, + metadata: {}, }).content, ).toEqual([ { @@ -93,7 +93,7 @@ describe("acp tools", () => { content: "created", }, content: [{ type: "text", text: "wrote /tmp/file.ts" }], - structured: {}, + metadata: {}, }).content, ).toEqual([ { @@ -103,20 +103,22 @@ describe("acp tools", () => { ]) }) - test("uses clean structured read content instead of model-facing formatting", () => { + test("unwraps read's JSON page envelope instead of showing model-facing formatting", () => { expect( completedToolUpdate({ toolCallId: "tool-read", toolName: "read", input: { path: "/tmp/file.ts" }, - content: [{ type: "text", text: "1: first\n2: second" }], - structured: { - type: "text-page", - content: "first\nsecond", - mime: "text/plain", - offset: 1, - truncated: false, - }, + content: [ + { + type: "text", + text: JSON.stringify( + { type: "text-page", content: "first\nsecond", mime: "text/plain", offset: 1, truncated: false }, + null, + 2, + ), + }, + ], }).content, ).toEqual([{ type: "content", content: { type: "text", text: "first\nsecond" } }]) @@ -125,13 +127,17 @@ describe("acp tools", () => { toolCallId: "tool-list", toolName: "read", input: { path: "/tmp" }, - content: [], - structured: { - entries: [ - { path: "a.ts", type: "file" }, - { path: "src", type: "directory" }, - ], - }, + content: [ + { + type: "text", + text: JSON.stringify({ + entries: [ + { path: "a.ts", type: "file" }, + { path: "src", type: "directory" }, + ], + }), + }, + ], }).content, ).toEqual([{ type: "content", content: { type: "text", text: "a.ts\nsrc" } }]) }) @@ -171,7 +177,7 @@ describe("acp tools", () => { newString: "after", }, content: [{ type: "text", text: "Edit applied successfully." }], - structured: { output: "Edit applied successfully." }, + metadata: { output: "Edit applied successfully." }, }), ).toEqual({ toolCallId: "tool-1", @@ -189,7 +195,7 @@ describe("acp tools", () => { }, ], rawOutput: { - structured: { output: "Edit applied successfully." }, + metadata: { output: "Edit applied successfully." }, }, }) }) @@ -209,7 +215,7 @@ describe("acp tools", () => { }) }) - test("builds completed raw output with structured data and optional result", () => { + test("builds completed raw output with optional metadata", () => { const attachments = [ { type: "file", @@ -225,12 +231,10 @@ describe("acp tools", () => { toolName: "read", input: {}, content: [], - structured: { output: "done", metadata: { exit: 0 }, attachments }, - result: "done", + metadata: { output: "done", metadata: { exit: 0 }, attachments }, }).rawOutput, ).toEqual({ - structured: { output: "done", metadata: { exit: 0 }, attachments }, - result: "done", + metadata: { output: "done", metadata: { exit: 0 }, attachments }, }) expect( @@ -239,9 +243,8 @@ describe("acp tools", () => { toolName: "read", input: {}, content: [], - structured: { output: "done" }, }).rawOutput, - ).toEqual({ structured: { output: "done" } }) + ).toEqual({}) }) test("extracts image attachments only from data URLs", () => { @@ -255,7 +258,7 @@ describe("acp tools", () => { { type: "file", mime: "image/png", uri: "https://example.com/image.png" }, { type: "file", mime: "text/plain", uri: "data:text/plain;base64,BBBB" }, ], - structured: {}, + metadata: {}, }).content, ).toEqual([ { @@ -272,7 +275,7 @@ describe("acp tools", () => { toolName: "read", input: { filePath: "/tmp/a" }, content: [{ type: "text", text: "partial output" }], - structured: { path: "/tmp/a" }, + metadata: { path: "/tmp/a" }, error: "failed", }), ).toEqual({ @@ -286,7 +289,7 @@ describe("acp tools", () => { { type: "content", content: { type: "text", text: "partial output" } }, { type: "content", content: { type: "text", text: "failed" } }, ], - rawOutput: { structured: { path: "/tmp/a" }, error: "failed" }, + rawOutput: { metadata: { path: "/tmp/a" }, error: "failed" }, }) }) }) diff --git a/packages/cli/test/import-boundaries.test.ts b/packages/cli/test/import-boundaries.test.ts index 8b189ce2c032..3634aa5aed93 100644 --- a/packages/cli/test/import-boundaries.test.ts +++ b/packages/cli/test/import-boundaries.test.ts @@ -23,7 +23,12 @@ describe("CLI frontend import boundaries", () => { expect(Object.keys(run).sort()).toEqual(["runNonInteractive", "runV1Bridge"]) expect(Object.keys(mini).sort()).toEqual(["runMiniFrontend"]) - expect(Object.keys(tool).sort()).toEqual(["toolInlineInfo", "toolOutputText"]) + expect(Object.keys(tool).sort()).toEqual([ + "nonEmptyToolContent", + "readDisplayText", + "toolInlineInfo", + "toolOutputText", + ]) expect(Object.keys(cli.exports).filter((key) => key === "./mini" || key.startsWith("./mini/"))).toEqual([]) }) diff --git a/packages/cli/test/run/noninteractive.test.ts b/packages/cli/test/run/noninteractive.test.ts index f0a680061f24..8527bcaa57a3 100644 --- a/packages/cli/test/run/noninteractive.test.ts +++ b/packages/cli/test/run/noninteractive.test.ts @@ -134,15 +134,14 @@ function failedTool(inputID: string): V2Event[] { sessionID: "ses_1", assistantMessageID: "msg_failed_tool", callID: "call_failed_tool", - structured: { checkpoint: 1 }, - content: [{ type: "text", text: "partial output" }], + metadata: { checkpoint: 1 }, }, }, { id: "evt_failed_tool_terminal", created: 4, type: "session.tool.failed", - durable: { aggregateID: "ses_1", seq: 4, version: 1 }, + durable: { aggregateID: "ses_1", seq: 4, version: 2 }, data: { sessionID: "ses_1", assistantMessageID: "msg_failed_tool", @@ -190,12 +189,12 @@ function successfulGrep(inputID: string): V2Event[] { id: "evt_grep_success", created: 3, type: "session.tool.success", - durable: { aggregateID: "ses_1", seq: 3, version: 1 }, + durable: { aggregateID: "ses_1", seq: 3, version: 2 }, data: { sessionID: "ses_1", assistantMessageID: "msg_grep", callID: "call_grep", - structured: { matches: 2 }, + metadata: { matches: 2 }, content: [{ type: "text", text }], executed: false, }, @@ -258,9 +257,7 @@ async function run(input: { spyOn(sdk.session, "wait").mockImplementation(() => input.wait?.() ?? wait.promise) spyOn(sdk.message, "list").mockImplementation(() => ok({ - data: input.messages?.(promptID) ?? [ - { id: promptID, type: "user", text: "hello", time: { created: 1 } }, - ], + data: input.messages?.(promptID) ?? [{ id: promptID, type: "user", text: "hello", time: { created: 1 } }], cursor: {}, }), ) @@ -316,7 +313,7 @@ afterEach(() => { }) describe("runNonInteractivePrompt", () => { - test("keeps formatted tool output and compact structured metadata in JSON", async () => { + test("keeps formatted tool output and compact tool metadata in JSON", async () => { const output = await capture({ format: "json", turn: successfulGrep }) const events = output.stdout .split("\n") @@ -332,13 +329,13 @@ describe("runNonInteractivePrompt", () => { status: "completed", output: expect.stringContaining("Found 2 matches"), metadata: { - structured: { matches: 2 }, + metadata: { matches: 2 }, content: [{ type: "text", text: expect.stringContaining("/src/a.ts") }], }, }, }, }) - expect(events[0].part.state.metadata.structured).toEqual({ matches: 2 }) + expect(events[0].part.state.metadata.metadata).toEqual({ matches: 2 }) expect(events[0].part.state.metadata.result).toBeUndefined() }) @@ -534,7 +531,7 @@ describe("runNonInteractivePrompt", () => { id: "call_failed_tool", state: { status: "completed", - structured: { checkpoint: 1 }, + metadata: { checkpoint: 1 }, content: [{ type: "text", text: "partial output" }], }, }, @@ -544,7 +541,7 @@ describe("runNonInteractivePrompt", () => { id: "call_failed_tool", state: { status: "error", - structured: { checkpoint: 1 }, + metadata: { checkpoint: 1 }, content: [{ type: "text", text: "partial output" }], error: { message: "tool failed" }, }, @@ -574,7 +571,7 @@ describe("runNonInteractivePrompt", () => { }, }) expect(events[0].part.state.output).toBeUndefined() - expect(events[0].part.state.metadata.structured).toBeUndefined() + expect(events[0].part.state.metadata.metadata).toBeUndefined() expect(events[0].part.state.metadata.content).toBeUndefined() expect(output.stderr).toBe("") }) diff --git a/packages/cli/vite.node.config.ts b/packages/cli/vite.node.config.ts index 791a0131ac2b..15bcc8490a3f 100644 --- a/packages/cli/vite.node.config.ts +++ b/packages/cli/vite.node.config.ts @@ -75,6 +75,10 @@ export const define = sdk.Plugin.define` const effectPluginModule = promisePluginModule .replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect") .replace("Promise plugin", "Effect plugin") + const promiseToolModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.promise")] +if (!sdk) throw new Error("OpenCode Promise plugin SDK is unavailable") +export const Tool = sdk.Tool +export const make = sdk.Tool.make` const effectToolModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.effect")] if (!sdk) throw new Error("OpenCode Effect plugin SDK is unavailable") export const Tool = sdk.Tool @@ -83,10 +87,8 @@ export const RegistrationError = sdk.Tool.RegistrationError export const make = sdk.Tool.make export const validateName = sdk.Tool.validateName export const registrationEntries = sdk.Tool.registrationEntries -export const withPermission = sdk.Tool.withPermission -export const permission = sdk.Tool.permission -export const definition = sdk.Tool.definition -export const settle = sdk.Tool.settle` +export const validateNamespace = sdk.Tool.validateNamespace +export const toLLMDefinition = sdk.Tool.toLLMDefinition` return `#!/usr/bin/env -S node ${nodeExecArgv.join(" ")} import __cjs_mod__ from "node:module" import { chmodSync as __ocChmod, existsSync as __ocExists, lstatSync as __ocLstat, mkdirSync as __ocMkdir, renameSync as __ocRename, rmSync as __ocRm, writeFileSync as __ocWrite } from "node:fs" @@ -100,6 +102,7 @@ const require = __cjs_mod__.createRequire(import.meta.url) const __ocPluginModules = ${JSON.stringify({ "@opencode-ai/plugin/v2": "opencode:plugin-v2", "@opencode-ai/plugin/v2/plugin": "opencode:plugin-v2-plugin", + "@opencode-ai/plugin/v2/tool": "opencode:plugin-v2-tool", "@opencode-ai/plugin/v2/effect": "opencode:plugin-v2-effect", "@opencode-ai/plugin/v2/effect/plugin": "opencode:plugin-v2-effect-plugin", "@opencode-ai/plugin/v2/effect/tool": "opencode:plugin-v2-effect-tool", @@ -107,6 +110,7 @@ const __ocPluginModules = ${JSON.stringify({ const __ocPluginSources = ${JSON.stringify({ "opencode:plugin-v2": promiseModule, "opencode:plugin-v2-plugin": promisePluginModule, + "opencode:plugin-v2-tool": promiseToolModule, "opencode:plugin-v2-effect": effectModule, "opencode:plugin-v2-effect-plugin": effectPluginModule, "opencode:plugin-v2-effect-tool": effectToolModule, diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index a08c7b324689..a1b2367c8b4e 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -104,6 +104,12 @@ export type SessionMessageProviderState = { [x: string]: JsonValue } export type SessionMessageToolStateStreaming = { status: "streaming"; input: string } +export type SessionMessageToolStateRunning = { + status: "running" + input: { [x: string]: JsonValue } + metadata: { [x: string]: JsonValue } +} + export type ToolTextContent = { type: "text"; text: string } export type ToolFileContent = { type: "file"; uri: string; mime: string; name?: string } @@ -918,6 +924,15 @@ export type SessionToolInputDelta = { data: { sessionID: string; assistantMessageID: string; callID: string; delta: string } } +export type SessionToolProgress = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.tool.progress" + location?: LocationRef + data: { sessionID: string; assistantMessageID: string; callID: string; metadata: { [x: string]: JsonValue } } +} + export type SessionCompactionDelta = { id: string created: number @@ -1809,28 +1824,19 @@ export type SessionPendingUserData1 = { metadata?: { [x: string]: any } } -export type SessionMessageToolStateRunning = { - status: "running" - input: { [x: string]: JsonValue } - structured: { [x: string]: JsonValue } - content: Array -} - export type SessionMessageToolStateCompleted = { status: "completed" input: { [x: string]: JsonValue } - content: Array - structured: { [x: string]: JsonValue } - result?: JsonValue + content: [LLMToolContent, ...Array] + metadata?: { [x: string]: JsonValue } } export type SessionMessageToolStateError = { status: "error" input: { [x: string]: JsonValue } - content: Array - structured: { [x: string]: JsonValue } error: SessionStructuredError - result?: JsonValue + content?: [LLMToolContent, ...Array] + metadata?: { [x: string]: JsonValue } } export type SessionToolSuccess = { @@ -1838,15 +1844,14 @@ export type SessionToolSuccess = { created: number metadata?: { [x: string]: any } type: "session.tool.success" - durable: { aggregateID: string; seq: number; version: 1 } + durable: { aggregateID: string; seq: number; version: 2 } location?: LocationRef data: { sessionID: string assistantMessageID: string callID: string - structured: { [x: string]: any } - content: Array - result?: any + content: [LLMToolContent, ...Array] + metadata?: { [x: string]: JsonValue } executed: boolean resultState?: SessionMessageProviderState6 } @@ -1857,7 +1862,7 @@ export type SessionToolFailed = { created: number metadata?: { [x: string]: any } type: "session.tool.failed" - durable: { aggregateID: string; seq: number; version: 1 } + durable: { aggregateID: string; seq: number; version: 2 } location?: LocationRef data: { sessionID: string @@ -1865,28 +1870,12 @@ export type SessionToolFailed = { callID: string error: SessionStructuredError content?: [LLMToolContent, ...Array] - metadata?: { [x: string]: any } - result?: any + metadata?: { [x: string]: JsonValue } executed: boolean resultState?: SessionMessageProviderState7 } } -export type SessionToolProgress = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.tool.progress" - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - callID: string - structured: { [x: string]: any } - content: Array - } -} - export type SessionMessageCompaction = | SessionMessageCompactionRunning | SessionMessageCompactionCompleted diff --git a/packages/codemode/README.md b/packages/codemode/README.md index 09ba57346ebf..923da8d441d7 100644 --- a/packages/codemode/README.md +++ b/packages/codemode/README.md @@ -33,7 +33,7 @@ const lookupOrder = Tool.make({ description: "Look up an order by ID", input: Schema.Struct({ id: Schema.String }), output: Schema.Struct({ id: Schema.String, status: Schema.String }), - run: ({ id }) => Effect.succeed({ id, status: "open" }), + execute: ({ id }) => Effect.succeed({ id, status: "open" }), }) const runtime = CodeMode.make({ @@ -55,10 +55,10 @@ const result = await Effect.runPromise( ### `Tool.make` `input` and `output` accept either an Effect Schema or a render-only JSON Schema document. Effect Schema input is -decoded before `run`; Effect Schema output is decoded and safely copied before the program sees it. JSON Schemas only -shape the model-visible signature. Without `output`, the signature uses `Promise`. +decoded before `execute`; Effect Schema output is decoded and safely copied before the program sees it. JSON Schemas +only shape the model-visible signature. Without `output`, the signature uses `Promise`. -Descriptions and schemas are model-visible contracts. Authorization belongs in `run`. +Descriptions and schemas are model-visible contracts. Authorization belongs in `execute`. Dots in tool names create namespaces: `{ "issues.list": tool }` and `{ issues: { list: tool } }` both expose `tools.issues.list(...)`. Other characters use bracket notation, such as diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index 0d989595ec82..4b8b9001107b 100644 --- a/packages/codemode/interpreter-support.md +++ b/packages/codemode/interpreter-support.md @@ -188,7 +188,7 @@ ultimate source of truth. first-call-wins resolve/reject functions, and ignore throws after settlement. Inherited/accessor `then` fields and a JavaScript `this` receiver remain outside the supported object/function model. - [x] Dotted tool names are canonicalized into namespace paths; a path can be both callable and a namespace, and the - last definition supplied for a canonical path wins. + last tool supplied for a canonical path wins. - [x] Tool path segments may be named `constructor`, `prototype`, or `__proto__` because paths use inert Map keys. - [x] Outbound tool arguments follow JSON serialization semantics, like `JSON.stringify`: object properties with `undefined` values are dropped, `undefined` array elements and non-finite numbers become `null`, and sparse diff --git a/packages/codemode/src/interpreter/execute.ts b/packages/codemode/src/interpreter/execute.ts index af2eccf9b856..a3f789868efe 100644 --- a/packages/codemode/src/interpreter/execute.ts +++ b/packages/codemode/src/interpreter/execute.ts @@ -45,7 +45,7 @@ export const executeWithLimits = const program = parseProgram(options.code) const promises = new PromiseRuntime>(scope) const interpreter = new Interpreter>( - tools.invoke, + tools.execute, tools.search, tools.keys, promises, diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts index 3af385d5130c..2659533346a0 100644 --- a/packages/codemode/src/interpreter/runtime.ts +++ b/packages/codemode/src/interpreter/runtime.ts @@ -271,7 +271,10 @@ const promiseResolutionNode: AstNode = { type: "PromiseResolution" } export class Interpreter { private scopes: ScopeStack - private readonly invokeTool: (path: ReadonlyArray, args: Array) => Effect.Effect + private readonly executeTool: ( + path: ReadonlyArray, + args: Array, + ) => Effect.Effect private readonly invokeSearch: (args: Array) => Effect.Effect private readonly toolKeys: (path: ReadonlyArray) => ReadonlyArray private readonly logs: Array @@ -286,7 +289,7 @@ export class Interpreter { } constructor( - invokeTool: (path: ReadonlyArray, args: Array) => Effect.Effect, + executeTool: (path: ReadonlyArray, args: Array) => Effect.Effect, invokeSearch: (args: Array) => Effect.Effect, toolKeys: (path: ReadonlyArray) => ReadonlyArray, promises: PromiseRuntime, @@ -294,7 +297,7 @@ export class Interpreter { ) { const globalScope = new Map() this.scopes = new ScopeStack([globalScope]) - this.invokeTool = invokeTool + this.executeTool = executeTool this.invokeSearch = invokeSearch this.toolKeys = toolKeys this.logs = logs @@ -369,7 +372,7 @@ export class Interpreter { path: ReadonlyArray, args: Array, ): Effect.Effect { - return this.createPromise(Effect.suspend(() => this.invokeTool(path, args))) + return this.createPromise(Effect.suspend(() => this.executeTool(path, args))) } private createPromise(effect: Effect.Effect): Effect.Effect { @@ -2079,7 +2082,7 @@ export class Interpreter { } private invokeFunction(fn: CodeModeFunction, args: Array): Effect.Effect { - const invocation = new Interpreter(this.invokeTool, this.invokeSearch, this.toolKeys, this.promises, this.logs) + const invocation = new Interpreter(this.executeTool, this.invokeSearch, this.toolKeys, this.promises, this.logs) invocation.scopes = new ScopeStack([...fn.capturedScopes, new Map()]) const run = Effect.gen(function* () { // Seed all parameters first so defaults cannot fall through to same-named outer bindings. diff --git a/packages/codemode/src/openapi/index.ts b/packages/codemode/src/openapi/index.ts index 5ea688129e1d..1da5779cb446 100644 --- a/packages/codemode/src/openapi/index.ts +++ b/packages/codemode/src/openapi/index.ts @@ -1,5 +1,5 @@ import { HttpClient } from "effect/unstable/http" -import { make, type Definition } from "../tool.js" +import { make, type Tool } from "../tool.js" import { invoke } from "./runtime.js" import { componentDefinitions, @@ -108,7 +108,7 @@ export const fromSpec = (options: Options): Result => { description: operation.description ?? operation.summary ?? `${operation.method} ${path}`, input: inputSchema(input.fields, requestDefinitions), output: output.value, - run: (input) => invoke(plan, input), + execute: (input) => invoke(plan, input), }), ) } @@ -117,16 +117,16 @@ export const fromSpec = (options: Options): Result => { return { tools, skipped } } -const setTool = (tools: Tools, path: ReadonlyArray, definition: Definition): void => { +const setTool = (tools: Tools, path: ReadonlyArray, tool: Tool): void => { const [head, ...rest] = path if (head === undefined) return if (rest.length === 0) { - tools[head] = definition + tools[head] = tool return } const child = tools[head] if (child === undefined || !isRecord(child) || child._tag === "CodeModeTool") { tools[head] = Object.create(null) as Tools } - setTool(tools[head] as Tools, rest, definition) + setTool(tools[head] as Tools, rest, tool) } diff --git a/packages/codemode/src/openapi/types.ts b/packages/codemode/src/openapi/types.ts index 252f49d86c7c..d1d4e5b49a5c 100644 --- a/packages/codemode/src/openapi/types.ts +++ b/packages/codemode/src/openapi/types.ts @@ -1,6 +1,6 @@ import { Effect } from "effect" import { HttpClient } from "effect/unstable/http" -import type { Definition, JsonSchema } from "../tool.js" +import type { Tool, JsonSchema } from "../tool.js" /** A parsed OpenAPI 3.x document. YAML must be parsed by the host. */ export type Document = Record @@ -58,7 +58,7 @@ export type Skipped = { readonly reason: string } -export type Tools = { [name: string]: Definition | Tools } +export type Tools = { [name: string]: Tool | Tools } export type Result = { /** Namespaced tools; the host places them under a key in its `tools` object. */ diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index febc2ba58606..389d3af2760a 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -8,7 +8,7 @@ import { inputTypeScript, outputTypeScript, } from "./tool-schema.js" -import { isDefinition as isToolDefinition, type Definition } from "./tool.js" +import { isTool, type Tool } from "./tool.js" import type { Tools } from "./tools.js" import { CodeModeDate, @@ -28,7 +28,7 @@ type ServicesOf> = Depth["length"] exten ? never : T extends { readonly _tag: "CodeModeTool" - readonly run: (input: unknown) => Effect.Effect + readonly execute: (input: unknown) => Effect.Effect } ? R : T extends object @@ -118,8 +118,6 @@ export class ToolRuntimeError extends Error { } } -const isDefinition = (value: Definition | Tools): value is Definition => isToolDefinition(value) - const runHost = (effect: Effect.Effect): Effect.Effect => effect.pipe( Effect.catchCause((cause) => { @@ -286,9 +284,9 @@ export const copyOut = (value: unknown, mode: CopyOutMode): unknown => { return value } -// Dots in tool names are namespace separators; the last definition for a canonical path wins. +// Dots in tool names are namespace separators; the last tool for a canonical path wins. type ToolNode = { - definition?: Definition + tool?: Tool readonly children: Map> } @@ -303,7 +301,7 @@ const toolTrie = (tools: Tools): ToolNode => { current.children.set(segment, child) current = child } - if (isDefinition(value)) current.definition = value + if (isTool(value)) current.tool = value else insert(current, value) } } @@ -314,25 +312,25 @@ const toolTrie = (tools: Tools): ToolNode => { const canonicalSegments = (path: ReadonlyArray): ReadonlyArray => path.flatMap((segment) => segment.split(".")) -const definitions = ( +const flattenTools = ( node: ToolNode, path: ReadonlyArray = [], -): Array<{ path: string; definition: Definition }> => [ - ...(node.definition === undefined ? [] : [{ path: path.join("."), definition: node.definition }]), - ...Array.from(node.children, ([name, child]) => definitions(child, [...path, name])).flat(), +): Array<{ path: string; tool: Tool }> => [ + ...(node.tool === undefined ? [] : [{ path: path.join("."), tool: node.tool }]), + ...Array.from(node.children, ([name, child]) => flattenTools(child, [...path, name])).flat(), ] -const describeDefinition = (path: string, definition: Definition): ToolDescription => ({ +const describeTool = (path: string, tool: Tool): ToolDescription => ({ path, - description: definition.description, - signature: `${toolExpression(path)}(input: ${inputTypeScript(definition, true)}): Promise<${outputTypeScript(definition, true)}>`, + description: tool.description, + signature: `${toolExpression(path)}(input: ${inputTypeScript(tool, true)}): Promise<${outputTypeScript(tool, true)}>`, }) -const visibleDefinitions = (tools: Tools) => - definitions(toolTrie(tools)).map(({ path, definition }) => ({ +const visibleTools = (tools: Tools) => + flattenTools(toolTrie(tools)).map(({ path, tool }) => ({ path, - definition, - description: describeDefinition(path, definition), + tool, + description: describeTool(path, tool), })) export type DiscoveryPlan = { @@ -361,12 +359,12 @@ const termForms = (term: string): Array => { return forms } -const makeSearchTool = (searchIndex: ReadonlyArray): Definition => ({ +const makeSearchTool = (searchIndex: ReadonlyArray): Tool => ({ _tag: "CodeModeTool", description: "Search available tools", input: SearchInput, output: SearchOutput, - run: (input) => + execute: (input) => Effect.sync(() => { const request = input as typeof SearchInput.Type const query = request.query ?? "" @@ -422,8 +420,8 @@ const makeSearchTool = (searchIndex: ReadonlyArray): Definition => }) const searchSignature = (() => { - const definition = makeSearchTool([]) - return `search(input: ${inputTypeScript(definition, true)}): ${outputTypeScript(definition, true)}` + const tool = makeSearchTool([]) + return `search(input: ${inputTypeScript(tool, true)}): ${outputTypeScript(tool, true)}` })() const catalogLine = (tool: ToolDescription) => { @@ -432,13 +430,13 @@ const catalogLine = (tool: ToolDescription) => { return description === "" ? ` - ${tool.signature}` : ` - ${tool.signature} // ${description}` } -const toSearchEntry = (path: string, definition: Definition, description: ToolDescription): SearchEntry => ({ +const toSearchEntry = (path: string, tool: Tool, description: ToolDescription): SearchEntry => ({ description, namespace: path.split(".", 1)[0]!, searchText: [ path, - definition.description, - ...inputProperties(definition).flatMap(({ name, description: property }) => + tool.description, + ...inputProperties(tool).flatMap(({ name, description: property }) => property === undefined ? [name] : [name, property], ), ] @@ -447,14 +445,14 @@ const toSearchEntry = (path: string, definition: Definition, description: }) export const searchIndex = (tools: Tools): ReadonlyArray => - visibleDefinitions(tools).map(({ path, definition, description }) => toSearchEntry(path, definition, description)) + visibleTools(tools).map(({ path, tool, description }) => toSearchEntry(path, tool, description)) // Budget signatures round-robin so every namespace remains visible. export const prepare = (tools: Tools, catalogBudget = defaultCatalogBudget): DiscoveryPlan => { if (!Number.isSafeInteger(catalogBudget) || catalogBudget < 0) { throw new RangeError("discovery.catalogBudget must be a non-negative safe integer") } - const visible = visibleDefinitions(tools) + const visible = visibleTools(tools) const described = visible.map(({ description }) => description) const namespaces = new Map>() @@ -589,7 +587,7 @@ export const prepare = (tools: Tools, catalogBudget = defaultCatalogBudget return { catalog: described, instructions: lines.join("\n"), - searchIndex: visible.map(({ path, definition, description }) => toSearchEntry(path, definition, description)), + searchIndex: visible.map(({ path, tool, description }) => toSearchEntry(path, tool, description)), } } @@ -605,7 +603,7 @@ const namespaceKeys = (root: ToolNode, path: ReadonlyArray): Reado return Array.from(node.children.keys()) } -const resolve = (root: ToolNode, path: ReadonlyArray): Definition => { +const resolve = (root: ToolNode, path: ReadonlyArray): Tool => { const segments = canonicalSegments(path) const node = lookup(root, segments) if (node === undefined) { @@ -613,16 +611,16 @@ const resolve = (root: ToolNode, path: ReadonlyArray): Definition< "Use search({ query }) to find available described tools.", ]) } - if (node.definition === undefined) { + if (node.tool === undefined) { throw new ToolRuntimeError("UnknownTool", `Tool '${segments.join(".")}' is not callable.`) } - return node.definition + return node.tool } export type ToolRuntime = { readonly root: ToolReference readonly calls: Array - readonly invoke: (path: ReadonlyArray, args: Array) => Effect.Effect + readonly execute: (path: ReadonlyArray, args: Array) => Effect.Effect readonly search: (args: Array) => Effect.Effect readonly keys: (path: ReadonlyArray) => ReadonlyArray } @@ -676,7 +674,7 @@ export const make = ( return calls.length - 1 }).pipe(Effect.tap((index) => hooks?.onToolCallStart?.({ index, name, input }) ?? Effect.void)) - const invokeDefinition = (name: string, tool: Definition, externalArgs: Array) => + const executeTool = (name: string, tool: Tool, externalArgs: Array) => Effect.gen(function* () { if (externalArgs.length !== 1) throw new ToolRuntimeError("InvalidToolInput", `Tool '${name}' expects exactly one input object.`) @@ -688,7 +686,7 @@ export const make = ( const index = yield* recordAndObserve(name, input) return yield* observeEnd( Effect.gen(function* () { - const raw = yield* runHost(Effect.suspend(() => tool.run(input))) + const raw = yield* runHost(Effect.suspend(() => tool.execute(input))) const result = yield* Effect.try({ try: () => decodeToolOutput(tool, raw), catch: () => new ToolRuntimeError("InvalidToolOutput", `Invalid output from tool '${name}'.`), @@ -705,18 +703,18 @@ export const make = ( keys: (path) => namespaceKeys(root, path), search: (args) => Effect.suspend(() => - invokeDefinition( + executeTool( "search", searchTool, args.map((arg) => copyOut(copyIn(arg, "Arguments for tool 'search'"), "json")), ), ), - invoke: (path, args) => + execute: (path, args) => Effect.gen(function* () { const name = canonicalSegments(path).join(".") const externalArgs = args.map((arg) => copyOut(copyIn(arg, `Arguments for tool '${name}'`), "json")) const tool = resolve(root, path) - return yield* invokeDefinition(name, tool, externalArgs) + return yield* executeTool(name, tool, externalArgs) }), } } diff --git a/packages/codemode/src/tool-schema.ts b/packages/codemode/src/tool-schema.ts index d8b48dc54850..52e1cd4033d8 100644 --- a/packages/codemode/src/tool-schema.ts +++ b/packages/codemode/src/tool-schema.ts @@ -1,5 +1,5 @@ import { JsonPointer, Schema } from "effect" -import type { Definition, JsonSchema, SchemaType } from "./tool.js" +import type { Tool, JsonSchema, SchemaType } from "./tool.js" const isEffectSchema = (schema: SchemaType): schema is Schema.Decoder & Schema.Top => Schema.isSchema(schema) @@ -192,16 +192,16 @@ export type InputProperty = { readonly required: boolean } -export const inputProperties = (definition: Definition): Array => { +export const inputProperties = (tool: Tool): Array => { try { - const document = isEffectSchema(definition.input) - ? (Schema.toJsonSchemaDocument(definition.input) as { + const document = isEffectSchema(tool.input) + ? (Schema.toJsonSchemaDocument(tool.input) as { readonly schema: JsonSchema readonly definitions?: Readonly> }) : { - schema: definition.input, - definitions: { ...(definition.input.definitions ?? {}), ...(definition.input.$defs ?? {}) }, + schema: tool.input, + definitions: { ...(tool.input.definitions ?? {}), ...(tool.input.$defs ?? {}) }, } const definitions = document.definitions ?? {} let schema = document.schema @@ -223,22 +223,22 @@ export const inputProperties = (definition: Definition): Array(definition: Definition, pretty = false): string => - isEffectSchema(definition.input) - ? toTypeScript(definition.input, false, pretty) - : jsonSchemaToTypeScript(definition.input, pretty) - -export const outputTypeScript = (definition: Definition, pretty = false): string => - definition.output === undefined - ? "unknown" - : isEffectSchema(definition.output) - ? toTypeScript(definition.output, true, pretty) - : jsonSchemaToTypeScript(definition.output, pretty) - -export const decodeInput = (definition: Definition, value: unknown): unknown => - isEffectSchema(definition.input) ? Schema.decodeUnknownSync(definition.input)(value) : value - -export const decodeOutput = (definition: Definition, value: unknown): unknown => - definition.output !== undefined && isEffectSchema(definition.output) - ? Schema.decodeUnknownSync(definition.output)(value) - : value +export const inputTypeScript = (tool: Tool, pretty = false): string => + isEffectSchema(tool.input) ? toTypeScript(tool.input, false, pretty) : jsonSchemaToTypeScript(tool.input, pretty) + +export const outputTypeScript = (tool: Tool, pretty = false): string => + tool.output === undefined + ? "void" + : isEffectSchema(tool.output) + ? toTypeScript(tool.output, true, pretty) + : jsonSchemaToTypeScript(tool.output, pretty) + +export const decodeInput = (tool: Tool, value: unknown): unknown => + isEffectSchema(tool.input) ? Schema.decodeUnknownSync(tool.input)(value) : value + +export const decodeOutput = (tool: Tool, value: unknown): unknown => + tool.output === undefined + ? undefined + : isEffectSchema(tool.output) + ? Schema.decodeUnknownSync(tool.output)(value) + : value diff --git a/packages/codemode/src/tool.ts b/packages/codemode/src/tool.ts index e75fa7ba2609..d44e3d0f4e27 100644 --- a/packages/codemode/src/tool.ts +++ b/packages/codemode/src/tool.ts @@ -29,29 +29,29 @@ export type JsonSchema = { /** Either a validating Effect Schema or a render-only JSON Schema document. */ export type SchemaType = Schema.Decoder | JsonSchema -/** Schema-backed tool definition exposed through CodeMode's `tools` object. */ -export type Definition = { +/** Executable tool tool exposed through CodeMode's `tools` object. */ +export type Tool = { readonly _tag: "CodeModeTool" readonly description: string readonly input: SchemaType readonly output: SchemaType | undefined - readonly run: (input: unknown) => Effect.Effect + readonly execute: (input: unknown) => Effect.Effect } type InputType = S extends Schema.Decoder ? S["Type"] : unknown -type ResultType = S extends Schema.Decoder ? S["Encoded"] : unknown +type ResultType = S extends undefined ? void : S extends Schema.Decoder ? S["Encoded"] : unknown -/** Options for defining one CodeMode tool. */ +/** Options for declaring one CodeMode tool. */ export type Options = { readonly description: string readonly input: I readonly output?: O - readonly run: (input: InputType) => Effect.Effect, unknown, R> + readonly execute: (input: InputType) => Effect.Effect, unknown, R> } -// Object.hasOwn: an inherited _tag must not classify a namespace as a Definition. -export const isDefinition = (value: unknown): value is Definition => +// Object.hasOwn: an inherited _tag must not classify a namespace as a Tool. +export const isTool = (value: unknown): value is Tool => typeof value === "object" && value !== null && "_tag" in value && @@ -59,18 +59,18 @@ export const isDefinition = (value: unknown): value is Definition value._tag === "CodeModeTool" /** - * Defines one schema-described tool available to a CodeMode program through `tools.*`. + * Declares one schema-described tool available to a CodeMode program through `tools.*`. * * Effect Schemas validate values; JSON Schemas only shape the model-visible signature. - * Without `output`, results are exposed as `unknown`. Hosts remain responsible for authorization + * Without `output`, results are exposed as `void`. Hosts remain responsible for authorization * and durable side effects. */ export const make = ( options: Options, -): Definition => ({ +): Tool => ({ _tag: "CodeModeTool", description: options.description, input: options.input, output: options.output, - run: (input) => options.run(input as InputType), + execute: (input) => options.execute(input as InputType), }) diff --git a/packages/codemode/src/tools.ts b/packages/codemode/src/tools.ts index 04e36dcef224..8c9759fb338e 100644 --- a/packages/codemode/src/tools.ts +++ b/packages/codemode/src/tools.ts @@ -1,5 +1,5 @@ -import type { Definition } from "./tool.js" +import type { Tool } from "./tool.js" export type Tools = { - readonly [name: string]: Definition | Tools + readonly [name: string]: Tool | Tools } diff --git a/packages/codemode/test/callbacks.test.ts b/packages/codemode/test/callbacks.test.ts index 3c6439171854..f257b732c0ca 100644 --- a/packages/codemode/test/callbacks.test.ts +++ b/packages/codemode/test/callbacks.test.ts @@ -27,7 +27,7 @@ const echo = Tool.make({ description: "Echo the input", input: Schema.Struct({ id: Schema.Number }), output: Schema.Number, - run: (input: { id: number }) => Effect.succeed(input.id), + execute: (input: { id: number }) => Effect.succeed(input.id), }) const withTool = (code: string) => Effect.runPromise(CodeMode.make({ tools: { host: { echo } } }).execute(code)) const toolError = async (code: string) => { diff --git a/packages/codemode/test/codemode.test.ts b/packages/codemode/test/codemode.test.ts index 8699daf07da0..9d5db0ba5837 100644 --- a/packages/codemode/test/codemode.test.ts +++ b/packages/codemode/test/codemode.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test" import { Cause, Effect, Schema } from "effect" import { CodeMode, Tool, toolError } from "../src/index.js" -const run = (tool: Tool.Definition) => +const run = (tool: Tool.Tool) => Effect.runPromise(CodeMode.make({ tools: { host: { call: tool } } }).execute("return await tools.host.call({})")) class UnsafeHostError extends Schema.TaggedErrorClass()("UnsafeHostError", { @@ -16,7 +16,7 @@ describe("CodeMode host failure boundary", () => { description: "Fail safely", input: Schema.Struct({}), output: Schema.String, - run: () => Effect.fail(toolError("Authorized request was refused")), + execute: () => Effect.fail(toolError("Authorized request was refused")), }), ) @@ -32,7 +32,7 @@ describe("CodeMode host failure boundary", () => { description: "Fail safely", input: Schema.Struct({}), output: Schema.String, - run: () => Effect.fail(toolError("File not found: /tmp/report.json")), + execute: () => Effect.fail(toolError("File not found: /tmp/report.json")), }), ) @@ -52,7 +52,7 @@ describe("CodeMode host failure boundary", () => { description: "Fail internally", input: Schema.Struct({}), output: Schema.String, - run: () => failure, + execute: () => failure, }), ) @@ -71,7 +71,7 @@ describe("CodeMode host failure boundary", () => { description: "Return invalid output", input: Schema.Struct({}), output: Schema.Struct({ safe: Schema.String }), - run: () => Effect.succeed({ safe: 1, secret } as unknown as { readonly safe: string }), + execute: () => Effect.succeed({ safe: 1, secret } as unknown as { readonly safe: string }), }), ) @@ -88,7 +88,7 @@ describe("CodeMode host failure boundary", () => { description: "Return hostile output", input: Schema.Struct({}), output: Schema.Unknown, - run: () => + execute: () => Effect.succeed( new Proxy( {}, @@ -118,7 +118,7 @@ describe("CodeMode host failure boundary", () => { description: "Refuse", input: Schema.Struct({}), output: Schema.String, - run: () => Effect.fail(toolError("Refused")), + execute: () => Effect.fail(toolError("Refused")), }), }, }, @@ -145,7 +145,7 @@ describe("CodeMode host failure boundary", () => { description: "Interrupt", input: Schema.Struct({}), output: Schema.String, - run: () => Effect.interrupt, + execute: () => Effect.interrupt, }), }, }, @@ -166,7 +166,7 @@ describe("CodeMode tool-call observation", () => { description: "Look up a value", input: Schema.Struct({ query: Schema.String }), output: Schema.String, - run: ({ query }) => Effect.succeed(query), + execute: ({ query }) => Effect.succeed(query), }) const result = await Effect.runPromise( @@ -189,7 +189,7 @@ describe("CodeMode tool-call observation", () => { description: "Look up a value", input: Schema.Struct({ query: Schema.String }), output: Schema.String, - run: ({ query }) => (query === "boom" ? Effect.fail(toolError("Lookup refused")) : Effect.succeed(query)), + execute: ({ query }) => (query === "boom" ? Effect.fail(toolError("Lookup refused")) : Effect.succeed(query)), }) const runtime = CodeMode.make({ @@ -430,7 +430,7 @@ describe("CodeMode schema flexibility", () => { properties: { id: { type: "string" }, count: { type: "number" } }, required: ["id"], }, - run: (input) => + execute: (input) => Effect.sync(() => { observed.push(input) return { echoed: input } @@ -442,14 +442,14 @@ describe("CodeMode schema flexibility", () => { { path: "adapter.call", description: "Call an adapter-described tool", - signature: "tools.adapter.call(input: {\n id: string,\n count?: number,\n}): Promise", + signature: "tools.adapter.call(input: {\n id: string,\n count?: number,\n}): Promise", }, ]) // JSON Schema is render-only: mistyped input passes through unvalidated. const result = await Effect.runPromise(runtime.execute(`return await tools.adapter.call({ id: 42 })`)) expect(result.ok).toBe(true) - if (result.ok) expect(result.value).toStrictEqual({ echoed: { id: 42 } }) + if (result.ok) expect(result.value).toBeNull() expect(observed).toStrictEqual([{ id: 42 }]) }) @@ -458,7 +458,7 @@ describe("CodeMode schema flexibility", () => { const call = Tool.make({ description: "Observe raw input", input: { type: "object" }, - run: (input) => + execute: (input) => Effect.sync(() => { observed.push(input) return "ok" @@ -483,7 +483,7 @@ describe("CodeMode schema flexibility", () => { const find = Tool.make({ description: "Find things", input: Schema.Struct({ query: Schema.optionalKey(Schema.String), limit: Schema.optionalKey(Schema.Number) }), - run: (input) => + execute: (input) => Effect.sync(() => { observed.push(input) return "ok" @@ -517,7 +517,7 @@ describe("CodeMode schema flexibility", () => { }, }, }, - run: () => Effect.succeed({ login: "kit", id: 7 }), + execute: () => Effect.succeed({ login: "kit", id: 7 }), }) const runtime = CodeMode.make({ tools: { users: { lookup } } }) @@ -534,18 +534,18 @@ describe("CodeMode schema flexibility", () => { if (result.ok) expect(result.value).toStrictEqual({ login: "kit", id: 7 }) }) - test("Effect Schema output without an input transform still renders unknown when omitted", async () => { + test("Effect Schema output without an input transform renders void when omitted", async () => { const ping = Tool.make({ description: "Ping", input: Schema.Struct({ host: Schema.String }), - run: () => Effect.succeed("pong"), + execute: () => Effect.succeed("pong"), }) const runtime = CodeMode.make({ tools: { net: { ping } } }) - expect(runtime.catalog()[0]?.signature).toBe("tools.net.ping(input: {\n host: string,\n}): Promise") + expect(runtime.catalog()[0]?.signature).toBe("tools.net.ping(input: {\n host: string,\n}): Promise") const result = await Effect.runPromise(runtime.execute(`return await tools.net.ping({ host: "example.test" })`)) expect(result.ok).toBe(true) - if (result.ok) expect(result.value).toBe("pong") + if (result.ok) expect(result.value).toBeNull() }) }) @@ -554,7 +554,7 @@ describe("CodeMode public contract", () => { description: "Look up an order by ID", input: Schema.Struct({ id: Schema.String }), output: Schema.Struct({ id: Schema.String, status: Schema.String }), - run: ({ id }) => Effect.succeed({ id, status: "open" }), + execute: ({ id }) => Effect.succeed({ id, status: "open" }), }) const tools = { orders: { lookup } } const source = `return await tools.orders.lookup({ id: "order_42" })` @@ -577,7 +577,7 @@ describe("CodeMode public contract", () => { description: "echo", input: Schema.Struct({}), output: Schema.Number, - run: () => Effect.succeed(1), + execute: () => Effect.succeed(1), }) const effect = CodeMode.execute({ tools: { host: { echo } }, @@ -634,7 +634,7 @@ describe("CodeMode public contract", () => { description: "Resolve a library ID", input: Schema.Struct({ libraryName: Schema.String }), output: Schema.String, - run: ({ libraryName }) => Effect.succeed(`/resolved/${libraryName}`), + execute: ({ libraryName }) => Effect.succeed(`/resolved/${libraryName}`), }) const runtime = CodeMode.make({ tools: { context7: { "resolve-library-id": resolveLibrary } } }) @@ -760,18 +760,18 @@ describe("CodeMode public contract", () => { expect(instructions).not.toContain("search(") }) - test("uses one ranked search returning complete definitions for large catalogs", async () => { + test("uses one ranked search returning complete tools for large catalogs", async () => { const upload = Tool.make({ description: "Upload one readable local file to the current Discord thread", input: Schema.Struct({ path: Schema.String }), output: Schema.Struct({ sent: Schema.Boolean }), - run: () => Effect.succeed({ sent: true }), + execute: () => Effect.succeed({ sent: true }), }) const generate = Tool.make({ description: "Generate an image and upload it to the current Discord thread", input: Schema.Struct({ prompt: Schema.String }), output: Schema.Struct({ sent: Schema.Boolean }), - run: () => Effect.succeed({ sent: true }), + execute: () => Effect.succeed({ sent: true }), }) const runtime = CodeMode.make({ tools: { thread: { uploadFile: upload, generateImage: generate }, orders: { lookup } }, @@ -865,7 +865,7 @@ describe("CodeMode public contract", () => { description: `Numbered tool ${index}`, input: Schema.Struct({ id: Schema.String }), output: Schema.String, - run: () => Effect.succeed("ok"), + execute: () => Effect.succeed("ok"), }) const runtime = CodeMode.make({ tools: { @@ -911,7 +911,7 @@ describe("CodeMode public contract", () => { description, input: Schema.Struct({ id: Schema.String }), output: Schema.String, - run: () => Effect.succeed("ok"), + execute: () => Effect.succeed("ok"), }) const runtime = CodeMode.make({ tools: { @@ -954,13 +954,13 @@ describe("CodeMode public contract", () => { properties: { attachment: { type: "string", description: "Local path of the payload to send" } }, required: ["attachment"], }, - run: () => Effect.succeed("ok"), + execute: () => Effect.succeed("ok"), }) const other = Tool.make({ description: "Rename the workspace", input: Schema.Struct({ name: Schema.String }), output: Schema.String, - run: () => Effect.succeed("ok"), + execute: () => Effect.succeed("ok"), }) const runtime = CodeMode.make({ tools: { files: { upload, other } } }) @@ -990,7 +990,7 @@ describe("CodeMode public contract", () => { description, input: Schema.Struct({ id: Schema.String }), output: Schema.String, - run: () => Effect.succeed("ok"), + execute: () => Effect.succeed("ok"), }) const runtime = CodeMode.make({ tools: { @@ -1029,7 +1029,7 @@ describe("CodeMode public contract", () => { description, input: Schema.Struct({}), output: Schema.String, - run: () => Effect.succeed("ok"), + execute: () => Effect.succeed("ok"), }) // Deliberately declared out of alphabetical order. const runtime = CodeMode.make({ @@ -1071,7 +1071,7 @@ describe("CodeMode public contract", () => { description: "Cheap", input: Schema.Struct({ q: Schema.String }), output: Schema.String, - run: () => Effect.succeed("ok"), + execute: () => Effect.succeed("ok"), }) const expensive = Tool.make({ description: @@ -1081,7 +1081,7 @@ describe("CodeMode public contract", () => { anotherEvenLongerParameterName: Schema.Number, }), output: Schema.String, - run: () => Effect.succeed("ok"), + execute: () => Effect.succeed("ok"), }) // Round 1 places alpha.cheap (~17 estimated tokens) and beta.cheap (~17); in round 2 // alpha.expensive does not fit, which marks only alpha done - it must NOT prevent @@ -1112,7 +1112,7 @@ describe("CodeMode public contract", () => { }, required: ["id"], } as const, - run: () => Effect.succeed("ok"), + execute: () => Effect.succeed("ok"), }) const runtime = CodeMode.make({ tools: { records: { lookup: documented } }, @@ -1130,7 +1130,7 @@ describe("CodeMode public contract", () => { description: "Double a number", input: Schema.Struct({ value: Schema.NumberFromString }), output: Schema.NumberFromString, - run: ({ value }) => + execute: ({ value }) => Effect.sync(() => { observed.push(value) return String(value * 2) @@ -1226,7 +1226,7 @@ describe("CodeMode public contract", () => { description: "Count invocations", input: Schema.Struct({}), output: Schema.Number, - run: () => Effect.succeed(1), + execute: () => Effect.succeed(1), }) const result = await Effect.runPromise( CodeMode.execute({ diff --git a/packages/codemode/test/enumeration.test.ts b/packages/codemode/test/enumeration.test.ts index 116fbab0d9eb..981297c7f8f5 100644 --- a/packages/codemode/test/enumeration.test.ts +++ b/packages/codemode/test/enumeration.test.ts @@ -13,7 +13,7 @@ const echo = (description: string) => description, input: Schema.Struct({ value: Schema.String }), output: Schema.String, - run: ({ value }) => Effect.succeed(value), + execute: ({ value }) => Effect.succeed(value), }) const tools = { diff --git a/packages/codemode/test/openapi.test.ts b/packages/codemode/test/openapi.test.ts index f4da5c31e0d8..da705977bfae 100644 --- a/packages/codemode/test/openapi.test.ts +++ b/packages/codemode/test/openapi.test.ts @@ -143,12 +143,7 @@ describe("OpenAPI.fromSpec", () => { const remove = toolAt(api.tools, "users.remove") expect(api.skipped).toEqual([]) - if ( - !Tool.isDefinition(get) || - !Tool.isDefinition(create) || - !Tool.isDefinition(search) || - !Tool.isDefinition(remove) - ) { + if (!Tool.isTool(get) || !Tool.isTool(create) || !Tool.isTool(search) || !Tool.isTool(remove)) { throw new Error("happy-path fixture did not generate every operation") } expect(inputTypeScript(get)).toBe( @@ -241,23 +236,23 @@ describe("OpenAPI.fromSpec", () => { expect(toolAt(result.tools, "v2.session.create")).not.toBeUndefined() const sessionGet = toolAt(result.tools, "v2.session.get") - expect(Tool.isDefinition(sessionGet)).toBe(true) - if (!Tool.isDefinition(sessionGet)) throw new Error("v2.session.get was not generated") + expect(Tool.isTool(sessionGet)).toBe(true) + if (!Tool.isTool(sessionGet)) throw new Error("v2.session.get was not generated") expect(inputTypeScript(sessionGet)).toBe("{ sessionID: string }") expect(outputTypeScript(sessionGet)).toContain("id: string") expect(outputTypeScript(sessionGet)).toContain("additions: number") const switchAgent = toolAt(result.tools, "v2.session.switchAgent") - expect(Tool.isDefinition(switchAgent)).toBe(true) - if (!Tool.isDefinition(switchAgent)) throw new Error("v2.session.switchAgent was not generated") + expect(Tool.isTool(switchAgent)).toBe(true) + if (!Tool.isTool(switchAgent)) throw new Error("v2.session.switchAgent was not generated") expect(inputTypeScript(switchAgent)).toBe("{ sessionID: string; agent: string }") const instructionPut = toolAt(result.tools, "v2.session.instructions.entry.put") - expect(Tool.isDefinition(instructionPut)).toBe(true) - if (!Tool.isDefinition(instructionPut)) throw new Error("v2.session.instructions.entry.put was not generated") + expect(Tool.isTool(instructionPut)).toBe(true) + if (!Tool.isTool(instructionPut)) throw new Error("v2.session.instructions.entry.put was not generated") expect(inputTypeScript(instructionPut)).toBe("{ sessionID: string; key: string; value: unknown }") expect(toolAt(result.tools, "v2_session_instructions_entry_put_2")).toBeUndefined() - expect(Tool.isDefinition(toolAt(result.tools, "v2.pty.connect"))).toBe(false) + expect(Tool.isTool(toolAt(result.tools, "v2.pty.connect"))).toBe(false) expect(toolAt(result.tools, "v2.session.log")).toBeUndefined() expect(toolAt(result.tools, "v2.event.subscribe")).toBeUndefined() expect(toolAt(result.tools, "v2.fs.read")).toBeUndefined() @@ -278,9 +273,9 @@ describe("OpenAPI.fromSpec", () => { }, }) - expect(Tool.isDefinition(toolAt(result.tools, "group.item"))).toBe(true) - expect(Tool.isDefinition(toolAt(result.tools, "group_item_2"))).toBe(true) - expect(Tool.isDefinition(toolAt(result.tools, "group.operation.other"))).toBe(true) + expect(Tool.isTool(toolAt(result.tools, "group.item"))).toBe(true) + expect(Tool.isTool(toolAt(result.tools, "group_item_2"))).toBe(true) + expect(Tool.isTool(toolAt(result.tools, "group.operation.other"))).toBe(true) }) test("synthesizes flat operation IDs from methods and paths", () => { @@ -305,7 +300,7 @@ describe("OpenAPI.fromSpec", () => { "deleteUsersById", "getOrganizationsByOrganizationidUsersById", ]) { - expect(Tool.isDefinition(toolAt(tools, path))).toBe(true) + expect(Tool.isTool(toolAt(tools, path))).toBe(true) } }) @@ -330,7 +325,7 @@ describe("OpenAPI.fromSpec", () => { "test", ) - if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + if (!Tool.isTool(tool)) throw new Error("test was not generated") expect(inputTypeScript(tool)).toBe("{ limit: number }") }) @@ -358,8 +353,8 @@ describe("OpenAPI.fromSpec", () => { }) const search = toolAt(result.tools, "search") - expect(Tool.isDefinition(search)).toBe(true) - if (!Tool.isDefinition(search)) throw new Error("search was not generated") + expect(Tool.isTool(search)).toBe(true) + if (!Tool.isTool(search)) throw new Error("search was not generated") expect(inputTypeScript(search)).toBe("{ value?: string | null }") const schema: unknown = search.input const input = isRecord(schema) ? schema : {} @@ -397,14 +392,14 @@ describe("OpenAPI.fromSpec", () => { "test", ) - if (!Tool.isDefinition(tool) || !isRecord(tool.output)) throw new Error("test output was not generated") + if (!Tool.isTool(tool) || !isRecord(tool.output)) throw new Error("test output was not generated") expect(tool.output.$defs).toMatchObject({ Local: { type: "string" }, Global: { type: "number" } }) }) test("projects read-only and write-only properties by schema direction", () => { for (const version of ["3.0.3", "3.1.0"]) { const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec: directionalSpec(version) }).tools, "users.create") - if (!Tool.isDefinition(tool) || !isRecord(tool.input) || !isRecord(tool.output)) { + if (!Tool.isTool(tool) || !isRecord(tool.input) || !isRecord(tool.output)) { throw new Error(`users.create was not generated for OpenAPI ${version}`) } @@ -467,7 +462,7 @@ describe("OpenAPI.fromSpec", () => { }).tools, "test", ) - if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + if (!Tool.isTool(tool)) throw new Error("test was not generated") expect(inputTypeScript(tool)).toBe("{ name: string }") }) @@ -518,7 +513,7 @@ describe("OpenAPI.fromSpec", () => { }).tools, "test", ) - if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated") + if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated") const properties = isRecord(tool.input.properties) ? tool.input.properties : {} const record = isRecord(properties.record) ? properties.record : {} const definitions = isRecord(tool.input.$defs) ? tool.input.$defs : {} @@ -567,7 +562,7 @@ describe("OpenAPI.fromSpec", () => { }).tools, "test", ) - if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + if (!Tool.isTool(tool)) throw new Error("test was not generated") expect(inputTypeScript(tool)).toBe("{ name: string }") }) @@ -607,7 +602,7 @@ describe("OpenAPI.fromSpec", () => { }).tools, "test", ) - if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated") + if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated") const definitions = isRecord(tool.input.$defs) ? tool.input.$defs : {} const node = isRecord(definitions.Node) ? definitions.Node : {} @@ -648,7 +643,7 @@ describe("OpenAPI.fromSpec", () => { }).tools, "test", ) - if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated") + if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated") const definitions = isRecord(tool.input.$defs) ? tool.input.$defs : {} const leaf = isRecord(definitions[`C${depth - 1}`]) ? definitions[`C${depth - 1}`] : {} @@ -686,7 +681,7 @@ describe("OpenAPI.fromSpec", () => { }).tools, "test", ) - if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + if (!Tool.isTool(tool)) throw new Error("test was not generated") expect(inputTypeScript(tool)).toBe("{ name: string }") } @@ -725,7 +720,7 @@ describe("OpenAPI.fromSpec", () => { }).tools, "test", ) - if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated") + if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated") const properties = isRecord(tool.input.properties) ? tool.input.properties : {} const record: Record = isRecord(properties.record) ? properties.record : {} @@ -763,7 +758,7 @@ describe("OpenAPI.fromSpec", () => { }).tools, "test", ) - if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated") + if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated") const properties = isRecord(tool.input.properties) ? tool.input.properties : {} const choice: Record = isRecord(properties.choice) ? properties.choice : {} const pick: Record = isRecord(properties.pick) ? properties.pick : {} @@ -807,7 +802,7 @@ describe("OpenAPI.fromSpec", () => { }).tools, "test", ) - if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated") + if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated") const properties = isRecord(tool.input.properties) ? tool.input.properties : {} const record = isRecord(properties.record) ? properties.record : {} @@ -836,7 +831,7 @@ describe("OpenAPI.fromSpec", () => { }).tools, "test", ) - if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + if (!Tool.isTool(tool)) throw new Error("test was not generated") expect(inputTypeScript(tool)).toBe("{ filter: { state: string } }") }) @@ -866,7 +861,7 @@ describe("OpenAPI.fromSpec", () => { }).tools, "test", ) - if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + if (!Tool.isTool(tool)) throw new Error("test was not generated") expect(inputTypeScript(tool)).toBe("{ filter: { value: string } }") }) @@ -901,7 +896,7 @@ describe("OpenAPI.fromSpec", () => { }).tools, "test", ) - if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated") + if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated") const properties = isRecord(tool.input.properties) ? tool.input.properties : {} const body = isRecord(properties.body) ? properties.body : {} const allOf = Array.isArray(body.allOf) ? body.allOf : [] @@ -923,11 +918,11 @@ describe("OpenAPI.fromSpec", () => { }), ) const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec: directionalSpec("3.1.0") }).tools, "users.create") - if (!Tool.isDefinition(tool)) throw new Error("users.create was not generated") + if (!Tool.isTool(tool)) throw new Error("users.create was not generated") const result = await Effect.runPromise( tool - .run({ + .execute({ id: "ignored-top-level", generated: "ignored-generated", name: "Ada", @@ -1022,10 +1017,12 @@ describe("OpenAPI.fromSpec", () => { test("serializes deep-object query parameters from the opencode fixture", async () => { const client = recordingClient(() => json({ directory: "/tmp" })) const location = toolAt(OpenAPI.fromSpec({ spec: await opencodeSpec(), baseUrl }).tools, "v2.location.get") - if (!Tool.isDefinition(location)) throw new Error("v2.location.get was not generated") + if (!Tool.isTool(location)) throw new Error("v2.location.get was not generated") await Effect.runPromise( - location.run({ location: { directory: "/tmp", workspace: "workspace-1" } }).pipe(Effect.provide(client.layer)), + location + .execute({ location: { directory: "/tmp", workspace: "workspace-1" } }) + .pipe(Effect.provide(client.layer)), ) const url = new URL(client.requests[0]!.url) @@ -1058,11 +1055,11 @@ describe("OpenAPI.fromSpec", () => { }, }) const tool = toolAt(result.tools, "items") - if (!Tool.isDefinition(tool)) throw new Error("items was not generated") + if (!Tool.isTool(tool)) throw new Error("items was not generated") await Effect.runPromise( tool - .run({ + .execute({ keys: ["a!", "b*"], tags: ["x", "y"], filter: { state: "open", page: 2 }, @@ -1081,9 +1078,9 @@ describe("OpenAPI.fromSpec", () => { expect(url.searchParams.get("nullable")).toBe("null") expect(url.searchParams.get("constructor")).toBe("safe") expect(client.requests[0]!.headers.meta).toBe("a=b,c=d") - await expect(Effect.runPromise(tool.run({ keys: [undefined] }).pipe(Effect.provide(client.layer)))).rejects.toThrow( - "unsupported nested value", - ) + await expect( + Effect.runPromise(tool.execute({ keys: [undefined] }).pipe(Effect.provide(client.layer))), + ).rejects.toThrow("unsupported nested value") }) test("preserves ordered exploded and deep-object query parameters", async () => { @@ -1101,11 +1098,11 @@ describe("OpenAPI.fromSpec", () => { }).tools, "test", ) - if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + if (!Tool.isTool(tool)) throw new Error("test was not generated") await Effect.runPromise( tool - .run({ + .execute({ tags: ["first value", "second&value"], filter: { state: "open now", page: 2 }, location: { directory: "/tmp/a b", workspace: "work&1" }, @@ -1116,14 +1113,14 @@ describe("OpenAPI.fromSpec", () => { expect(client.requests[0]?.url).toBe( `${baseUrl}/test?tags=first+value&tags=second%26value&state=open+now&page=2&location%5Bdirectory%5D=%2Ftmp%2Fa+b&location%5Bworkspace%5D=work%261`, ) - await expect(Effect.runPromise(tool.run({ tags: [{}] }).pipe(Effect.provide(client.layer)))).rejects.toThrow( + await expect(Effect.runPromise(tool.execute({ tags: [{}] }).pipe(Effect.provide(client.layer)))).rejects.toThrow( "Parameter 'tags' contains an unsupported nested value.", ) await expect( - Effect.runPromise(tool.run({ filter: { state: {} } }).pipe(Effect.provide(client.layer))), + Effect.runPromise(tool.execute({ filter: { state: {} } }).pipe(Effect.provide(client.layer))), ).rejects.toThrow("Query parameter 'filter' contains an unsupported nested value.") await expect( - Effect.runPromise(tool.run({ location: { directory: [] } }).pipe(Effect.provide(client.layer))), + Effect.runPromise(tool.execute({ location: { directory: [] } }).pipe(Effect.provide(client.layer))), ).rejects.toThrow("Deep-object parameter 'location' contains an unsupported nested value.") expect(client.requests).toHaveLength(1) }) @@ -1203,9 +1200,9 @@ describe("OpenAPI.fromSpec", () => { }).tools, "getTest", ) - if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + if (!Tool.isTool(tool)) throw new Error("test was not generated") - await Effect.runPromise(tool.run({}).pipe(Effect.provide(client.layer))) + await Effect.runPromise(tool.execute({}).pipe(Effect.provide(client.layer))) expect(inputTypeScript(tool)).toBe("{}") expect(client.requests[0]!.headers.authorization).toBe("Bearer secret") @@ -1240,9 +1237,9 @@ describe("OpenAPI.fromSpec", () => { authenticated([{ key: [] }], { key: { type: "apiKey", in: "query", name: "__proto__" } }).tools, "test", ) - if (!Tool.isDefinition(prototype)) throw new Error("prototype auth tool was not generated") + if (!Tool.isTool(prototype)) throw new Error("prototype auth tool was not generated") - await Effect.runPromise(prototype.run({}).pipe(Effect.provide(client.layer))) + await Effect.runPromise(prototype.execute({}).pipe(Effect.provide(client.layer))) expect(new URL(client.requests[0]!.url).searchParams.get("__proto__")).toBe("secret") const duplicate = toolAt( @@ -1252,8 +1249,8 @@ describe("OpenAPI.fromSpec", () => { }).tools, "test", ) - if (!Tool.isDefinition(duplicate)) throw new Error("duplicate auth tool was not generated") - await expect(Effect.runPromise(duplicate.run({}).pipe(Effect.provide(client.layer)))).rejects.toThrow( + if (!Tool.isTool(duplicate)) throw new Error("duplicate auth tool was not generated") + await expect(Effect.runPromise(duplicate.execute({}).pipe(Effect.provide(client.layer)))).rejects.toThrow( "multiple credentials", ) @@ -1278,8 +1275,8 @@ describe("OpenAPI.fromSpec", () => { }, }) const alternativeTool = toolAt(alternative.tools, "test") - if (!Tool.isDefinition(alternativeTool)) throw new Error("supported auth alternative was not generated") - await Effect.runPromise(alternativeTool.run({}).pipe(Effect.provide(client.layer))) + if (!Tool.isTool(alternativeTool)) throw new Error("supported auth alternative was not generated") + await Effect.runPromise(alternativeTool.execute({}).pipe(Effect.provide(client.layer))) expect(client.requests.at(-1)?.headers.authorization).toBe("Bearer secret") }) @@ -1290,9 +1287,9 @@ describe("OpenAPI.fromSpec", () => { servers: [{ url: "https://document.example" }], } satisfies Document const tool = toolAt(OpenAPI.fromSpec({ spec }).tools, "test") - if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + if (!Tool.isTool(tool)) throw new Error("test was not generated") - await Effect.runPromise(tool.run({}).pipe(Effect.provide(client.layer))) + await Effect.runPromise(tool.execute({}).pipe(Effect.provide(client.layer))) expect(client.requests[0]?.url).toBe("https://operation.example/v1/test") const invalid = OpenAPI.fromSpec({ spec, baseUrl: "https://example.com/api?tenant=one" }) @@ -1363,10 +1360,10 @@ describe("OpenAPI.fromSpec", () => { }).tools, "test", ) - if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + if (!Tool.isTool(tool)) throw new Error("test was not generated") await expect( - Effect.runPromise(tool.run({ filter: { value: undefined } }).pipe(Effect.provide(client.layer))), + Effect.runPromise(tool.execute({ filter: { value: undefined } }).pipe(Effect.provide(client.layer))), ).rejects.toThrow("unsupported nested value") expect(resolutions).toEqual([]) expect(client.requests).toEqual([]) @@ -1389,33 +1386,33 @@ describe("OpenAPI.fromSpec", () => { }).tools, "test", ) - if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + if (!Tool.isTool(tool)) throw new Error("test was not generated") - await Effect.runPromise(tool.run({ body: { name: "updated" } }).pipe(Effect.provide(client.layer))) + await Effect.runPromise(tool.execute({ body: { name: "updated" } }).pipe(Effect.provide(client.layer))) expect(client.requests[0]!.headers["content-type"]).toBe("application/merge-patch+json") const cyclic: Record = {} cyclic.self = cyclic - await expect(Effect.runPromise(tool.run({ body: cyclic }).pipe(Effect.provide(client.layer)))).rejects.toThrow( + await expect(Effect.runPromise(tool.execute({ body: cyclic }).pipe(Effect.provide(client.layer)))).rejects.toThrow( "Invalid JSON body", ) }) test("rejects oversized and malformed JSON responses", async () => { const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec: singleOperation({}) }).tools, "test") - if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + if (!Tool.isTool(tool)) throw new Error("test was not generated") const oversized = recordingClient( () => new Response(null, { headers: { "content-length": String(50 * 1024 * 1024 + 1) } }), ) const malformed = recordingClient(() => new Response("{", { headers: { "content-type": "application/json" } })) const chunked = recordingClient(() => new Response(new Uint8Array(50 * 1024 * 1024 + 1))) - await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(oversized.layer)))).rejects.toThrow( + await expect(Effect.runPromise(tool.execute({}).pipe(Effect.provide(oversized.layer)))).rejects.toThrow( "response exceeds 50 MiB", ) - await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(malformed.layer)))).rejects.toThrow( + await expect(Effect.runPromise(tool.execute({}).pipe(Effect.provide(malformed.layer)))).rejects.toThrow( "returned malformed JSON", ) - await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(chunked.layer)))).rejects.toThrow( + await expect(Effect.runPromise(tool.execute({}).pipe(Effect.provide(chunked.layer)))).rejects.toThrow( "response exceeds 50 MiB", ) }) @@ -1428,11 +1425,11 @@ describe("OpenAPI.fromSpec", () => { }, }) const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec }).tools, "test") - if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + if (!Tool.isTool(tool)) throw new Error("test was not generated") const client = recordingClient(() => new Response("123", { headers: { "content-type": "text/plain" } })) expect(outputTypeScript(tool)).toBe("string | null") - await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(client.layer)))).resolves.toBe("123") + await expect(Effect.runPromise(tool.execute({}).pipe(Effect.provide(client.layer)))).resolves.toBe("123") }) test("fails missing required parameters before auth and network", async () => { @@ -1497,13 +1494,13 @@ describe("OpenAPI.fromSpec", () => { const update = toolAt(tools, "things.update") const echo = toolAt(tools, "echo") - expect(Tool.isDefinition(update)).toBe(true) - if (!Tool.isDefinition(update)) throw new Error("things.update was not generated") + expect(Tool.isTool(update)).toBe(true) + if (!Tool.isTool(update)) throw new Error("things.update was not generated") expect(inputTypeScript(update)).toBe( "{ path_id: string; query_id: string; path_id_2?: string; header_id: string; body_id: string }", ) - expect(Tool.isDefinition(echo)).toBe(true) - if (!Tool.isDefinition(echo)) throw new Error("echo was not generated") + expect(Tool.isTool(echo)).toBe(true) + if (!Tool.isTool(echo)) throw new Error("echo was not generated") expect(inputTypeScript(echo)).toBe("{ body: string }") const runtime = CodeMode.make({ tools }) @@ -1584,13 +1581,13 @@ describe("OpenAPI.fromSpec", () => { for (const name of ["optional", "dictionary", "composed", "nullable"]) { const tool = toolAt(tools, `body.${name}`) - expect(Tool.isDefinition(tool)).toBe(true) - if (!Tool.isDefinition(tool)) throw new Error(`body.${name} was not generated`) + expect(Tool.isTool(tool)).toBe(true) + if (!Tool.isTool(tool)) throw new Error(`body.${name} was not generated`) const input = isRecord(tool.input) ? tool.input : {} expect(Object.keys(isRecord(input.properties) ? input.properties : {})).toStrictEqual(["body"]) } const optional = toolAt(tools, "body.optional") - if (!Tool.isDefinition(optional)) throw new Error("body.optional was not generated") + if (!Tool.isTool(optional)) throw new Error("body.optional was not generated") expect(inputTypeScript(optional)).toBe("{ body?: { name: string } }") }) }) diff --git a/packages/codemode/test/promise.test.ts b/packages/codemode/test/promise.test.ts index f5b52eeefdda..63d8540e900e 100644 --- a/packages/codemode/test/promise.test.ts +++ b/packages/codemode/test/promise.test.ts @@ -33,7 +33,7 @@ const echoTool = (trace: Trace) => description: "Echo an id immediately", input: Schema.Struct({ id: Schema.Number }), output: Schema.Number, - run: ({ id }) => + execute: ({ id }) => Effect.sync(() => { trace.starts.push(id) trace.completed += 1 @@ -46,7 +46,7 @@ const gatedTool = (trace: Trace, gate: (id: number) => Deferred.Deferred) description: "Echo an id once its gate opens", input: Schema.Struct({ id: Schema.Number }), output: Schema.Number, - run: ({ id }) => + execute: ({ id }) => Effect.gen(function* () { trace.starts.push(id) trace.active += 1 @@ -70,7 +70,7 @@ const openTool = (gate: (id: number) => Deferred.Deferred) => description: "Open the gate for an id", input: Schema.Struct({ id: Schema.Number }), output: Schema.Boolean, - run: ({ id }) => Deferred.succeed(gate(id), undefined), + execute: ({ id }) => Deferred.succeed(gate(id), undefined), }) const pendingTool = (trace: Trace) => @@ -78,7 +78,7 @@ const pendingTool = (trace: Trace) => description: "Never settle", input: Schema.Struct({ id: Schema.Number }), output: Schema.Number, - run: ({ id }) => + execute: ({ id }) => Effect.gen(function* () { trace.starts.push(id) trace.active += 1 @@ -98,14 +98,14 @@ const failingTool = Tool.make({ description: "Always refuse", input: Schema.Struct({}), output: Schema.String, - run: () => Effect.fail(toolError("Lookup refused")), + execute: () => Effect.fail(toolError("Lookup refused")), }) const interruptedTool = Tool.make({ description: "Interrupt this call", input: Schema.Struct({}), output: Schema.String, - run: () => Effect.interrupt, + execute: () => Effect.interrupt, }) const completedTool = (trace: Trace) => @@ -113,7 +113,7 @@ const completedTool = (trace: Trace) => description: "Return the number of completed calls", input: Schema.Struct({}), output: Schema.Number, - run: () => Effect.succeed(trace.completed), + execute: () => Effect.succeed(trace.completed), }) /** Never settles, and holds interruption cleanup for `cleanupMs` so completion cleanup can outlast a timeout. */ @@ -122,7 +122,7 @@ const stubbornTool = (trace: Trace) => description: "Never settle; clean up slowly when interrupted", input: Schema.Struct({ cleanupMs: Schema.Number }), output: Schema.Number, - run: ({ cleanupMs }) => + execute: ({ cleanupMs }) => Effect.never.pipe( Effect.onInterrupt(() => Effect.andThen( diff --git a/packages/codemode/test/signature.test.ts b/packages/codemode/test/signature.test.ts index dea0e890ac5d..38121e5c6ef0 100644 --- a/packages/codemode/test/signature.test.ts +++ b/packages/codemode/test/signature.test.ts @@ -18,7 +18,8 @@ const listIssues = Tool.make({ }, required: ["owner"], }, - run: () => Effect.succeed("[]"), + output: {}, + execute: () => Effect.succeed("[]"), }) // An Effect Schema tool whose field annotations must flow through the emitted JSON Schema. @@ -31,7 +32,7 @@ const lookupOrder = Tool.make({ output: Schema.Struct({ status: Schema.String.annotate({ description: "Current order status" }), }), - run: () => Effect.succeed({ status: "open" }), + execute: () => Effect.succeed({ status: "open" }), }) describe("pretty signature rendering", () => { @@ -261,7 +262,7 @@ describe("non-identifier property names render as quoted keys", () => { properties: { "content-type": { type: "string" } }, required: ["content-type"], } as const, - run: () => Effect.succeed({ "content-type": "text/plain" }), + execute: () => Effect.succeed({ "content-type": "text/plain" }), }) expect(inputTypeScript(tool)).toContain('"foo-bar"?: string') expect(outputTypeScript(tool)).toBe('{ "content-type": string }') @@ -272,7 +273,7 @@ describe("non-identifier property names render as quoted keys", () => { const tool = Tool.make({ description: "Schema tool with awkward field names", input: Schema.Struct({ "foo-bar": Schema.String, plain: Schema.optionalKey(Schema.Number) }), - run: () => Effect.succeed(null), + execute: () => Effect.succeed(null), }) expect(inputTypeScript(tool)).toBe('{ "foo-bar": string; plain?: number }') expect(inputTypeScript(tool, true)).toBe(["{", ' "foo-bar": string,', " plain?: number,", "}"].join("\n")) @@ -306,7 +307,7 @@ describe("union schemas render every alternative", () => { }, } as const, output: { anyOf: [{ type: "number" }, { type: "boolean" }] } as const, - run: () => Effect.succeed(1), + execute: () => Effect.succeed(1), }) expect(inputTypeScript(tool)).toBe("{ value?: string | number }") expect(outputTypeScript(tool)).toBe("number | boolean") @@ -417,7 +418,8 @@ describe("non-identifier tool paths", () => { }, required: ["query", "libraryName"], } as const, - run: () => Effect.succeed("/reactjs/react.dev"), + output: {}, + execute: () => Effect.succeed("/reactjs/react.dev"), }) const runtime = CodeMode.make({ tools: { context7: { "resolve-library-id": resolveLibrary } } }) diff --git a/packages/codemode/test/stdlib.test.ts b/packages/codemode/test/stdlib.test.ts index 19989b75a5f6..e3a840fd0ffe 100644 --- a/packages/codemode/test/stdlib.test.ts +++ b/packages/codemode/test/stdlib.test.ts @@ -329,7 +329,7 @@ describe("RegExp", () => { description: "Decorate a string", input: Schema.String, output: Schema.String, - run: (input) => Effect.succeed(`[${input}]`), + execute: (input) => Effect.succeed(`[${input}]`), }) const result = await Effect.runPromise( CodeMode.execute({ @@ -1028,7 +1028,7 @@ describe("CodeMode values at intra-CodeMode checkpoints", () => { const capture = Tool.make({ description: "Capture the exact input the host receives", input: { type: "object" }, - run: (input) => + execute: (input) => Effect.sync(() => { observed.push(input) return "ok" diff --git a/packages/codemode/test/tool-paths.test.ts b/packages/codemode/test/tool-paths.test.ts index c92739c5df3f..91d23cdee71e 100644 --- a/packages/codemode/test/tool-paths.test.ts +++ b/packages/codemode/test/tool-paths.test.ts @@ -7,7 +7,7 @@ const echo = (description: string, result: string) => description, input: Schema.Struct({}), output: Schema.String, - run: () => Effect.succeed(result), + execute: () => Effect.succeed(result), }) const value = async (runtime: CodeMode.Runtime, code: string) => { @@ -88,7 +88,7 @@ describe("callable namespaces", () => { expect(diagnostic.message).toContain("Unknown tool 'issues.missing'") }) - test("a namespace without its own definition stays non-callable", async () => { + test("a namespace without its own tool stays non-callable", async () => { const nested = CodeMode.make({ tools: { "issues.list": echo("List issues", "list") } }) const diagnostic = await failure(nested, `return await tools.issues({})`) expect(diagnostic.kind).toBe("UnknownTool") @@ -114,9 +114,9 @@ describe("blocked member names on tool paths", () => { expect(await value(runtime, `return Object.keys(tools.issues)`)).toEqual(["constructor"]) }) - test("a literal __proto__ key cannot poison a namespace into a fake definition", async () => { + test("a literal __proto__ key cannot poison a namespace into a fake tool", async () => { const poisoned = CodeMode.make({ - tools: { ns: { "__proto__": echo("Hidden", "hidden"), real: echo("Real tool", "real") } }, + tools: { ns: { __proto__: echo("Hidden", "hidden"), real: echo("Real tool", "real") } }, }) expect(poisoned.catalog().map((tool) => tool.path)).toEqual(["ns.real"]) expect(await value(poisoned, `return await tools.ns.real({})`)).toBe("real") @@ -138,7 +138,7 @@ describe("empty segments", () => { }) describe("canonical path collisions", () => { - test("the last definition supplied for a canonical path wins", async () => { + test("the last tool supplied for a canonical path wins", async () => { const runtime = CodeMode.make({ tools: { "issues.list": echo("First", "first"), issues: { list: echo("Second", "second") } }, }) diff --git a/packages/core/src/codemode.ts b/packages/core/src/codemode.ts index cd7e9ee5573c..7d2dd2c57085 100644 --- a/packages/core/src/codemode.ts +++ b/packages/core/src/codemode.ts @@ -4,19 +4,17 @@ import { Context, Effect, Layer, Scope } from "effect" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { PermissionV2 } from "./permission" import { ExecuteTool } from "./tool/execute" -import { permission, registrationEntries, type AnyTool } from "./tool/tool" -import { Tools } from "./tool/tools" +import type { Any, Registration } from "./tool/tool" import { Wildcard } from "./util/wildcard" export interface Materialization { - readonly tool?: AnyTool + readonly tool?: Any readonly instructions?: string } export interface Interface { readonly register: ( - tools: Readonly>, - options?: Tools.RegisterOptions, + registrations: ReadonlyArray, ) => Effect.Effect readonly materialize: (permissions?: PermissionV2.Ruleset) => Effect.Effect } @@ -26,29 +24,28 @@ export class Service extends Context.Service()("@opencode/v2 const layer = Layer.effect( Service, Effect.gen(function* () { - const local = new Map< - string, - Array<{ readonly token: object; readonly registration: ExecuteTool.Registration }> - >() + const local = new Map>() return Service.of({ - register: Effect.fn("CodeMode.register")(function* (tools, options) { - const entries = registrationEntries(tools, options?.namespace) - if (entries.length === 0) return + register: Effect.fn("CodeMode.register")(function* (registrations) { + if (registrations.length === 0) return yield* Effect.uninterruptible( Effect.gen(function* () { const token = {} - for (const entry of entries) - local.set(entry.key, [ - ...(local.get(entry.key) ?? []), - { token, registration: { tool: entry.tool, name: entry.name, namespace: entry.namespace } }, + for (const registration of registrations) + local.set(registration.key, [ + ...(local.get(registration.key) ?? []), + { + token, + registration, + }, ]) yield* Effect.addFinalizer(() => Effect.sync(() => { - for (const entry of entries) { - const registrations = local.get(entry.key)?.filter((item) => item.token !== token) ?? [] - if (registrations.length > 0) local.set(entry.key, registrations) - else local.delete(entry.key) + for (const registration of registrations) { + const remaining = local.get(registration.key)?.filter((item) => item.token !== token) ?? [] + if (remaining.length > 0) local.set(registration.key, remaining) + else local.delete(registration.key) } }), ) @@ -61,7 +58,7 @@ const layer = Layer.effect( for (const [name, entries] of local) { const registration = entries.at(-1)?.registration if (!registration) continue - const rule = rules.findLast((rule) => Wildcard.match(permission(registration.tool, name), rule.action)) + const rule = rules.findLast((rule) => Wildcard.match(registration.permission, rule.action)) if (rule?.resource === "*" && rule.effect === "deny") continue registrations.set(name, registration) } diff --git a/packages/core/src/database/migration.gen.ts b/packages/core/src/database/migration.gen.ts index dd6214b095a6..a7eaed9b7326 100644 --- a/packages/core/src/database/migration.gen.ts +++ b/packages/core/src/database/migration.gen.ts @@ -56,5 +56,6 @@ export const migrations = ( import("./migration/20260710025429_instruction_sync"), import("./migration/20260716020354_kv"), import("./migration/20260722011141_delete_tool_progress_events"), + import("./migration/20260722170000_canonical_tool_results"), ]) ).map((module) => module.default) satisfies DatabaseMigration.Migration[] diff --git a/packages/core/src/database/migration/20260722170000_canonical_tool_results.ts b/packages/core/src/database/migration/20260722170000_canonical_tool_results.ts new file mode 100644 index 000000000000..2cb656cd0265 --- /dev/null +++ b/packages/core/src/database/migration/20260722170000_canonical_tool_results.ts @@ -0,0 +1,123 @@ +import { sql } from "drizzle-orm" +import { Effect, Schema } from "effect" +import type { DatabaseMigration } from "../migration" + +const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString) +const isObject = Schema.is(Schema.Record(Schema.String, Schema.Unknown)) +const isJsonObject = Schema.is(Schema.Record(Schema.String, Schema.Json)) + +const object = (value: unknown): Record => (isObject(value) ? value : {}) + +const stringify = (value: unknown) => { + try { + return JSON.stringify(value, null, 2) ?? String(value) + } catch { + return String(value) + } +} + +const contentOf = (state: Record) => (Array.isArray(state.content) ? state.content : []) +const resultOf = (state: Record) => + isObject(state.result) && "value" in state.result ? state.result.value : state.result +const metadataOf = (state: Record) => { + if (isJsonObject(state.structured) && Object.keys(state.structured).length > 0) + return { metadata: state.structured } + return isJsonObject(state.metadata) ? { metadata: state.metadata } : {} +} +const completedContent = (state: Record) => { + const preserved = contentOf(state) + if (preserved.length > 0) return preserved + return [{ type: "text", text: stringify(Object.keys(object(state.structured)).length ? state.structured : resultOf(state)) }] +} + +/** + * One-time rewrite of projected tool rows into the canonical result shape: + * terminal states store model content plus optional metadata; the generic + * `structured` and `result` fields disappear. Provider-hosted result payloads + * move into provider-owned result state so hosted continuation survives. + * Pre-release durable event versions are intentionally left untouched. + */ +export default { + id: "20260722170000_canonical_tool_results", + up(tx) { + return Effect.gen(function* () { + // Keyset-paginated batches keep memory bounded: production databases hold + // gigabytes of assistant rows, and materializing them all at once was + // measured at a ~5GB RSS spike. + let cursor = "" + while (true) { + const messages = yield* tx.all<{ id: string; data: string }>( + sql`SELECT id, data FROM session_message WHERE type = 'assistant' AND id > ${cursor} ORDER BY id LIMIT 1000`, + ) + if (messages.length === 0) break + cursor = messages[messages.length - 1].id + yield* rewrite(tx, messages) + } + }) + }, +} satisfies DatabaseMigration.Migration + +function rewrite(tx: Parameters[0], messages: { id: string; data: string }[]) { + return Effect.gen(function* () { + for (const row of messages) { + // A row that never decoded is skipped rather than failing the whole + // migration on every startup; it was equally unreadable before. + const decoded = decodeJson(row.data) + if (decoded._tag === "None") { + yield* Effect.logWarning("skipping undecodable session_message row").pipe(Effect.annotateLogs({ id: row.id })) + continue + } + const data = object(decoded.value) + if (!Array.isArray(data.content)) continue + let changed = false + const content = data.content.map((part) => { + const tool = object(part) + if (tool.type !== "tool" || !isObject(tool.state)) return part + const state = tool.state + if (state.status !== "completed" && state.status !== "error" && state.status !== "running") return part + if (!("structured" in state) && !("result" in state)) return part + changed = true + if (state.status === "running") + return { + ...tool, + state: { + status: "running", + input: object(state.input), + metadata: object(state.structured), + }, + } + // Hosted payloads are irreducible provider replay state; keep them under + // the provider-owned result state instead of a generic result field. + const hosted = + tool.executed === true && isObject(state.result) && "value" in state.result + ? { providerResultState: { ...object(tool.providerResultState), result: state.result.value } } + : {} + const preserved = contentOf(state) + if (state.status === "completed") + return { + ...tool, + ...hosted, + state: { + status: "completed", + input: object(state.input), + content: completedContent(state), + ...metadataOf(state), + }, + } + return { + ...tool, + ...hosted, + state: { + status: "error", + input: object(state.input), + error: state.error, + ...(preserved.length > 0 ? { content: preserved } : {}), + ...metadataOf(state), + }, + } + }) + if (!changed) continue + yield* tx.run(sql`UPDATE session_message SET data = ${JSON.stringify({ ...data, content })} WHERE id = ${row.id}`) + } + }) +} diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index a97b6bf0bb45..7761aa18235d 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -127,8 +127,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int ), }, model: { - get: (providerID, modelID) => - catalog.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)), + get: (providerID, modelID) => catalog.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)), list: () => response(catalog.model.available()), default: () => response(catalog.model.default()), }, @@ -358,7 +357,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int Effect.gen(function* () { const registrations: Array<{ readonly name: string - readonly tool: Tool.AnyTool + readonly tool: Tool.Any readonly options?: Tool.RegisterOptions }> = [] yield* Effect.sync(() => @@ -395,25 +394,42 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int }) } return toolHooks.hook.after((event) => { - const output = { + // JS plugin boundary: marshal the canonical outcome out, copy mutations back. + const output: Record = { tool: event.tool, sessionID: event.sessionID, agent: event.agent, messageID: event.messageID, callID: event.callID, input: event.input, - result: event.result, - output: event.output, + status: event.status, + content: event.content, + metadata: event.metadata, outputPaths: event.outputPaths, + ...(event.status === "error" ? { error: event.error } : {}), } return Reflect.apply(callback, undefined, [output]).pipe( - Effect.tap(() => - Effect.sync(() => { - event.result = output.result - event.output = output.output - event.outputPaths = output.outputPaths - }), - ), + Effect.tap(() => { + const decoded = Schema.decodeUnknownOption(Tool.ExecuteAfterOutcome)(output) + if (decoded._tag === "None") + return Effect.logWarning("ignoring invalid execute.after tool outcome", { tool: event.tool }) + if (decoded.value.status !== event.status) + return Effect.logWarning("ignoring execute.after tool status change", { tool: event.tool }) + return Effect.sync(() => { + if (event.status === "completed" && decoded.value.status === "completed") { + if (output.content !== event.content) event.content = decoded.value.content + if (output.metadata !== event.metadata) event.metadata = decoded.value.metadata + if (output.outputPaths !== event.outputPaths) event.outputPaths = decoded.value.outputPaths + return + } + if (event.status === "error" && decoded.value.status === "error") { + if (output.error !== event.error) event.error = decoded.value.error + if (output.content !== event.content) event.content = decoded.value.content + if (output.metadata !== event.metadata) event.metadata = decoded.value.metadata + if (output.outputPaths !== event.outputPaths) event.outputPaths = decoded.value.outputPaths + } + }) + }), ) }) }, diff --git a/packages/core/src/plugin/promise.ts b/packages/core/src/plugin/promise.ts index e54365e79f6d..950f6a2afcb9 100644 --- a/packages/core/src/plugin/promise.ts +++ b/packages/core/src/plugin/promise.ts @@ -2,7 +2,7 @@ export * as PluginPromise from "./promise" import { define } from "@opencode-ai/plugin/v2/effect/plugin" import type { Context, Plugin } from "@opencode-ai/plugin/v2/plugin" -import type { AnyTool } from "@opencode-ai/plugin/v2/tool" +import type { Any, RegisterOptions } from "@opencode-ai/plugin/v2/tool" import { Agent } from "@opencode-ai/schema/agent" import { Integration } from "@opencode-ai/schema/integration" import { Location } from "@opencode-ai/schema/location" @@ -189,7 +189,8 @@ export function fromPromise(plugin: Plugin) { register( host.tool.transform((draft) => callback({ - add: (tool: AnyTool) => draft.add(tool.name, fromPromiseTool(tool), tool.options), + add: (name: string, tool: Any, options?: RegisterOptions) => + draft.add(name, fromPromiseTool(tool), options), }), ), ), @@ -302,19 +303,8 @@ function wireEvent(value: unknown): unknown { return wire(value) } -function fromPromiseTool(tool: AnyTool) { - if ("jsonSchema" in tool) - return Tool.make({ - ...tool, - execute: (input, context) => - Effect.promise(() => - tool.execute(input, { - ...context, - progress: (update) => Effect.runPromise(context.progress(update)), - }), - ), - }) - return Tool.make({ +function fromPromiseTool(tool: Any): Tool.Any { + return { ...tool, execute: (input, context) => Effect.promise(() => @@ -323,5 +313,5 @@ function fromPromiseTool(tool: AnyTool) { progress: (update) => Effect.runPromise(context.progress(update)), }), ), - }) + } } diff --git a/packages/core/src/session/generate-node.ts b/packages/core/src/session/generate-node.ts index 96804fc57f74..0158760c3bf1 100644 --- a/packages/core/src/session/generate-node.ts +++ b/packages/core/src/session/generate-node.ts @@ -36,8 +36,8 @@ export const layer = Layer.effect( const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(selection.session.id) ? selection.session.id.slice(4) : selection.session.id - const executableTools = yield* registry.materialize(selection.agent.info.permissions) - const toolDefinitions = executableTools.definitions + const toolSet = yield* registry.snapshot(selection.agent.info.permissions) + const toolDefinitions = toolSet.definitions const toolsByName = new Map(toolDefinitions.map((tool) => [tool.name, tool])) const contextEvent = yield* hooks.trigger("session", "context", { sessionID: selection.session.id, @@ -52,7 +52,10 @@ export const layer = Layer.effect( Message.user(input.prompt), ], tools: Object.fromEntries( - toolDefinitions.map((tool) => [tool.name, { description: tool.description, input: { ...tool.inputSchema } }]), + toolDefinitions.map((tool) => [ + tool.name, + { description: tool.description, input: { ...tool.inputSchema } }, + ]), ), }) const hookedTools = Object.entries(contextEvent.tools).flatMap(([name, tool]) => { diff --git a/packages/core/src/session/message-updater.ts b/packages/core/src/session/message-updater.ts index ecc6bf6257a3..3a256e043a88 100644 --- a/packages/core/src/session/message-updater.ts +++ b/packages/core/src/session/message-updater.ts @@ -355,8 +355,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { SessionMessage.ToolStateRunning.make({ status: "running", input: event.data.input, - structured: {}, - content: [], + metadata: {}, }), ) } @@ -366,11 +365,12 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { const match = latestTool(draft, event.data.callID) if (match && match.state.status === "running") { - match.state.structured = event.data.structured - match.state.content = [...event.data.content] + match.state.metadata = event.data.metadata } }) }, + // Terminal tool events are self-contained; projection is a direct copy and + // never reaches into ephemeral progress history. "session.tool.success": (event) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { const match = latestTool(draft, event.data.callID) @@ -382,9 +382,8 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { SessionMessage.ToolStateCompleted.make({ status: "completed", input: match.state.input, - structured: event.data.structured, - content: [...event.data.content], - result: event.data.result, + content: event.data.content, + ...(event.data.metadata === undefined ? {} : { metadata: event.data.metadata }), }), ) } @@ -402,9 +401,8 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { status: "error", error: event.data.error, input: typeof match.state.input === "string" ? {} : match.state.input, - structured: event.data.metadata ?? (match.state.status === "running" ? match.state.structured : {}), - content: event.data.content ?? (match.state.status === "running" ? match.state.content : []), - result: event.data.result, + ...(event.data.content === undefined ? {} : { content: event.data.content }), + ...(event.data.metadata === undefined ? {} : { metadata: event.data.metadata }), }), ) } diff --git a/packages/core/src/session/model-request.ts b/packages/core/src/session/model-request.ts index 6afe3318ac82..f7fcb95adb00 100644 --- a/packages/core/src/session/model-request.ts +++ b/packages/core/src/session/model-request.ts @@ -14,13 +14,15 @@ import { MAX_STEPS_PROMPT } from "./runner/max-steps" import PROMPT_DEFAULT from "./runner/prompt/base.txt" import { toLLMMessages } from "./runner/to-llm-message" -type ToolCallResolution = - | { readonly type: "reject"; readonly error: SessionError.Error } - | { readonly type: "settle"; readonly settle: ToolRegistry.Materialization["settle"] } - interface Prepared { readonly request: LLMRequest - readonly resolveToolCall: (name: string) => ToolCallResolution + /** + * One request-scoped execution operation. Unknown, hook-removed, and + * step-limit-violating calls fail individually through the same seam. + */ + readonly executeTool: ToolRegistry.ToolSet["execute"] + /** True when this request is the final Step; violating calls are rejected and no continuation follows. */ + readonly stepLimitReached: boolean } interface PrepareInput { @@ -94,14 +96,16 @@ export const layer = Layer.effect( const model = resolved.model const providerMetadataKey = model.route.providerMetadataKey ?? model.provider const stepLimitReached = agent.info.steps !== undefined && input.step >= agent.info.steps - const executableTools = stepLimitReached ? undefined : yield* registry.materialize(agent.info.permissions) + // The final Step keeps definitions available to protocols with native "none", + // preserving their prompt cache prefix. Calls are still rejected at execution. + const toolSet = yield* registry.snapshot(agent.info.permissions) const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id const system = [agent.info.system ? agent.info.system : PROMPT_DEFAULT, input.context.initial] .filter((part) => part.length > 0) .map(SystemPart.make) const history = toLLMMessages(input.context.messages, resolved.ref, providerMetadataKey) const messages = stepLimitReached ? [...history, Message.assistant(MAX_STEPS_PROMPT)] : history - const toolDefinitions = executableTools?.definitions ?? [] + const toolDefinitions = toolSet.definitions const toolsByName = new Map(toolDefinitions.map((tool) => [tool.name, tool])) // Hooks may reshape available definitions but cannot advertise tools omitted by permissions or the Step limit. const contextEvent = yield* hooks.trigger("session", "context", { @@ -131,22 +135,23 @@ export const layer = Layer.effect( tools: hookedTools, toolChoice: stepLimitReached ? "none" : undefined, }) - const resolveToolCall = (name: string): ToolCallResolution => { - if (!executableTools) - return { - type: "reject", + const executeTool: ToolRegistry.ToolSet["execute"] = (executeInput) => { + if (stepLimitReached) + return Effect.succeed({ + status: "error", error: { type: "tool.execution", message: "Tools are disabled after the maximum agent steps" }, - } - if (toolsByName.has(name) && !Object.hasOwn(contextEvent.tools, name)) - return { - type: "reject", - error: { type: "tool.execution", message: `Tool is not available for this request: ${name}` }, - } - return { type: "settle", settle: executableTools.settle } + }) + if (toolsByName.has(executeInput.call.name) && !Object.hasOwn(contextEvent.tools, executeInput.call.name)) + return Effect.succeed({ + status: "error", + error: { type: "tool.unknown", message: `Tool is not available for this request: ${executeInput.call.name}` }, + }) + return toolSet.execute(executeInput) } return { request, - resolveToolCall, + executeTool, + stepLimitReached, } }) diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index ad792ce1c74d..3d92a5c8134f 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -144,21 +144,18 @@ const layer = Layer.effect( } yield* publish(event) if (LLMEvent.is.toolInputError(event)) { - if (prepared.resolveToolCall(event.name).type === "settle") needsContinuation = true + if (!prepared.stepLimitReached) needsContinuation = true return } if (event.type !== "tool-call" || event.providerExecuted) return - const tool = prepared.resolveToolCall(event.name) - if (tool.type === "reject") { - yield* serialized(publisher.failUnsettledTools(tool.error)) - return - } - needsContinuation = true + // Unavailable calls fail individually through the same execution seam; + // continuation depends only on remaining Step allowance. + if (!prepared.stepLimitReached) needsContinuation = true const assistantMessageID = yield* publisher.assistantMessageID(event.id) ownedToolFibers.push( yield* Effect.uninterruptibleMask((restore) => restore( - tool.settle({ + prepared.executeTool({ sessionID: session.id, agent: agent.id, messageID: assistantMessageID, @@ -166,17 +163,7 @@ const layer = Layer.effect( progress: (update) => serialized(publisher.progress(event.id, update)), }), ).pipe( - Effect.flatMap((settlement) => - publish( - LLMEvent.toolResult({ - id: event.id, - name: event.name, - result: settlement.result, - output: settlement.output, - }), - settlement.error, - ), - ), + Effect.flatMap((execution) => serialized(publisher.toolExecution(event.id, event.name, execution))), ), ).pipe(FiberSet.run(toolFibers)), ) diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts index df3e95089597..f84253c39d2a 100644 --- a/packages/core/src/session/runner/publish-llm-event.ts +++ b/packages/core/src/session/runner/publish-llm-event.ts @@ -1,5 +1,5 @@ -import { ToolOutput, type LLMEvent, type ProviderMetadata, type ToolResultValue } from "@opencode-ai/ai" -import { Effect } from "effect" +import { type LLMEvent, type ProviderMetadata, type ToolContent, type ToolResultValue } from "@opencode-ai/ai" +import { Effect, Schema } from "effect" import { EventV2 } from "../../event" import { ModelV2 } from "../../model" import { SessionEvent } from "../event" @@ -11,6 +11,8 @@ import { AgentV2 } from "../../agent" import { Snapshot } from "../../snapshot" import { RelativePath } from "../../schema" import { SessionUsage } from "../usage" +import { Tool } from "../../tool/tool" +import { MAX_BYTES } from "../../tool-output-store" import type { ToolRegistry } from "../../tool/registry" type Input = { @@ -25,24 +27,11 @@ type Input = { const record = (value: unknown): Record => typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record) : { value } -const message = (value: unknown) => { - if (typeof value === "string") return value - try { - return JSON.stringify(value) ?? String(value) - } catch { - return String(value) - } -} - -type SettledOutput = - | { readonly structured: Record; readonly content: ToolOutput["content"] } - | { readonly error: SessionError.Error } - -const settledOutput = (value: ToolOutput | undefined, result: ToolResultValue): SettledOutput => { - if (result.type === "error") return { error: { type: "tool.execution", message: message(result.value) } } - const settled = value ?? ToolOutput.fromResultValue(result) - if (!settled) throw new Error(`Unsupported tool result: ${message(result)}`) - return { structured: record(settled.structured), content: settled.content } +/** Derives canonical model content from a provider-hosted tool result. */ +const hostedContent = (result: ToolResultValue): readonly [ToolContent, ...ToolContent[]] => { + if (result.type === "content" && result.value.length > 0) + return result.value as unknown as readonly [ToolContent, ...ToolContent[]] + return [{ type: "text", text: Tool.stringify(result.value) }] } /** Persist one step without executing tools or starting a continuation step. */ @@ -60,11 +49,8 @@ export const createLLMEventPublisher = (events: Pick() const failureSnapshot = (tool: { readonly progress?: ToolRegistry.Progress }) => { if (!tool.progress) return {} - const first = tool.progress.content[0] - return { - ...(first === undefined ? {} : { content: [first, ...tool.progress.content.slice(1)] as const }), - metadata: tool.progress.structured, - } + const metadata = Tool.jsonMetadata(tool.progress, MAX_BYTES) + return metadata === undefined ? {} : { metadata } } let assistantMessageID = input.assistantMessageID let stepStarted = false @@ -254,11 +240,7 @@ export const createLLMEventPublisher = (events: Pick ${event.name}`)) if (tool.settled) { + // A late error result is a benign straggler (e.g. after an abort + // sweep); a late success would mean double execution, so it dies. if (event.result.type === "error") return return yield* Effect.die(new Error(`Duplicate tool result: ${event.id}`)) } tool.settled = true - const result = error ? { error } : settledOutput(event.output, event.result) const executed = event.providerExecuted === true || tool.providerExecuted const resultState = providerState(event.providerMetadata) - if ("error" in result) { + if (error !== undefined || event.result.type === "error") { yield* events.publish(SessionEvent.Tool.Failed, { sessionID: input.sessionID, assistantMessageID: tool.assistantMessageID, callID: event.id, - error: result.error, + error: error ?? { type: "tool.execution", message: Tool.stringify(event.result.value) }, ...failureSnapshot(tool), - result: event.result, executed, resultState, }) @@ -438,8 +421,7 @@ export const createLLMEventPublisher = (events: Pick ${name}`)) + if (tool.settled) { + if (execution.status === "error") return + return yield* Effect.die(new Error(`Duplicate tool execution: ${callID}`)) + } + tool.settled = true + if (execution.status === "completed") { + yield* events.publish(SessionEvent.Tool.Success, { + sessionID: input.sessionID, + assistantMessageID: tool.assistantMessageID, + callID, + content: execution.content, + ...(execution.metadata === undefined ? {} : { metadata: execution.metadata }), + executed: tool.providerExecuted, + }) + return + } + // An execution-provided snapshot wins; otherwise fall back to retained progress. + const snapshot = + execution.content !== undefined || execution.metadata !== undefined + ? { + ...(execution.content === undefined ? {} : { content: execution.content }), + ...(execution.metadata === undefined ? {} : { metadata: execution.metadata }), + } + : failureSnapshot(tool) + yield* events.publish(SessionEvent.Tool.Failed, { + sessionID: input.sessionID, + assistantMessageID: tool.assistantMessageID, + callID, + error: execution.error, + ...snapshot, + executed: tool.providerExecuted, }) }) return { publish, progress, + toolExecution, flush, failAssistant, publishStepFailure, diff --git a/packages/core/src/session/runner/to-llm-message.ts b/packages/core/src/session/runner/to-llm-message.ts index 1c875387d0bb..bf115c4ff1e4 100644 --- a/packages/core/src/session/runner/to-llm-message.ts +++ b/packages/core/src/session/runner/to-llm-message.ts @@ -1,11 +1,4 @@ -import { - Message, - ToolCallPart, - ToolOutput, - ToolResultPart, - type ContentPart, - type ProviderMetadata, -} from "@opencode-ai/ai" +import { Message, ToolCallPart, ToolResultPart, type ContentPart, type ProviderMetadata } from "@opencode-ai/ai" import { Option, Schema } from "effect" import type { ModelV2 } from "../../model" import { SessionMessage } from "../message" @@ -90,15 +83,15 @@ const toolCall = (tool: SessionMessage.AssistantTool, providerMetadata: Provider const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: ProviderMetadata | undefined) => { if (tool.state.status === "completed") { // TODO: Materialize remote and managed URIs before provider-history lowering. - // ToolOutput.toResultValue rejects unresolved URIs rather than treating them as media bytes. - const result = - tool.executed === true && tool.state.result !== undefined - ? tool.state.result - : ToolOutput.toResultValue({ structured: tool.state.structured, content: tool.state.content }) + const content = tool.state.content + const single = content.length === 1 ? content[0] : undefined return ToolResultPart.make({ id: tool.id, name: tool.name, - result, + result: + single?.type === "text" + ? { type: "text" as const, value: single.text } + : { type: "content" as const, value: content }, providerExecuted: tool.executed, providerMetadata, }) @@ -107,10 +100,7 @@ const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: Provid return ToolResultPart.make({ id: tool.id, name: tool.name, - result: - tool.executed === true && tool.state.result !== undefined - ? tool.state.result - : { error: tool.state.error, content: tool.state.content, structured: tool.state.structured }, + result: { error: tool.state.error, content: tool.state.content ?? [] }, resultType: "error", providerExecuted: tool.executed, providerMetadata, @@ -119,8 +109,8 @@ const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: Provid } const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref, providerMetadataKey: string) => { - const sameModel = - String(message.model.providerID) === String(model.providerID) && String(message.model.id) === String(model.id) + const sameProvider = String(message.model.providerID) === String(model.providerID) + const sameModel = sameProvider && String(message.model.id) === String(model.id) const reuseProviderMetadata = sameModel && message.error === undefined const content = message.content.flatMap((item): ContentPart[] => { if (item.type === "text") return [{ type: "text", text: item.text }] @@ -138,19 +128,21 @@ const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref, provid : [] const reuseToolProviderMetadata = reuseProviderMetadata || - (sameModel && - item.executed === true && - (item.state.status === "completed" || (item.state.status === "error" && item.state.result !== undefined))) + (sameModel && item.executed === true && (item.state.status === "completed" || item.state.status === "error")) const call = toolCall( item, reuseToolProviderMetadata ? providerMetadata(providerMetadataKey, item.providerState) : undefined, ) if (item.executed !== true) return [call] + // Hosted result payloads are provider-format state, not model state: + // replay must survive a model switch within the same provider. const result = toolResult( item, reuseToolProviderMetadata ? providerMetadata(providerMetadataKey, item.providerResultState ?? item.providerState) - : undefined, + : sameProvider && item.executed === true && item.providerResultState !== undefined + ? providerMetadata(providerMetadataKey, item.providerResultState) + : undefined, ) return result ? [call, result] : [call] }) diff --git a/packages/core/src/session/to-session-error.ts b/packages/core/src/session/to-session-error.ts index e3ad94fd10cd..fe21051d8ab4 100644 --- a/packages/core/src/session/to-session-error.ts +++ b/packages/core/src/session/to-session-error.ts @@ -39,8 +39,13 @@ export function toSessionError(cause: unknown): SessionError.Error { } if (cause instanceof PermissionV2.BlockedError) return { type: "permission.rejected", message: cause.message } if (cause instanceof QuestionV2.RejectedError) return { type: "aborted", message: cause.message } - if (cause instanceof ToolFailure || cause instanceof Tool.Failure) - return cause.error === undefined ? { type: "tool.execution", message: cause.message } : toSessionError(cause.error) + if (cause instanceof ToolFailure || cause instanceof Tool.Failure) { + if (cause.error === undefined) return { type: "tool.execution", message: cause.message } + // The canonical error is the sole model-visible representation, so a cause + // with no message must not erase the tool's curated failure message. + const unwrapped = toSessionError(cause.error) + return unwrapped.message === "" ? { ...unwrapped, type: "tool.execution", message: cause.message } : unwrapped + } if (cause instanceof StepFailedError) return cause.error if (cause instanceof AgentNotFoundError) return { type: "unknown", message: cause.message } if (cause instanceof UserInterruptedError) return { type: "aborted", message: cause.message } diff --git a/packages/core/src/tool-output-store.ts b/packages/core/src/tool-output-store.ts index f7eccefea038..d59a342159fd 100644 --- a/packages/core/src/tool-output-store.ts +++ b/packages/core/src/tool-output-store.ts @@ -8,7 +8,7 @@ import { Global } from "@opencode-ai/util/global" import { makeGlobalNode, makeLocationNode } from "@opencode-ai/util/effect/app-node" import { SessionSchema } from "./session/schema" import { Identifier } from "./util/identifier" -import type { ToolOutput } from "@opencode-ai/ai" +import type { ToolContent } from "@opencode-ai/ai" export const MAX_LINES = 2_000 export const MAX_BYTES = 50 * 1024 @@ -19,11 +19,11 @@ export const MANAGED_DIRECTORY = "tool-output" export interface BoundInput { readonly sessionID: SessionSchema.ID readonly callID: string - readonly output: ToolOutput + readonly content: ReadonlyArray } export interface BoundResult { - readonly output: ToolOutput + readonly content: ReadonlyArray readonly outputPaths: ReadonlyArray } @@ -137,21 +137,14 @@ const layer = Layer.effect( const bound = Effect.fn("ToolOutputStore.bound")(function* (input: BoundInput) { const outputLimits = yield* limits() - const media = input.output.content.filter((item) => item.type === "file") - const text = input.output.content.filter((item) => item.type === "text") - const contextual = - input.output.content.length === 0 - ? yield* Effect.try({ - try: () => JSON.stringify(input.output.structured, null, 2) ?? String(input.output.structured), - catch: (cause) => new StorageError({ operation: "encode", cause }), - }) - : text.map((item) => item.text).join("") + const media = input.content.filter((item) => item.type === "file") + const contextual = input.content.flatMap((item) => (item.type === "text" ? [item.text] : [])).join("") if ( lineCount(contextual) <= outputLimits.maxLines && Buffer.byteLength(contextual, "utf-8") <= outputLimits.maxBytes ) return { - output: input.output, + content: input.content, outputPaths: [], } @@ -159,16 +152,13 @@ const layer = Layer.effect( const marker = `... output truncated; full content saved to ${outputPath} ...` return { - output: { - structured: input.output.structured, - content: [ - { - type: "text" as const, - text: boundedPreview(contextual, marker, outputLimits.maxLines, outputLimits.maxBytes), - }, - ...media, - ], - }, + content: [ + { + type: "text" as const, + text: boundedPreview(contextual, marker, outputLimits.maxLines, outputLimits.maxBytes), + }, + ...media, + ], outputPaths: [outputPath], } }) diff --git a/packages/core/src/tool/AGENTS.md b/packages/core/src/tool/AGENTS.md index beb838a567e6..7c1e3f150699 100644 --- a/packages/core/src/tool/AGENTS.md +++ b/packages/core/src/tool/AGENTS.md @@ -1,26 +1,26 @@ # Core Tool Architecture -This folder owns Core's one local tool representation, process and Location registration, effective lookup, and settlement. +This folder owns Core's local tools, Location-scoped registrations, effective lookup, execution, and terminal outcomes. ## Representations -- `tool.ts` defines the structural canonical `Tool.make({ description, input, output, execute, toModelOutput })` declaration. Shipped built-ins and plugin tools use the same type. +- `tool.ts` defines the structural canonical `Tool.make({ description, input, output?, execute })` tool. Executors return model content and metadata alongside declared machine output. Shipped built-ins and plugin tools use the same type. - `tools.ts` exposes the registration-only `Tools.Service` view used by Location producers. -- `registry.ts` stores only canonical Location registrations, derives definitions, invokes tools, and applies generic output bounding. +- `registry.ts` stores only canonical Location registrations, derives LLM definitions, executes tools, and applies generic output bounding. Do not add a second executable entry type, registry-owned executor, authorization callback, output-path callback, or legacy normalization path. ## Construction -Tool schemas and projection use `input` and `output` terminology. A tool value carries its schemas, executor, projection, and optional catalog permission directly so separately loaded plugin package instances can exchange it structurally. +Tool schemas use `input` and `output` terminology. A tool carries schemas and executable behavior without public identity. A registration binds its name, namespace, CodeMode placement, and optional catalog permission action. Location-scoped built-in layers acquire `PermissionV2.Service` and every other required Location service while the layer is constructed. The executor captures those services. Permission sources are always constructed from the canonical invocation context: ```ts const source = { type: "tool" as const, - messageID: context.assistantMessageID, - callID: context.toolCallID, + messageID: context.messageID, + callID: context.callID, } ``` @@ -42,13 +42,13 @@ Registrations are scoped: ## Permissions -The registry has no `PermissionV2.Service` dependency and performs no execution authorization. An internal built-in-only operation attaches a permission action solely to preserve whole-tool definition filtering; it is not part of public `Tool.make`. Most tools default to their registered name; `edit`, `write`, and `patch` declare the shared `edit` action. +The registry has no `PermissionV2.Service` dependency and performs no execution authorization. Registration options may attach a permission action solely to preserve whole-tool definition filtering. Most registrations default to their effective name; `edit`, `write`, and `patch` use the shared `edit` action. -Definition filtering is catalog visibility, not execution authorization. A call still executes the captured leaf policy if it reaches settlement. +Tool filtering is catalog visibility, not execution authorization. A call still executes the captured tool's leaf policy if it reaches execution. ## Output -Built-ins return complete validated domain output. `ToolRegistry.Materialization.settle` is the only execution and generic model-output bounding boundary and owns managed retention paths. +Built-ins return complete tool responses. `ToolRegistry.ToolSet.execute` is the only local execution and generic model-output bounding boundary and owns managed retention paths. Producer capture limits are separate. For example, Bash keeps `AppProcess.maxOutputBytes` and accurately reports stdout/stderr capture loss, but it does not run model-output truncation or return a managed `outputPath`. diff --git a/packages/core/src/tool/edit.ts b/packages/core/src/tool/edit.ts index 0456195dd99c..00b49ca0d0ae 100644 --- a/packages/core/src/tool/edit.ts +++ b/packages/core/src/tool/edit.ts @@ -97,15 +97,11 @@ export const Plugin = { .transform((draft) => draft.add( name, - Tool.withPermission( - Tool.make({ + Tool.make({ description: "Replace exact text in one file. Relative paths resolve within the active Location. Absolute paths inside the Location are accepted. Explicit external absolute paths require external_directory approval before edit approval.", input: Input, output: Output, - toModelOutput: ({ input, output }) => [ - { type: "text", text: toModelOutput(output, input.oldString, input.newString) }, - ], execute: (input, context) => { const unableToEdit = (effect: Effect.Effect) => effect.pipe( @@ -207,12 +203,16 @@ export const Plugin = { ], replacements, } satisfies Output - }) + }).pipe( + Effect.map((output) => ({ + output, + content: toModelOutput(output, input.oldString, input.newString), + metadata: { files: output.files }, + })), + ) }, }), - "edit", - ), - { codemode: false }, + { codemode: false, permission: "edit" }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/execute.ts b/packages/core/src/tool/execute.ts index b9ffa751e3fd..30ca66712cf0 100644 --- a/packages/core/src/tool/execute.ts +++ b/packages/core/src/tool/execute.ts @@ -1,9 +1,10 @@ export * as ExecuteTool from "./execute" +export type { Registration } from "./tool" import { CodeMode, Tool, toolError } from "@opencode-ai/codemode" -import { ToolOutput } from "@opencode-ai/ai" +import type { ToolContent } from "@opencode-ai/ai" import { Effect, Ref, Schema } from "effect" -import { definition, make, settle, type AnyTool } from "./tool" +import { execute, make, toLLMDefinition, type Content, type Metadata, type Registration } from "./tool" const ExecuteFile = Schema.Struct({ data: Schema.String, @@ -14,16 +15,11 @@ const ExecuteFile = Schema.Struct({ const ExecuteCall = Schema.Struct({ tool: Schema.String, status: Schema.Literals(["running", "completed", "error"]), - input: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown)), + input: Schema.optionalKey(Schema.Record(Schema.String, Schema.Json)), }) type ExecuteCall = typeof ExecuteCall.Type -const ExecuteMetadata = Schema.Struct({ - toolCalls: Schema.Array(ExecuteCall), - error: Schema.optionalKey(Schema.Literal(true)), -}) - const ExecuteOutput = Schema.Struct({ output: Schema.String, toolCalls: Schema.Array(ExecuteCall), @@ -36,12 +32,6 @@ type CollectedFiles = { readonly files: Array } -export interface Registration { - readonly tool: AnyTool - readonly name: string - readonly namespace?: string -} - // Invariant model-facing guidance; the changing tool catalog is delivered through Instructions. const description = [ "Run JavaScript in a confined Code Mode runtime through { code }.", @@ -55,20 +45,6 @@ export const create = (registrations: ReadonlyMap) => { description, input: CodeMode.Input, output: ExecuteOutput, - structured: ExecuteMetadata, - toStructuredOutput: ({ output }) => ({ - toolCalls: output.toolCalls, - ...(output.error ? { error: true as const } : {}), - }), - toModelOutput: ({ output }) => [ - { type: "text" as const, text: output.output }, - ...output.files.map((file) => ({ - type: "file" as const, - data: file.data, - mime: file.mime, - ...(file.name === undefined ? {} : { name: file.name }), - })), - ], execute: ({ code }, context) => Effect.gen(function* () { const callIndex = yield* Ref.make(0) @@ -85,21 +61,17 @@ export const create = (registrations: ReadonlyMap) => { (name, registration, input) => Effect.gen(function* () { const index = yield* Ref.getAndUpdate(callIndex, (index) => index + 1) - const output = yield* settle( - registration.tool, - { type: "tool-call", id: context.callID, name, input }, - { - sessionID: context.sessionID, - agent: context.agent, - messageID: context.messageID, - callID: context.callID, - progress: context.progress, - }, - ).pipe(Effect.mapError((failure) => toolError(failure.message, failure))) - const outputFileParts = outputFiles(output) + const executed = yield* execute(registration.tool, input, { + sessionID: context.sessionID, + agent: context.agent, + messageID: context.messageID, + callID: context.callID, + progress: context.progress, + }).pipe(Effect.mapError((failure) => toolError(failure.message, failure))) + const outputFileParts = outputFiles(executed.content) if (outputFileParts.length > 0) yield* Ref.update(files, (items) => [...items, { index, files: outputFileParts }]) - return output.structured + return executed.output }), { onToolCallStart: ({ index, name, input }) => @@ -126,7 +98,30 @@ export const create = (registrations: ReadonlyMap) => { .toSorted((left, right) => left.index - right.index) .flatMap((item) => item.files) const output = formatResult(result) - return { output, toolCalls, files: collected, ...(result.ok ? {} : { error: true as const }) } + const value: typeof ExecuteOutput.Type = { + output, + toolCalls, + files: collected, + ...(result.ok ? {} : { error: true }), + } + const content: [Content, ...Content[]] = [{ type: "text", text: value.output }] + content.push( + ...value.files.map((file) => ({ + type: "file" as const, + data: file.data, + mime: file.mime, + ...(file.name === undefined ? {} : { name: file.name }), + })), + ) + const metadata: Metadata = { + toolCalls: value.toolCalls, + ...(value.error ? { error: true } : {}), + } + return { + output: value, + content, + metadata, + } }), }) } @@ -137,28 +132,30 @@ export const instructions = (registrations: ReadonlyMap) = function runtime( registrations: ReadonlyMap, - invoke: (name: string, registration: Registration, input: unknown) => Effect.Effect, + executeTool: (name: string, registration: Registration, input: unknown) => Effect.Effect, hooks?: CodeMode.ToolCallHooks, ) { - const tools: Record> = {} + const tools: Record> = {} for (const [name, registration] of registrations) { - const child = definition(name, registration.tool) - const path = registration.namespace === undefined ? registration.name : `${registration.namespace}.${registration.name}` + const child = toLLMDefinition(name, registration.tool) + const path = + registration.namespace === undefined ? registration.name : `${registration.namespace}.${registration.name}` tools[path] = Tool.make({ description: child.description, input: child.inputSchema, output: child.outputSchema, - run: (input) => invoke(name, registration, input), + execute: (input) => executeTool(name, registration, input), }) } return CodeMode.make({ tools, ...hooks }) } -function displayInput(input: unknown): Record | undefined { +// Tool inputs arrive as parsed JSON, so the JSON value cast is a boundary fact. +function displayInput(input: unknown): Record | undefined { if (input === null || input === undefined) return - if (typeof input !== "object" || Array.isArray(input)) return { input } + if (typeof input !== "object" || Array.isArray(input)) return { input: input as typeof Schema.Json.Type } if (Object.keys(input).length === 0) return - return input as Record + return input as Record } function formatResult(result: CodeMode.Result) { @@ -180,8 +177,8 @@ function formatValue(value: CodeMode.DataValue) { return JSON.stringify(value, null, 2) ?? String(value) } -function outputFiles(output: ToolOutput): Array { - return output.content.flatMap((part) => { +function outputFiles(content: ReadonlyArray): Array { + return content.flatMap((part) => { if (part.type !== "file") return [] const prefix = `data:${part.mime};base64,` if (!part.uri.startsWith(prefix)) return [] diff --git a/packages/core/src/tool/glob.ts b/packages/core/src/tool/glob.ts index 27a7d26a0103..50ef68539424 100644 --- a/packages/core/src/tool/glob.ts +++ b/packages/core/src/tool/glob.ts @@ -8,7 +8,7 @@ import { FileSystem } from "../filesystem" import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "../location" import { Ripgrep } from "../ripgrep" -import { NonNegativeInt, RelativePath } from "../schema" +import { RelativePath } from "../schema" import { PermissionV2 } from "../permission" import { Tool } from "./tool" @@ -25,9 +25,6 @@ export const Input = Schema.Struct({ }) export const Output = Schema.Array(FileSystem.Entry) -const StructuredOutput = Schema.Struct({ - count: NonNegativeInt, -}) type ModelOutput = typeof Output.Encoded /** Format raw search results into the concise line-oriented output models expect. */ @@ -54,16 +51,6 @@ export const Plugin = { "Find files by glob pattern within the active Location. Returns concise relative file resources. Use a relative path to narrow the search and limit to bound the result count.", input: Input, output: Output, - structured: StructuredOutput, - toStructuredOutput: ({ output }) => ({ count: output.length }), - toModelOutput: ({ output }) => [ - { - type: "text", - text: toModelOutput( - output.map((entry) => ({ ...entry, path: path.resolve(location.directory, entry.path) })), - ), - }, - ], execute: (input, context) => Effect.gen(function* () { yield* permission.assert({ @@ -104,6 +91,13 @@ export const Plugin = { ), ) }).pipe( + Effect.map((output) => ({ + output, + content: toModelOutput( + output.map((entry) => ({ ...entry, path: path.resolve(location.directory, entry.path) })), + ), + metadata: { count: output.length }, + })), Effect.mapError((error) => error instanceof ToolFailure ? error diff --git a/packages/core/src/tool/grep.ts b/packages/core/src/tool/grep.ts index 56cb004ddf8d..fa7326302d81 100644 --- a/packages/core/src/tool/grep.ts +++ b/packages/core/src/tool/grep.ts @@ -9,7 +9,7 @@ import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "../location" import { PermissionV2 } from "../permission" import { Ripgrep } from "../ripgrep" -import { NonNegativeInt, RelativePath } from "../schema" +import { RelativePath } from "../schema" import { Tool } from "./tool" export const name = "grep" @@ -30,9 +30,6 @@ export const Input = Schema.Struct({ }) export const Output = Schema.Array(FileSystem.Match) -const StructuredOutput = Schema.Struct({ - matches: NonNegativeInt, -}) type ModelOutput = typeof Output.Encoded /** Format raw search matches into the familiar concise model output. */ @@ -68,19 +65,6 @@ export const Plugin = { "Search file contents by regular expression within the active Location or an absolute managed tool-output file. Use a path to narrow the search, include to filter files by glob, and limit to bound the match count. Returns concise file resources, line numbers, and bounded line previews.", input: Input, output: Output, - structured: StructuredOutput, - toStructuredOutput: ({ output }) => ({ matches: output.length }), - toModelOutput: ({ output }) => [ - { - type: "text", - text: toModelOutput( - output.map((match) => ({ - ...match, - entry: { ...match.entry, path: path.resolve(location.directory, match.entry.path) }, - })), - ), - }, - ], execute: (input, context) => Effect.gen(function* () { yield* permission.assert({ @@ -135,6 +119,16 @@ export const Plugin = { ), ) }).pipe( + Effect.map((output) => ({ + output, + content: toModelOutput( + output.map((match) => ({ + ...match, + entry: { ...match.entry, path: path.resolve(location.directory, match.entry.path) }, + })), + ), + metadata: { matches: output.length }, + })), Effect.mapError((error) => error instanceof ToolFailure ? error diff --git a/packages/core/src/tool/hooks.ts b/packages/core/src/tool/hooks.ts index 5a0c93c31498..2e6cf32942aa 100644 --- a/packages/core/src/tool/hooks.ts +++ b/packages/core/src/tool/hooks.ts @@ -1,33 +1,14 @@ export * as ToolHooks from "./hooks" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { Agent } from "@opencode-ai/schema/agent" -import { Session } from "@opencode-ai/schema/session" -import { SessionMessage } from "../session/message" import { State } from "../state" import { Context, Effect, Layer, Scope } from "effect" -import type { ToolOutput, ToolResultValue } from "@opencode-ai/ai" +import type { Tool } from "./tool" -export interface BeforeEvent { - readonly tool: string - readonly sessionID: Session.ID - readonly agent: Agent.ID - readonly messageID: SessionMessage.ID - readonly callID: string - input: unknown -} +export type BeforeEvent = Tool.ToolExecuteBeforeEvent -export interface AfterEvent { - readonly tool: string - readonly sessionID: Session.ID - readonly agent: Agent.ID - readonly messageID: SessionMessage.ID - readonly callID: string - readonly input: unknown - result: ToolResultValue - output?: ToolOutput - outputPaths?: ReadonlyArray -} +/** The canonical execution outcome. Hooks never observe the raw domain output. */ +export type AfterEvent = Tool.ToolExecuteAfterEvent export interface Interface { readonly hook: { diff --git a/packages/core/src/tool/mcp.ts b/packages/core/src/tool/mcp.ts index de9910a81971..24b10f79dadc 100644 --- a/packages/core/src/tool/mcp.ts +++ b/packages/core/src/tool/mcp.ts @@ -32,86 +32,90 @@ export const layer = Layer.effectDiscard( // registry never has a gap where MCP tools disappear mid-swap. const reconcile = lock.withPermit( Effect.gen(function* () { - const groups = new Map; codemode: boolean }>() + const groups = new Map< + string, + { + tools: Record + codemode: boolean + } + >() for (const tool of yield* mcp.tools()) { const group = groups.get(tool.server) ?? { tools: {}, codemode: tool.codemode !== false } const schema = (tool.inputSchema ?? {}) as JsonSchema.JsonSchema - group.tools[tool.name] = Tool.withPermission( - Tool.make({ - description: tool.description ?? "", - jsonSchema: { - ...schema, - type: "object", - properties: schema.properties ?? {}, - additionalProperties: false, - }, - outputSchema: tool.outputSchema as JsonSchema.JsonSchema | undefined, - execute: (input, context) => - Effect.gen(function* () { - yield* permission.assert({ - action: name(tool.server, tool.name), - resources: ["*"], - save: ["*"], - metadata: {}, - sessionID: context.sessionID, - agent: context.agent, - source: { - type: "tool", - messageID: context.messageID, - callID: context.callID, - }, + group.tools[tool.name] = Tool.make({ + description: tool.description ?? "", + input: { + ...schema, + type: "object", + properties: schema.properties ?? {}, + additionalProperties: false, + }, + output: (tool.outputSchema ?? {}) as JsonSchema.JsonSchema, + execute: (input, context) => + Effect.gen(function* () { + yield* permission.assert({ + action: name(tool.server, tool.name), + resources: ["*"], + save: ["*"], + metadata: {}, + sessionID: context.sessionID, + agent: context.agent, + source: { + type: "tool", + messageID: context.messageID, + callID: context.callID, + }, + }) + const result = yield* mcp + .callTool({ + server: tool.server, + name: tool.name, + args: (input ?? {}) as Record, }) - const result = yield* mcp - .callTool({ - server: tool.server, - name: tool.name, - args: (input ?? {}) as Record, - }) - .pipe( - Effect.catchTags({ - "MCP.NotFoundError": (error) => - new ToolFailure({ message: `MCP server "${error.server}" is not available` }), - "MCP.ToolCallError": (error) => new ToolFailure({ message: error.message }), - }), - ) - if (result.isError) - return yield* new ToolFailure({ - message: - result.content - .flatMap((part) => (part.type === "text" ? [part.text] : [])) - .join("\n") - .trim() || "MCP tool returned an error", - }) - const content = result.content.map((part) => - part.type === "text" - ? { type: "text" as const, text: part.text } - : { type: "file" as const, data: part.data, mime: part.mimeType }, + .pipe( + Effect.catchTags({ + "MCP.NotFoundError": (error) => + new ToolFailure({ message: `MCP server "${error.server}" is not available` }), + "MCP.ToolCallError": (error) => new ToolFailure({ message: error.message }), + }), ) - const text = content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n") - return { - structured: result.structured ?? (text === "" ? null : text), - content, - } - }).pipe( - Effect.mapError((error) => - error instanceof ToolFailure - ? error - : new ToolFailure({ message: `Unable to execute ${name(tool.server, tool.name)}` }), - ), + if (result.isError) + return yield* new ToolFailure({ + message: + result.content + .flatMap((part) => (part.type === "text" ? [part.text] : [])) + .join("\n") + .trim() || "MCP tool returned an error", + }) + const content = result.content.map((part) => + part.type === "text" + ? { type: "text" as const, text: part.text } + : { type: "file" as const, data: part.data, mime: part.mimeType }, + ) + const text = content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n") + return { + output: result.structured ?? (text === "" ? null : text), + ...(content.length === 0 ? {} : { content: content as [Tool.Content, ...Tool.Content[]] }), + } + }).pipe( + Effect.mapError((error) => + error instanceof ToolFailure + ? error + : new ToolFailure({ message: `Unable to execute ${name(tool.server, tool.name)}` }), ), - }), - name(tool.server, tool.name), - ) + ), + }) groups.set(tool.server, group) } const next = yield* Scope.fork(scope) - yield* Effect.forEach( - groups, - ([server, group]) => tools.register(group.tools, { namespace: namespace(server), codemode: group.codemode }), - { - discard: true, - }, - ).pipe(Scope.provide(next), Effect.orDie) + yield* tools + .registerBatch( + Array.from(groups, ([server, group]) => ({ + tools: group.tools, + options: { namespace: namespace(server), codemode: group.codemode }, + })), + ) + .pipe(Scope.provide(next), Effect.orDie) if (current) yield* Scope.close(current, Exit.void) current = next }), diff --git a/packages/core/src/tool/patch.ts b/packages/core/src/tool/patch.ts index 7a70b3c5c33e..5e90c3559667 100644 --- a/packages/core/src/tool/patch.ts +++ b/packages/core/src/tool/patch.ts @@ -75,12 +75,10 @@ export const Plugin = { .transform((draft) => draft.add( name, - Tool.withPermission( - Tool.make({ + Tool.make({ description: DESCRIPTION, input: Input, output: Output, - toModelOutput: ({ output }) => [{ type: "text", text: toModelOutput(output) }], execute: (input, context) => { const applied: Array = [] const fail = (path: string, error?: unknown) => { @@ -278,12 +276,17 @@ export const Plugin = { { discard: true }, ) return { applied, files: patchFiles } - }).pipe(Effect.mapError((error) => (error instanceof ToolFailure ? error : fail("patch", error)))) + }).pipe( + Effect.map((output) => ({ + output, + content: toModelOutput(output), + metadata: { files: output.files }, + })), + Effect.mapError((error) => (error instanceof ToolFailure ? error : fail("patch", error))), + ) }, }), - "edit", - ), - { codemode: false }, + { codemode: false, permission: "edit" }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/question.ts b/packages/core/src/tool/question.ts index aa343c965297..cdb80a0000f0 100644 --- a/packages/core/src/tool/question.ts +++ b/packages/core/src/tool/question.ts @@ -63,9 +63,6 @@ export const Plugin = { description, input: Input, output: Output, - toModelOutput: ({ input, output }) => [ - { type: "text", text: toModelOutput(input.questions, output.answers) }, - ], execute: (input, context) => permission .assert({ @@ -95,13 +92,18 @@ export const Plugin = { ), Effect.flatMap((state) => { if (state.status === "cancelled") return Effect.die(new CancelledError()) - return Effect.succeed({ + const output = { answers: input.questions.map((_, index): QuestionV2.Answer => { const value = state.answer[`q${index}`] if (value === undefined) return [] if (typeof value === "object") return Array.from(value) return [String(value)] }), + } + return Effect.succeed({ + output, + content: toModelOutput(input.questions, output.answers), + metadata: { answers: output.answers }, }) }), ), diff --git a/packages/core/src/tool/read.ts b/packages/core/src/tool/read.ts index 4021516cc4a3..5c0b9254beb7 100644 --- a/packages/core/src/tool/read.ts +++ b/packages/core/src/tool/read.ts @@ -48,20 +48,6 @@ export const Plugin = { "Read a text file or supported image, page through a large UTF-8 text file by line offset, or list a directory page. Relative paths resolve from the current location; absolute paths inside it are accepted, while external absolute paths require external_directory approval.", input: Input, output: Output, - structured: Schema.toEncoded(Output), - // Image base64 reaches the model through content items (normalized generically - // at tool settlement); persisting a second copy in structured would store the - // original unresized bytes in the message row. - toStructuredOutput: ({ output }) => - "encoding" in output && output.encoding === "base64" ? { ...output, content: "" } : output, - toModelOutput: ({ input, output }) => { - if (!("encoding" in output) || output.encoding !== "base64" || !SUPPORTED_IMAGE_MIMES.has(output.mime)) - return [] - return [ - { type: "text", text: "Image read successfully" }, - { type: "file", data: output.content, mime: output.mime, name: input.path }, - ] - }, execute: (input, context) => { return Effect.gen(function* () { const source = { @@ -125,6 +111,20 @@ export const Plugin = { return yield* Effect.fail(new ReadToolFileSystem.BinaryFileError({ resource })) return content }).pipe( + Effect.map((output) => { + // Image base64 reaches the model through content items; avoid a second + // unresized copy in model text. + const content = + "encoding" in output && output.encoding === "base64" + ? SUPPORTED_IMAGE_MIMES.has(output.mime) + ? ([ + { type: "text", text: "Image read successfully" }, + { type: "file", data: output.content, mime: output.mime, name: input.path }, + ] as const) + : JSON.stringify({ ...output, content: "" }, null, 2) + : JSON.stringify(output, null, 2) + return { output, content } + }), Effect.mapError((error) => { const message = error instanceof ReadToolFileSystem.BinaryFileError || diff --git a/packages/core/src/tool/registry.ts b/packages/core/src/tool/registry.ts index 1dd10038df7a..9316f6f8f6ee 100644 --- a/packages/core/src/tool/registry.ts +++ b/packages/core/src/tool/registry.ts @@ -1,7 +1,7 @@ export * as ToolRegistry from "./registry" -import { ToolOutput, type ToolCall, type ToolDefinition, type ToolResultValue } from "@opencode-ai/ai" -import { Context, Effect, Layer, Scope, Semaphore } from "effect" +import { type ToolCall, type ToolContent, type ToolDefinition } from "@opencode-ai/ai" +import { Context, Effect, Layer, Schema, Scope, Semaphore } from "effect" import type { AgentV2 } from "../agent" import { Image } from "../image" import { PermissionV2 } from "../permission" @@ -10,19 +10,10 @@ import { SessionSchema } from "../session/schema" import { ToolOutputStore } from "../tool-output-store" import { Wildcard } from "../util/wildcard" import { CodeMode } from "../codemode" -import { - definition, - permission, - registrationEntries, - RegistrationError, - settle, - validateNamespace, - type AnyTool, -} from "./tool" +import { Tool, nonEmpty, registrationEntries, toLLMDefinition, validateName, validateNamespace } from "./tool" import { Tools } from "./tools" import { ToolHooks } from "./hooks" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { SessionError } from "@opencode-ai/schema/session-error" import { toSessionError } from "../session/to-session-error" export type ExecuteInput = { @@ -33,38 +24,42 @@ export type ExecuteInput = { readonly progress?: (update: Progress) => Effect.Effect } -export interface Progress { - readonly structured: Readonly> - readonly content: ToolOutput["content"] -} +/** Live replacement metadata for a running tool. */ +export type Progress = Tool.Metadata export interface Interface { - readonly materialize: (permissions?: PermissionV2.Ruleset) => Effect.Effect + readonly snapshot: (permissions?: PermissionV2.Ruleset) => Effect.Effect /** Internal registration capability exposed publicly only through Tools.Service. */ readonly register: ( - tools: Readonly>, + tools: Readonly>, options?: Tools.RegisterOptions, - ) => Effect.Effect + ) => Effect.Effect /** Internal atomic registration capability used by plugin transforms. */ readonly registerBatch: ( registrations: ReadonlyArray<{ - readonly tools: Readonly> + readonly tools: Readonly> readonly options?: Tools.RegisterOptions }>, - ) => Effect.Effect + ) => Effect.Effect } -export interface Materialization { +/** + * One request-scoped snapshot pairing advertised definitions with captured + * tools. A model request executes exactly the tool values it advertised + * even if registration changes while the request is in flight. + */ +export interface ToolSet { readonly definitions: ReadonlyArray - readonly settle: (input: ExecuteInput) => Effect.Effect + readonly execute: (input: ExecuteInput) => Effect.Effect } -export interface Settlement { - readonly result: ToolResultValue - readonly output?: ToolOutput - readonly outputPaths?: ReadonlyArray - readonly error?: SessionError.Error -} +/** + * The canonical outcome of one local tool execution. `output` is the validated + * machine value for Code Mode and remains ephemeral; durable publication drops it. + */ +export type ToolOutcome = + | (Extract & { readonly output?: unknown }) + | Extract export class Service extends Context.Service()("@opencode/v2/ToolRegistry") {} @@ -76,26 +71,24 @@ const registryLayer = Layer.effect( const image = yield* Image.Service const codeMode = yield* CodeMode.Service - type NormalizedItem = ToolOutput["content"][number] | "decode" | "size" - const normalizeImages = Effect.fn("ToolRegistry.normalizeImages")(function* (content: ToolOutput["content"]) { + type NormalizedItem = ToolContent | "decode" | "size" + const normalizeImages = Effect.fn("ToolRegistry.normalizeImages")(function* (content: ReadonlyArray) { const normalized = yield* Effect.forEach(content, (item): Effect.Effect => { if (item.type !== "file" || !item.mime.startsWith("image/")) return Effect.succeed(item) // RFC 2397 permits parameters between the mime and ";base64". const base64 = /^data:[^,]*;base64,(.*)$/s.exec(item.uri)?.[1] if (base64 === undefined) return Effect.succeed(item) const resource = item.name ?? `${item.mime} tool output` - return image - .normalize(resource, { uri: resource, content: base64, encoding: "base64", mime: item.mime }) - .pipe( - Effect.map((result) => ({ - ...item, - uri: `data:${result.mime};base64,${result.content}`, - mime: result.mime, - })), - Effect.catchTag("Image.ResizerUnavailableError", () => Effect.succeed(item)), - Effect.catchTag("Image.DecodeError", () => Effect.succeed("decode" as const)), - Effect.catchTag("Image.SizeError", () => Effect.succeed("size" as const)), - ) + return image.normalize(resource, { uri: resource, content: base64, encoding: "base64", mime: item.mime }).pipe( + Effect.map((result) => ({ + ...item, + uri: `data:${result.mime};base64,${result.content}`, + mime: result.mime, + })), + Effect.catchTag("Image.ResizerUnavailableError", () => Effect.succeed(item)), + Effect.catchTag("Image.DecodeError", () => Effect.succeed("decode" as const)), + Effect.catchTag("Image.SizeError", () => Effect.succeed("size" as const)), + ) }) const note = (reason: "decode" | "size", text: string) => { const count = normalized.filter((item) => item === reason).length @@ -108,16 +101,24 @@ const registryLayer = Layer.effect( ...note("size", "could not be resized below the image size limit."), ] }) - type Registration = { - readonly tool: AnyTool - readonly name: string - readonly namespace?: string - } + + // Invalid or oversized metadata is dropped with a warning; it never fails a + // successful side-effecting tool. + const validMetadata = Effect.fnUntraced(function* (tool: string, metadata: Tool.Metadata | undefined) { + if (metadata === undefined) return undefined + const limits = yield* resources.limits() + const valid = Tool.jsonMetadata(metadata, limits.maxBytes) + if (valid === undefined) + yield* Effect.logWarning("dropping invalid or oversized tool metadata").pipe(Effect.annotateLogs({ tool })) + return valid + }) + + type Registration = Tool.Registration const local = new Map>() const registrationLock = Semaphore.makeUnsafe(1) - const settleTool = Effect.fn("ToolRegistry.settleTool")(function* (input: ExecuteInput, tool: AnyTool) { - // Hooks fire only for hosted/local tools; provider-executed calls never reach settleTool. + const executeTool = Effect.fn("ToolRegistry.executeTool")(function* (input: ExecuteInput, tool: Tool.Any) { + // Hooks fire only for hosted/local tools; provider-executed calls never reach executeTool. const beforeEvent: ToolHooks.BeforeEvent = { tool: input.call.name, sessionID: input.sessionID, @@ -127,76 +128,100 @@ const registryLayer = Layer.effect( input: input.call.input, } yield* toolHooks.runBefore(beforeEvent) - const pending = yield* settle( - tool, - { ...input.call, input: beforeEvent.input }, - { - sessionID: input.sessionID, - agent: input.agent, - messageID: input.messageID, - callID: input.call.id, - progress: (update) => { - const progress = input.progress - if (!progress) return Effect.void - return normalizeImages( - (update.content ?? []).map((part) => - part.type === "text" - ? { type: "text" as const, text: part.text } - : { - type: "file" as const, - uri: `data:${part.mime};base64,${part.data}`, - mime: part.mime, - name: part.name, - }, - ), - ).pipe(Effect.flatMap((content) => progress({ structured: update.structured, content }))) - }, + const execution = yield* Tool.execute(tool, beforeEvent.input, { + sessionID: input.sessionID, + agent: input.agent, + messageID: input.messageID, + callID: input.call.id, + progress: (metadata) => { + const progress = input.progress + if (!progress) return Effect.void + return validMetadata(input.call.name, metadata).pipe( + Effect.flatMap((valid) => (valid === undefined ? Effect.void : progress(valid))), + ) }, - ).pipe( - Effect.map((output) => ({ output })), - Effect.catchTag("LLM.ToolFailure", (failure) => - Effect.succeed({ - result: { type: "error" as const, value: failure.message }, - error: toSessionError(failure), - }), - ), + }).pipe( + Effect.map((value) => ({ value })), + Effect.catchTag("LLM.ToolFailure", (failure) => Effect.succeed({ failure: toSessionError(failure) })), ) - let settlement: Settlement - if ("result" in pending) { - settlement = pending - } else { + + const outcome: ToolOutcome = yield* Effect.gen(function* () { + if ("failure" in execution) return { status: "error" as const, error: execution.failure } const bounded = yield* resources.bound({ sessionID: input.sessionID, callID: input.call.id, - output: { structured: pending.output.structured, content: yield* normalizeImages(pending.output.content) }, + content: yield* normalizeImages(execution.value.content), }) - const result = ToolOutput.toResultValue(bounded.output) - settlement = - result.type === "error" - ? bounded.outputPaths.length > 0 - ? { result, outputPaths: bounded.outputPaths } - : { result } - : bounded.outputPaths.length > 0 - ? { result, output: bounded.output, outputPaths: bounded.outputPaths } - : { result, output: bounded.output } - } - const afterEvent: ToolHooks.AfterEvent = { + const metadata = yield* validMetadata(input.call.name, execution.value.metadata) + return { + status: "completed" as const, + ...(execution.value.output === undefined ? {} : { output: execution.value.output }), + content: nonEmpty(bounded.content) ?? execution.value.content, + ...(metadata === undefined ? {} : { metadata }), + ...(bounded.outputPaths.length > 0 ? { outputPaths: bounded.outputPaths } : {}), + } + }) + + const base = { tool: input.call.name, sessionID: input.sessionID, agent: input.agent, messageID: input.messageID, callID: input.call.id, input: beforeEvent.input, - result: settlement.result, - output: settlement.output, - outputPaths: settlement.outputPaths, } + const afterEvent: ToolHooks.AfterEvent = + outcome.status === "completed" + ? { + ...base, + status: "completed", + content: outcome.content, + ...(outcome.metadata === undefined ? {} : { metadata: outcome.metadata }), + ...(outcome.outputPaths === undefined ? {} : { outputPaths: outcome.outputPaths }), + } + : { + ...base, + status: "error", + error: outcome.error, + ...(outcome.content === undefined ? {} : { content: outcome.content }), + ...(outcome.metadata === undefined ? {} : { metadata: outcome.metadata }), + ...(outcome.outputPaths === undefined ? {} : { outputPaths: outcome.outputPaths }), + } yield* toolHooks.runAfter(afterEvent) + const afterMetadata = yield* validMetadata(input.call.name, afterEvent.metadata) + const afterContent = yield* Effect.gen(function* () { + if ( + afterEvent.content === undefined || + (outcome.status === "completed" && afterEvent.content === outcome.content) + ) + return { content: afterEvent.content, outputPaths: afterEvent.outputPaths } + const bounded = yield* resources.bound({ + sessionID: input.sessionID, + callID: input.call.id, + content: yield* normalizeImages(afterEvent.content), + }) + return { + content: nonEmpty(bounded.content), + outputPaths: + bounded.outputPaths.length === 0 + ? afterEvent.outputPaths + : Array.from(new Set([...(afterEvent.outputPaths ?? []), ...bounded.outputPaths])), + } + }) + if (afterEvent.status === "completed") + return { + status: "completed" as const, + ...(outcome.status === "completed" && outcome.output !== undefined ? { output: outcome.output } : {}), + content: afterContent.content ?? afterEvent.content, + ...(afterMetadata === undefined ? {} : { metadata: afterMetadata }), + ...(afterContent.outputPaths === undefined ? {} : { outputPaths: afterContent.outputPaths }), + } return { - result: afterEvent.result, - ...(afterEvent.output !== undefined ? { output: afterEvent.output } : {}), - ...(afterEvent.outputPaths !== undefined ? { outputPaths: afterEvent.outputPaths } : {}), - ...(settlement.error !== undefined ? { error: settlement.error } : {}), + status: "error" as const, + error: afterEvent.error, + ...(afterContent.content === undefined ? {} : { content: afterContent.content }), + ...(afterMetadata === undefined ? {} : { metadata: afterMetadata }), + ...(afterContent.outputPaths === undefined ? {} : { outputPaths: afterContent.outputPaths }), } }) @@ -205,12 +230,26 @@ const registryLayer = Layer.effect( const planned = yield* Effect.forEach(registrations, ({ tools, options }) => Effect.gen(function* () { if (options?.namespace !== undefined) yield* validateNamespace(options.namespace) - const entries = registrationEntries(tools, options?.namespace) + const entries = registrationEntries(tools, options) + yield* Effect.forEach(entries, (entry) => validateName(entry.name), { discard: true }) + const collision = entries.find( + (entry, index) => entries.findIndex((candidate) => candidate.key === entry.key) !== index, + ) + if (collision) + return yield* Effect.fail( + new Tool.RegistrationError({ + name: collision.key, + message: `Duplicate normalized tool name: ${collision.key}`, + }), + ) const codemode = options?.codemode ?? true const reserved = codemode ? undefined : entries.find((entry) => entry.key === "execute") if (reserved) return yield* Effect.fail( - new RegistrationError({ name: reserved.key, message: 'Tool name "execute" is reserved for CodeMode' }), + new Tool.RegistrationError({ + name: reserved.key, + message: 'Tool name "execute" is reserved for CodeMode', + }), ) return { tools, options, entries, codemode } }), @@ -218,7 +257,7 @@ const registryLayer = Layer.effect( // CodeMode registrations live in the CodeMode service; the registry keeps only direct tools. yield* Effect.forEach( planned.filter((plan) => plan.codemode && plan.entries.length > 0), - (plan) => codeMode.register(plan.tools, plan.options), + (plan) => codeMode.register(plan.entries), { discard: true }, ) const direct = planned.filter((plan) => !plan.codemode) @@ -237,6 +276,7 @@ const registryLayer = Layer.effect( tool: entry.tool, name: entry.name, namespace: entry.namespace, + permission: entry.permission, }, }, ]) @@ -269,7 +309,7 @@ const registryLayer = Layer.effect( ]), ), registerBatch, - materialize: Effect.fn("ToolRegistry.materialize")((permissions) => + snapshot: Effect.fn("ToolRegistry.snapshot")((permissions) => registrationLock.withPermit( Effect.gen(function* () { const direct = new Map() @@ -277,21 +317,21 @@ const registryLayer = Layer.effect( for (const [name, entries] of local) { const registration = entries.at(-1)?.registration if (!registration) continue - if (whollyDisabled(permission(registration.tool, name), rules)) continue + if (whollyDisabled(registration.permission, rules)) continue direct.set(name, registration) } - const execute = (yield* codeMode.materialize(permissions)).tool + const codemodeTool = (yield* codeMode.materialize(permissions)).tool return { definitions: [ - ...Array.from(direct, ([name, registration]) => definition(name, registration.tool)), - ...(execute ? [definition("execute", execute)] : []), + ...Array.from(direct, ([name, registration]) => toLLMDefinition(name, registration.tool)), + ...(codemodeTool ? [toLLMDefinition("execute", codemodeTool)] : []), ], - settle: (input: ExecuteInput) => { - if (input.call.name === "execute" && execute) return settleTool(input, execute) + execute: (input: ExecuteInput) => { + if (input.call.name === "execute" && codemodeTool) return executeTool(input, codemodeTool) const registration = direct.get(input.call.name) - if (registration) return settleTool(input, registration.tool) - return Effect.succeed({ - result: { type: "error", value: `Unknown tool: ${input.call.name}` }, + if (registration) return executeTool(input, registration.tool) + return Effect.succeed({ + status: "error", error: { type: "tool.unknown", message: `Unknown tool: ${input.call.name}` }, }) }, diff --git a/packages/core/src/tool/shell.ts b/packages/core/src/tool/shell.ts index 63980f911da8..534855a78d63 100644 --- a/packages/core/src/tool/shell.ts +++ b/packages/core/src/tool/shell.ts @@ -3,7 +3,7 @@ export * as ShellTool from "./shell" import path from "path" import { ToolFailure } from "@opencode-ai/ai" import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" -import { Effect, Fiber, Schedule, Schema, Scope } from "effect" +import { Deferred, Effect, Schema, Scope } from "effect" import { FSUtil } from "@opencode-ai/util/fs-util" import { LocationMutation } from "../location-mutation" import { PermissionV2 } from "../permission" @@ -147,19 +147,6 @@ export const Plugin = { description: `Execute one shell command string with the host user's filesystem, process, and network authority. The active Location is the default working directory. Relative workdir values resolve from that Location. External workdir values require external_directory approval; best-effort command-argument path warnings are advisory only. An optional timeout may be provided in milliseconds (zero: unlimited; foreground default: ${DEFAULT_TIMEOUT_MS}; maximum: ${MAX_TIMEOUT_MS}). Background commands default to unlimited. Uses the configured shell when set; otherwise uses /bin/sh on POSIX and COMSPEC or cmd.exe on Windows. Background mode (background=true) launches the command asynchronously and returns immediately; you are notified when it finishes.`, input: Input, output: Output, - structured: StructuredOutput, - toStructuredOutput: ({ output }) => ({ - truncated: output.truncated, - ...(output.exit === undefined ? {} : { exit: output.exit }), - ...(output.shellID === undefined ? {} : { shellID: output.shellID }), - ...(output.timeout === undefined ? {} : { timeout: output.timeout }), - }), - toModelOutput: ({ output }) => { - const parts: Content[] = [{ type: "text", text: output.output }] - const model = modelOutput(output) - if (model) parts.push({ type: "text", text: model }) - return parts - }, execute: (input, context) => Effect.gen(function* () { const source = { @@ -199,6 +186,7 @@ export const Plugin = { timeout, metadata: { sessionID: context.sessionID }, }) + yield* context.progress({ shellID: info.id }) const captureShell = Effect.fn("ShellTool.captureShell")(function* () { const page = yield* shell.output(info.id, { limit: MAX_CAPTURE_BYTES }) @@ -232,7 +220,9 @@ export const Plugin = { } }) + const settled = yield* Deferred.make() const run = settleShell().pipe( + Effect.tap((output) => Deferred.succeed(settled, output)), Effect.map((output) => output.output), Effect.onInterrupt(() => shell.remove(info.id).pipe(Effect.ignore)), ) @@ -256,32 +246,8 @@ export const Plugin = { } } - let previousProgress: { readonly output: string; readonly truncated: boolean } | undefined - const progress = yield* Effect.sleep("1 second").pipe( - Effect.andThen( - captureShell().pipe( - Effect.flatMap((capture) => - Effect.gen(function* () { - if ( - previousProgress?.output === capture.output && - previousProgress.truncated === capture.truncated - ) - return - previousProgress = capture - yield* context.progress({ - structured: { truncated: capture.truncated }, - content: [{ type: "text", text: capture.output }], - }) - }), - ), - ), - ), - Effect.repeat(Schedule.forever), - Effect.forkIn(scope, { startImmediately: true }), - ) const result = yield* runtime.job.block({ id: job.id, sessionID: context.sessionID }).pipe( Effect.onInterrupt(() => runtime.job.cancel(job.id).pipe(Effect.ignore)), - Effect.ensuring(Fiber.interrupt(progress)), ) if (result?.type === "backgrounded") { yield* shell.timeout(info.id, 0) @@ -298,11 +264,23 @@ export const Plugin = { return yield* Effect.fail(new Error(result.info.error ?? "Command failed")) if (result?.info.status === "cancelled") return yield* Effect.fail(new Error("Command cancelled")) - return { - ...(yield* settleShell()), - ...(warnings.length ? { warnings } : {}), - } + return { ...(yield* Deferred.await(settled)), ...(warnings.length ? { warnings } : {}) } }).pipe( + Effect.map((output) => { + const content: [Content, ...Content[]] = [{ type: "text", text: output.output }] + const model = modelOutput(output) + if (model) content.push({ type: "text", text: model }) + return { + output, + content, + metadata: { + truncated: output.truncated, + ...("exit" in output && output.exit !== undefined ? { exit: output.exit } : {}), + ...("shellID" in output && output.shellID !== undefined ? { shellID: output.shellID } : {}), + ...("timeout" in output && output.timeout !== undefined ? { timeout: output.timeout } : {}), + }, + } + }), Effect.mapError( (error) => new ToolFailure({ message: `Unable to execute command: ${input.command}`, error }), ), diff --git a/packages/core/src/tool/skill.ts b/packages/core/src/tool/skill.ts index dcf0e8bf70e5..c39748920400 100644 --- a/packages/core/src/tool/skill.ts +++ b/packages/core/src/tool/skill.ts @@ -21,11 +21,6 @@ export const Output = Schema.Struct({ directory: Schema.String, output: Schema.String, }) -const StructuredOutput = Schema.Struct({ - name: Output.fields.name, - directory: Output.fields.directory, -}) - export const description = [ "Load a specialized skill when the task at hand matches one of the available skills in the instructions.", "", @@ -70,9 +65,6 @@ export const Plugin = { description, input: Input, output: Output, - structured: StructuredOutput, - toStructuredOutput: ({ output }) => ({ name: output.name, directory: output.directory }), - toModelOutput: ({ output }) => [{ type: "text", text: output.output }], execute: (input, context) => Effect.gen(function* () { const current = yield* skills.list() @@ -101,7 +93,13 @@ export const Plugin = { output: toModelOutput(skill, files), } }).pipe(Effect.mapError((error) => unableToLoad(input.id, error))) - }), + }).pipe( + Effect.map((output) => ({ + output, + content: output.output, + metadata: { name: output.name, directory: output.directory }, + })), + ), }), { codemode: false }, ), diff --git a/packages/core/src/tool/subagent.ts b/packages/core/src/tool/subagent.ts index 1f2f2f6c8bd5..daef4e1cf391 100644 --- a/packages/core/src/tool/subagent.ts +++ b/packages/core/src/tool/subagent.ts @@ -31,11 +31,6 @@ export const Output = Schema.Struct({ status: Schema.Literals(["completed", "running"]), output: Schema.String, }) -const StructuredOutput = Schema.Struct({ - sessionID: Output.fields.sessionID, - status: Output.fields.status, -}) - export const description = [ "Spawn a subagent: a child session running a configured agent with fresh context.", "Foreground (default) runs the subagent to completion and returns its final response.", @@ -119,9 +114,6 @@ export const Plugin = { description, input: Input, output: Output, - structured: StructuredOutput, - toStructuredOutput: ({ output }) => ({ sessionID: output.sessionID, status: output.status }), - toModelOutput: ({ output }) => [{ type: "text", text: output.output }], execute: (input, context) => Effect.gen(function* () { const parent = yield* runtime.session @@ -186,7 +178,7 @@ export const Plugin = { const background = input.background === true yield* context.progress({ - structured: { sessionID: child.id, status: "running" }, + metadata: { sessionID: child.id, status: "running" }, }) const run = Effect.gen(function* () { @@ -238,7 +230,13 @@ export const Plugin = { if (result?.info.status === "cancelled") return yield* new ToolFailure({ message: "Subagent cancelled" }) return { sessionID: child.id, status: "completed" as const, output: result?.info.output ?? NO_TEXT } - }), + }).pipe( + Effect.map((output) => ({ + output, + content: output.output, + metadata: { sessionID: output.sessionID, status: output.status }, + })), + ), }), { codemode: false }, ), diff --git a/packages/core/src/tool/tool.ts b/packages/core/src/tool/tool.ts index 7525c9c0ddc7..e9917f94ddc6 100644 --- a/packages/core/src/tool/tool.ts +++ b/packages/core/src/tool/tool.ts @@ -1,2 +1,90 @@ -export * as Tool from "@opencode-ai/plugin/v2/effect/tool" +export * as Tool from "./tool" export * from "@opencode-ai/plugin/v2/effect/tool" + +import type { ToolContent } from "@opencode-ai/ai" +import { + decodeInput, + encodeOutput, + type Any, + type Content, + type Context, + Failure, + type Metadata, +} from "@opencode-ai/plugin/v2/effect/tool" +import { Effect, Schema } from "effect" + +/** Non-empty canonical model content. */ +export type NonEmptyContent = readonly [ToolContent, ...ToolContent[]] + +/** + * The execution-local result of one tool call: the machine output for + * Code Mode, canonical model content, and optional UI metadata. The typed + * domain output never leaves this function. + */ +export type Execution = { + readonly output?: unknown + readonly content: NonEmptyContent + readonly metadata?: Metadata +} + +export const execute = (tool: Any, input: unknown, context: Context): Effect.Effect => + Effect.gen(function* () { + const decoded = yield* decodeInput(tool.input, input) + const result = yield* tool.execute(decoded, context) + if (tool.output === undefined) { + if ("output" in result) return yield* Effect.die("Tool result declared output without an output schema") + return { + content: contentFrom(result.content), + ...(result.metadata === undefined ? {} : { metadata: result.metadata }), + } + } + if (!("output" in result)) + return yield* Effect.fail(new Failure({ message: "Tool did not return its declared output" })) + const encoded = yield* encodeOutput(tool.output, result.output) + return { + output: encoded, + content: contentFrom(result.content, encoded), + ...(result.metadata === undefined ? {} : { metadata: result.metadata }), + } + }) + +/** Model content from the tool's projection, falling back to the stringified encoded output. */ +const contentFrom = (projected: string | ReadonlyArray | undefined, encoded?: unknown): NonEmptyContent => { + if (typeof projected === "string") return [textContent(projected)] + if (projected !== undefined) { + const mapped = nonEmpty(projected.map(toModelContent)) + if (mapped !== undefined) return mapped + } + return [textContent(stringify(encoded))] +} + +export const toModelContent = (part: Content): ToolContent => + part.type === "text" + ? { type: "text", text: part.text } + : { type: "file", uri: `data:${part.mime};base64,${part.data}`, mime: part.mime, name: part.name } + +export const nonEmpty = (content: ReadonlyArray): NonEmptyContent | undefined => + content.length > 0 ? (content as NonEmptyContent) : undefined + +const textContent = (text: string): ToolContent => ({ type: "text", text }) + +/** Human-readable text for an arbitrary value; strings pass through unchanged. */ +export const stringify = (value: unknown) => { + if (typeof value === "string") return value + try { + return JSON.stringify(value) ?? String(value) + } catch { + return String(value) + } +} + +const MetadataSchema = Schema.Record(Schema.String, Schema.Json) + +/** Defensive boundary: non-JSON or oversized metadata is dropped, never failing the producing call. */ +export const jsonMetadata = (value: unknown, maxBytes?: number): Metadata | undefined => { + if (value === undefined) return undefined + const decoded = Schema.decodeUnknownOption(MetadataSchema)(value) + if (decoded._tag === "None") return undefined + if (maxBytes !== undefined && Buffer.byteLength(JSON.stringify(decoded.value), "utf-8") > maxBytes) return undefined + return decoded.value +} diff --git a/packages/core/src/tool/tools.ts b/packages/core/src/tool/tools.ts index cfc4dd95dced..31ecc10cdf04 100644 --- a/packages/core/src/tool/tools.ts +++ b/packages/core/src/tool/tools.ts @@ -7,13 +7,13 @@ export type RegisterOptions = Tool.RegisterOptions export interface Interface { readonly register: ( - tools: Readonly>, + tools: Readonly>, options?: Tool.RegisterOptions, ) => Effect.Effect /** Internal atomic registration capability used by plugin transforms. */ readonly registerBatch: ( registrations: ReadonlyArray<{ - readonly tools: Readonly> + readonly tools: Readonly> readonly options?: Tool.RegisterOptions }>, ) => Effect.Effect diff --git a/packages/core/src/tool/webfetch.ts b/packages/core/src/tool/webfetch.ts index 0ce325099509..d3f894bdbc48 100644 --- a/packages/core/src/tool/webfetch.ts +++ b/packages/core/src/tool/webfetch.ts @@ -37,10 +37,6 @@ const Output = Schema.Struct({ format: Input.fields.format, output: Schema.String, }) -const StructuredOutput = Schema.Struct({ - contentType: Output.fields.contentType, -}) - type Format = (typeof Input.Type)["format"] const acceptHeader = (format: Format) => { @@ -129,9 +125,6 @@ export const Plugin = { description, input: Input, output: Output, - structured: StructuredOutput, - toStructuredOutput: ({ output }) => ({ contentType: output.contentType }), - toModelOutput: ({ output }) => [{ type: "text", text: output.output }], execute: (input, context) => Effect.gen(function* () { yield* Effect.try({ @@ -171,12 +164,13 @@ export const Plugin = { try: () => convert(content, contentType, input.format), catch: (error) => error, }) - return { + const result = { url: input.url, contentType, format: input.format, output, } + return { output: result, content: result.output, metadata: { contentType: result.contentType } } }).pipe(Effect.mapError((error) => new ToolFailure({ message: `Unable to fetch ${input.url}`, error }))), }), { codemode: false }, diff --git a/packages/core/src/tool/websearch.ts b/packages/core/src/tool/websearch.ts index 49b059c9dffa..bc6f1bcfa7d9 100644 --- a/packages/core/src/tool/websearch.ts +++ b/packages/core/src/tool/websearch.ts @@ -190,10 +190,6 @@ const Output = Schema.Struct({ provider: Provider, text: Schema.String, }) -const StructuredOutput = Schema.Struct({ - provider: Output.fields.provider, -}) - export const Plugin = { id: "opencode.tool.websearch", effect: Effect.fn("WebSearchTool.Plugin")(function* (ctx: PluginContext) { @@ -209,9 +205,6 @@ export const Plugin = { description, input: Input, output: Output, - structured: StructuredOutput, - toStructuredOutput: ({ output }) => ({ provider: output.provider }), - toModelOutput: ({ output }) => [{ type: "text", text: output.text }], execute: (input, context) => { const provider = selectProvider(context.sessionID, config, config.provider) return Effect.gen(function* () { @@ -250,10 +243,11 @@ export const Plugin = { ...(config.parallelApiKey ? { Authorization: `Bearer ${config.parallelApiKey}` } : {}), }, ) - return { + const output = { provider, text: text ?? NO_RESULTS, } + return { output, content: output.text, metadata: { provider: output.provider } } }).pipe( Effect.mapError( (error) => new ToolFailure({ message: `Unable to search the web for ${input.query}`, error }), diff --git a/packages/core/src/tool/write.ts b/packages/core/src/tool/write.ts index 6c8041ff4248..4d86df9729c8 100644 --- a/packages/core/src/tool/write.ts +++ b/packages/core/src/tool/write.ts @@ -53,13 +53,11 @@ export const Plugin = { .transform((draft) => draft.add( name, - Tool.withPermission( - Tool.make({ + Tool.make({ description: "Write content to one file. Relative paths resolve within the active Location. Absolute paths inside the Location are accepted. Explicit external absolute paths require external_directory approval before edit approval.", input: Input, output: Output, - toModelOutput: ({ output }) => [{ type: "text", text: toModelOutput(output) }], execute: (input, context) => Effect.gen(function* () { const source = { @@ -86,12 +84,11 @@ export const Plugin = { }) return yield* files.writeTextPreservingBom({ target, content: input.content }) }).pipe( + Effect.map((output) => ({ output, content: toModelOutput(output) })), Effect.mapError((error) => new ToolFailure({ message: `Unable to write ${input.path}`, error })), ), }), - "edit", - ), - { codemode: false }, + { codemode: false, permission: "edit" }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/test/codemode.test.ts b/packages/core/test/codemode.test.ts index 695be92145eb..b80eb1a9855b 100644 --- a/packages/core/test/codemode.test.ts +++ b/packages/core/test/codemode.test.ts @@ -9,14 +9,16 @@ describe("CodeMode", () => { it.effect("owns registrations, execute, and catalog materialization", () => Effect.gen(function* () { const codeMode = yield* CodeMode.Service - yield* codeMode.register({ - echo: Tool.make({ - description: "Echo text", - input: Schema.Struct({ text: Schema.String }), - output: Schema.String, - execute: ({ text }) => Effect.succeed(text), + yield* codeMode.register( + Tool.registrationEntries({ + echo: Tool.make({ + description: "Echo text", + input: Schema.Struct({ text: Schema.String }), + output: Schema.String, + execute: ({ text }) => Effect.succeed({ output: text }), + }), }), - }) + ) const materialized = yield* codeMode.materialize() expect(materialized.tool).toBeDefined() diff --git a/packages/core/test/database-migration.test.ts b/packages/core/test/database-migration.test.ts index 58673f20393c..f98ded68f3a0 100644 --- a/packages/core/test/database-migration.test.ts +++ b/packages/core/test/database-migration.test.ts @@ -25,6 +25,7 @@ import addSessionForkMigration from "@opencode-ai/core/database/migration/202607 import timeSuspendedMigration from "@opencode-ai/core/database/migration/20260709163752_time_suspended" import instructionSyncMigration from "@opencode-ai/core/database/migration/20260710025429_instruction_sync" import deleteToolProgressEventsMigration from "@opencode-ai/core/database/migration/20260722011141_delete_tool_progress_events" +import canonicalToolResultsMigration from "@opencode-ai/core/database/migration/20260722170000_canonical_tool_results" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { EventV2 } from "@opencode-ai/core/event" @@ -583,6 +584,208 @@ describe("DatabaseMigration", () => { ) }) + test("rewrites projected tool rows into the canonical result shape", async () => { + await run( + Effect.gen(function* () { + const db = yield* makeDb + yield* db.run( + sql`CREATE TABLE session_message (id text PRIMARY KEY, session_id text NOT NULL, type text NOT NULL, seq integer NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL)`, + ) + yield* db.run(sql`CREATE TABLE event (id text PRIMARY KEY, type text NOT NULL, data text NOT NULL)`) + const assistant = { + agent: "build", + model: { id: "model", providerID: "provider" }, + content: [ + { type: "text", text: "before" }, + { + type: "tool", + id: "call_content", + name: "grep", + state: { + status: "completed", + input: { pattern: "TODO" }, + content: [{ type: "text", text: "src/a.ts:1: TODO" }], + structured: { value: [{ file: "src/a.ts", line: 1 }] }, + }, + time: { created: 1, completed: 2 }, + }, + { + type: "tool", + id: "call_structured_only", + name: "read", + state: { + status: "completed", + input: { path: "README.md" }, + content: [], + structured: { text: "hello" }, + }, + time: { created: 1, completed: 2 }, + }, + { + type: "tool", + id: "call_hosted", + name: "web_search", + executed: true, + providerResultState: { blockType: "web_search_tool_result" }, + state: { + status: "completed", + input: { query: "effect" }, + content: [], + structured: {}, + result: { type: "json", value: [{ url: "https://example.com" }] }, + }, + time: { created: 1, completed: 2 }, + }, + { + type: "tool", + id: "call_failed", + name: "shell", + state: { + status: "error", + input: { command: "sleep 99" }, + error: { type: "tool.execution", message: "timed out" }, + content: [{ type: "text", text: "partial output" }], + structured: { truncated: false }, + result: { type: "error", value: "timed out" }, + }, + time: { created: 1, completed: 2 }, + }, + { + type: "tool", + id: "call_running", + name: "shell", + state: { + status: "running", + input: { command: "sleep 1" }, + structured: { truncated: false }, + content: [{ type: "text", text: "tick" }], + }, + time: { created: 1, ran: 2 }, + }, + ], + time: { created: 1 }, + } + yield* db.run( + sql`INSERT INTO session_message VALUES ('msg_tools', 'ses_test', 'assistant', 1, 10, 11, ${JSON.stringify(assistant)})`, + ) + yield* db.run( + sql`INSERT INTO session_message VALUES ('msg_user', 'ses_test', 'user', 2, 12, 13, '{"text":"hi","time":{"created":1}}')`, + ) + // A row that never decoded must be skipped, not fail the migration. + yield* db.run( + sql`INSERT INTO session_message VALUES ('msg_corrupt', 'ses_test', 'assistant', 3, 14, 15, 'not json')`, + ) + yield* db.run( + sql`INSERT INTO event VALUES ('evt_success', 'session.tool.success.1', ${JSON.stringify({ + sessionID: "ses_test", + assistantMessageID: "msg_tools", + callID: "call_hosted", + structured: {}, + content: [], + result: { type: "json", value: [{ url: "https://example.com" }] }, + executed: true, + })})`, + ) + yield* db.run( + sql`INSERT INTO event VALUES ('evt_failed', 'session.tool.failed.1', ${JSON.stringify({ + sessionID: "ses_test", + assistantMessageID: "msg_tools", + callID: "call_failed", + error: { type: "tool.execution", message: "timed out" }, + metadata: { truncated: false }, + executed: false, + })})`, + ) + + yield* DatabaseMigration.applyOnly(db, [canonicalToolResultsMigration]) + + const row = yield* db.get<{ data: string }>(sql`SELECT data FROM session_message WHERE id = 'msg_tools'`) + const migrated = JSON.parse(row!.data) + // Every migrated row must decode with the current schema; reload hard-fails otherwise. + Schema.decodeUnknownSync(SessionMessage.Info)({ ...migrated, id: "msg_tools", type: "assistant" }) + const states = new Map( + migrated.content.flatMap((part: { type: string; id?: string }) => + part.type === "tool" ? [[part.id, part]] : [], + ), + ) + expect(states.get("call_content")).toMatchObject({ + state: { + status: "completed", + input: { pattern: "TODO" }, + content: [{ type: "text", text: "src/a.ts:1: TODO" }], + // Old generic structured payloads survive as canonical metadata. + metadata: { value: [{ file: "src/a.ts", line: 1 }] }, + }, + }) + expect((states.get("call_content") as { state: Record }).state).not.toHaveProperty( + "structured", + ) + expect(states.get("call_structured_only")).toMatchObject({ + state: { + status: "completed", + content: [{ type: "text", text: JSON.stringify({ text: "hello" }, null, 2) }], + metadata: { text: "hello" }, + }, + }) + expect(states.get("call_hosted")).toMatchObject({ + executed: true, + providerResultState: { + blockType: "web_search_tool_result", + result: [{ url: "https://example.com" }], + }, + state: { + status: "completed", + content: [{ type: "text", text: JSON.stringify([{ url: "https://example.com" }], null, 2) }], + }, + }) + expect(states.get("call_failed")).toMatchObject({ + state: { + status: "error", + error: { type: "tool.execution", message: "timed out" }, + content: [{ type: "text", text: "partial output" }], + metadata: { truncated: false }, + }, + }) + const failedState = (states.get("call_failed") as { state: Record }).state + expect(failedState).not.toHaveProperty("result") + expect(failedState).not.toHaveProperty("structured") + expect(states.get("call_running")).toMatchObject({ + state: { + status: "running", + metadata: { truncated: false }, + }, + }) + const event = yield* db.get<{ type: string; data: string }>(sql`SELECT type, data FROM event WHERE id = 'evt_success'`) + expect(event!.type).toBe("session.tool.success.1") + expect(JSON.parse(event!.data)).toEqual({ + sessionID: "ses_test", + assistantMessageID: "msg_tools", + callID: "call_hosted", + structured: {}, + content: [], + result: { type: "json", value: [{ url: "https://example.com" }] }, + executed: true, + }) + const failedEvent = yield* db.get<{ type: string; data: string }>(sql`SELECT type, data FROM event WHERE id = 'evt_failed'`) + expect(failedEvent!.type).toBe("session.tool.failed.1") + expect(JSON.parse(failedEvent!.data)).toEqual({ + sessionID: "ses_test", + assistantMessageID: "msg_tools", + callID: "call_failed", + error: { type: "tool.execution", message: "timed out" }, + metadata: { truncated: false }, + executed: false, + }) + expect(yield* db.get(sql`SELECT data FROM session_message WHERE id = 'msg_user'`)).toEqual({ + data: '{"text":"hi","time":{"created":1}}', + }) + expect(yield* db.get(sql`SELECT data FROM session_message WHERE id = 'msg_corrupt'`)).toEqual({ + data: "not json", + }) + }), + ) + }) + test("records the authoritative parent sequence on existing forks", async () => { await run( Effect.gen(function* () { diff --git a/packages/core/test/lib/tool.ts b/packages/core/test/lib/tool.ts index e3fdccb13ae7..2f41c3b9b47d 100644 --- a/packages/core/test/lib/tool.ts +++ b/packages/core/test/lib/tool.ts @@ -14,7 +14,7 @@ export const toolIdentity = { } export const toolDefinitions = (registry: ToolRegistry.Interface, permissions?: PermissionV2.Ruleset) => - registry.materialize(permissions).pipe(Effect.map((materialized) => materialized.definitions)) + registry.snapshot(permissions).pipe(Effect.map((toolSet) => toolSet.definitions)) export function waitForTool( registry: ToolRegistry.Interface, @@ -35,7 +35,7 @@ export function waitForTool( /** * Registers a core tool plugin's tools against the real registry without booting the * full plugin host. Only the tool domain is live; focused tool tests exercise - * registration, materialization, and settlement through the same path production uses. + * registration, snapshots, and execution through the same path production uses. */ export const registerToolPlugin = (plugin: { readonly id: string @@ -52,7 +52,7 @@ export const registerToolPlugin = (plugin: { Effect.gen(function* () { const registrations: Array<{ readonly name: string - readonly tool: Tool.AnyTool + readonly tool: Tool.Any readonly options?: Tool.RegisterOptions }> = [] callback({ @@ -73,8 +73,5 @@ export const registerToolPlugin = (plugin: { yield* plugin.effect(context) }) -export const settleTool = (registry: ToolRegistry.Interface, input: ToolRegistry.ExecuteInput) => - registry.materialize().pipe(Effect.flatMap((materialized) => materialized.settle(input))) - export const executeTool = (registry: ToolRegistry.Interface, input: ToolRegistry.ExecuteInput) => - settleTool(registry, input).pipe(Effect.map((settlement) => settlement.result)) + registry.snapshot().pipe(Effect.flatMap((toolSet) => toolSet.execute(input))) diff --git a/packages/core/test/mcp.test.ts b/packages/core/test/mcp.test.ts index 158ad32c6741..55933feb1f9f 100644 --- a/packages/core/test/mcp.test.ts +++ b/packages/core/test/mcp.test.ts @@ -33,7 +33,7 @@ import { Image } from "@opencode-ai/core/image" import { testEffect } from "./lib/effect" import { imagePassthrough } from "./lib/image" import { location } from "./fixture/location" -import { settleTool, toolDefinitions, toolIdentity, waitForTool } from "./lib/tool" +import { executeTool, toolDefinitions, toolIdentity, waitForTool } from "./lib/tool" let assertion: Deferred.Deferred | undefined let decision: Effect.Effect = Effect.void @@ -241,10 +241,41 @@ const mcp = Layer.mock(MCP.Service, { description: "Lookup", inputSchema: { type: "object", properties: {} }, }), + new MCP.Tool({ + server: MCP.ServerName.make("direct"), + name: "fail", + codemode: false, + description: "Always fails", + inputSchema: { type: "object", properties: {} }, + }), + new MCP.Tool({ + server: MCP.ServerName.make("direct"), + name: "media", + codemode: false, + description: "Returns text and an image", + inputSchema: { type: "object", properties: {} }, + }), ]), callTool: (input) => Effect.sync(() => { calls += 1 + if (input.name === "fail") + return new MCP.ToolResult({ + server: MCP.ServerName.make(input.server), + tool: input.name, + isError: true, + content: [{ type: "text", text: "search index unavailable" }], + }) + if (input.name === "media") + return new MCP.ToolResult({ + server: MCP.ServerName.make(input.server), + tool: input.name, + isError: false, + content: [ + { type: "text", text: "rendered chart" }, + { type: "media", data: "aGVsbG8=", mimeType: "image/png" }, + ], + }) return new MCP.ToolResult({ server: MCP.ServerName.make(input.server), tool: input.name, @@ -647,9 +678,7 @@ test("loads and reads MCP resources", async () => { }) expect(server.clientVersion()).toMatchObject({ name: "sdk", version: "1.2.3" }) }).pipe( - Effect.provide( - resourceMcpLayer(server.url, undefined, { clientInfo: { name: "sdk", version: "1.2.3" } }), - ), + Effect.provide(resourceMcpLayer(server.url, undefined, { clientInfo: { name: "sdk", version: "1.2.3" } })), ) }), ), @@ -774,8 +803,8 @@ it.effect("advertises MCP output schemas to Code Mode", () => Effect.gen(function* () { const registry = yield* ToolRegistry.Service yield* waitForTool(registry, "execute") - const materialized = yield* registry.materialize() - const execute = materialized.definitions.find((tool) => tool.name === "execute") + const definitions = yield* toolDefinitions(registry) + const execute = definitions.find((tool) => tool.name === "execute") expect(execute?.description).not.toContain("tools.demo.search") }), @@ -793,6 +822,50 @@ it.effect("advertises MCP tools directly when Code Mode is disabled for the serv }), ) +// Baseline (PLAN.md step 1): MCP isError must become one failed tool call, not a +// success whose text happens to describe an error. +it.effect("fails the call when MCP reports isError", () => + Effect.gen(function* () { + assertion = yield* Deferred.make() + decision = Effect.void + const registry = yield* ToolRegistry.Service + yield* waitForTool(registry, "direct_fail") + + const execution = yield* executeTool(registry, { + sessionID: SessionV2.ID.make("ses_mcp_is_error"), + ...toolIdentity, + call: { type: "tool-call", id: "call_mcp_is_error", name: "direct_fail", input: {} }, + }) + + expect(execution).toMatchObject({ status: "error", error: { message: "search index unavailable" } }) + expect(execution.content).toBeUndefined() + }), +) + +// Baseline (PLAN.md step 1): mixed MCP text and media content must reach the model intact. +it.effect("preserves MCP text and media content for the model", () => + Effect.gen(function* () { + assertion = yield* Deferred.make() + decision = Effect.void + const registry = yield* ToolRegistry.Service + yield* waitForTool(registry, "direct_media") + + const execution = yield* executeTool(registry, { + sessionID: SessionV2.ID.make("ses_mcp_media"), + ...toolIdentity, + call: { type: "tool-call", id: "call_mcp_media", name: "direct_media", input: {} }, + }) + + expect(execution.status).toBe("completed") + if (execution.status !== "completed") return + expect(execution.output).toBe("rendered chart") + expect(execution.content).toMatchObject([ + { type: "text", text: "rendered chart" }, + { type: "file", mime: "image/png" }, + ]) + }), +) + it.effect("waits for permission before calling an MCP tool", () => Effect.gen(function* () { calls = 0 @@ -802,7 +875,7 @@ it.effect("waits for permission before calling an MCP tool", () => const registry = yield* ToolRegistry.Service yield* waitForTool(registry, "execute") - const fiber = yield* settleTool(registry, { + const fiber = yield* executeTool(registry, { sessionID: SessionV2.ID.make("ses_mcp_permission"), ...toolIdentity, call: { @@ -841,7 +914,7 @@ it.effect("does not call MCP when permission is blocked", () => const registry = yield* ToolRegistry.Service yield* waitForTool(registry, "execute") - const settlement = yield* settleTool(registry, { + const execution = yield* executeTool(registry, { sessionID: SessionV2.ID.make("ses_mcp_blocked"), ...toolIdentity, call: { @@ -851,8 +924,9 @@ it.effect("does not call MCP when permission is blocked", () => input: { code: "return await tools.demo.search({})" }, }, }) - expect(settlement.result).toEqual({ type: "text", value: "Unable to execute demo_search" }) - expect(settlement.output?.structured).toEqual({ + expect(execution.status).toBe("completed") + expect(execution.content).toEqual([{ type: "text", text: "Unable to execute demo_search" }]) + expect(execution.metadata).toEqual({ toolCalls: [{ tool: "demo.search", status: "error" }], error: true, }) diff --git a/packages/core/test/plugin.test.ts b/packages/core/test/plugin.test.ts index 0d19e4e74b96..b7840581498d 100644 --- a/packages/core/test/plugin.test.ts +++ b/packages/core/test/plugin.test.ts @@ -258,7 +258,7 @@ describe("PluginV2", () => { description: "Plugin tool", input: Schema.Struct({}), output: Schema.Struct({ ok: Schema.Boolean }), - execute: () => Effect.succeed({ ok: true }), + execute: () => Effect.succeed({ output: { ok: true } }), }), { codemode: false }, ), @@ -267,10 +267,10 @@ describe("PluginV2", () => { }) yield* plugins.activate([versioned(plugin)]) - expect((yield* registry.materialize()).definitions.map((tool) => tool.name)).toContain("plugin_tool") + expect((yield* registry.snapshot()).definitions.map((tool) => tool.name)).toContain("plugin_tool") yield* plugins.activate([]) - expect((yield* registry.materialize()).definitions.map((tool) => tool.name)).not.toContain("plugin_tool") + expect((yield* registry.snapshot()).definitions.map((tool) => tool.name)).not.toContain("plugin_tool") }), ) @@ -283,7 +283,7 @@ describe("PluginV2", () => { description, input: Schema.Struct({}), output: Schema.Struct({ ok: Schema.Boolean }), - execute: () => Effect.succeed({ ok: true }), + execute: () => Effect.succeed({ output: { ok: true } }), }) const plugin = EffectPlugin.define({ id: "grouped-tools", @@ -299,7 +299,7 @@ describe("PluginV2", () => { yield* plugins.activate([versioned(plugin)]) - expect((yield* registry.materialize()).definitions.map((tool) => tool.name)).toEqual([ + expect((yield* registry.snapshot()).definitions.map((tool) => tool.name)).toEqual([ "plain", "context7_look_up", "execute", @@ -307,14 +307,14 @@ describe("PluginV2", () => { }), ) - it.effect("fires before/after tool hooks with mutable events around settlement", () => + it.effect("fires before/after tool hooks with mutable events around execution", () => Effect.gen(function* () { const plugins = yield* PluginV2.Service const registry = yield* ToolRegistry.Service const executed: unknown[] = [] const seen: { before?: unknown - after?: { input: unknown; result: unknown; output: unknown } + after?: { input: unknown; status: string; content: unknown; metadata: unknown } } = {} const plugin = EffectPlugin.define({ @@ -329,7 +329,8 @@ describe("PluginV2", () => { description: "Echo", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), - execute: ({ text }) => Effect.sync(() => executed.push({ text })).pipe(Effect.as({ text })), + execute: ({ text }) => + Effect.sync(() => executed.push({ text })).pipe(Effect.as({ output: { text } })), }), { codemode: false }, ), @@ -348,9 +349,23 @@ describe("PluginV2", () => { yield* ctx.tool .hook("execute.after", (event) => Effect.sync(() => { - seen.after = { input: event.input, result: event.result, output: event.output } - event.result = { type: "text", value: "after-mutated" } - event.output = { structured: { rewritten: true }, content: [] } + seen.after = { + input: event.input, + status: event.status, + content: event.content, + metadata: event.metadata, + } + if (event.status !== "completed") return + event.content = [{ type: "text", text: "after-mutated" }] + event.metadata = { rewritten: true } + }), + ) + .pipe(Effect.asVoid) + + yield* ctx.tool + .hook("execute.after", (event) => + Effect.sync(() => { + if (event.status === "completed") event.content = [] as never }), ) .pipe(Effect.asVoid) @@ -359,8 +374,8 @@ describe("PluginV2", () => { yield* plugins.activate([versioned(plugin)]) - const materialized = yield* registry.materialize() - const settlement = yield* materialized.settle({ + const toolSet = yield* registry.snapshot() + const execution = yield* toolSet.execute({ sessionID: SessionV2.ID.make("ses_hooks"), agent: AgentV2.ID.make("build"), messageID: SessionMessage.ID.make("msg_hooks"), @@ -371,11 +386,15 @@ describe("PluginV2", () => { expect(executed).toEqual([{ text: "before-mutated" }]) expect(seen.after).toEqual({ input: { text: "before-mutated" }, - result: { type: "json", value: { text: "before-mutated" } }, - output: { structured: { text: "before-mutated" }, content: [] }, + status: "completed", + content: [{ type: "text", text: '{"text":"before-mutated"}' }], + metadata: undefined, + }) + expect(execution).toMatchObject({ + status: "completed", + content: [{ type: "text", text: "after-mutated" }], + metadata: { rewritten: true }, }) - expect(settlement.result).toEqual({ type: "text", value: "after-mutated" }) - expect(settlement.output).toEqual({ structured: { rewritten: true }, content: [] }) }), ) }) diff --git a/packages/core/test/plugin/promise.test.ts b/packages/core/test/plugin/promise.test.ts index 7922d965bba3..76e2a1e31b15 100644 --- a/packages/core/test/plugin/promise.test.ts +++ b/packages/core/test/plugin/promise.test.ts @@ -14,6 +14,7 @@ import { SessionPending } from "@opencode-ai/core/session/pending" import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { ProviderV2 } from "@opencode-ai/core/provider" import { Plugin } from "@opencode-ai/plugin/v2" +import { Tool } from "@opencode-ai/plugin/v2/tool" import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session" import { Model } from "@opencode-ai/schema/model" import { Provider } from "@opencode-ai/schema/provider" @@ -270,7 +271,7 @@ describe("fromPromise", () => { }), ) - it.effect("constructs plain Promise tool declarations in the host", () => + it.effect("constructs plain Promise tool definitions in the host", () => Effect.gen(function* () { const plugins = yield* PluginV2.Service const registry = yield* ToolRegistry.Service @@ -280,35 +281,41 @@ describe("fromPromise", () => { id: "promise-tool", setup: async (ctx) => { await ctx.tool.transform((tools) => { - tools.add({ - name: "hello", - options: { codemode: false }, - description: "Hello", - input: Schema.Struct({ name: Schema.String }), - output: Schema.String, - execute: async ({ name }, context) => { - await context.progress({ structured: { phase: "greeting" } }) - return `Hello, ${name}!` - }, - }) + tools.add( + "hello", + Tool.make({ + description: "Hello", + input: Schema.Struct({ name: Schema.String }), + output: Schema.String, + execute: async ({ name }, context) => { + await context.progress({ phase: "greeting" }) + return { output: `Hello, ${name}!` } + }, + }), + { codemode: false }, + ) }) }, }) yield* PluginPromise.fromPromise(promisePlugin).effect(host) - const materialized = yield* registry.materialize() - expect(materialized.definitions).toContainEqual(expect.objectContaining({ name: "hello", description: "Hello" })) + const toolSet = yield* registry.snapshot() + expect(toolSet.definitions).toContainEqual(expect.objectContaining({ name: "hello", description: "Hello" })) expect( - yield* materialized.settle({ + yield* toolSet.execute({ sessionID: SessionV2.ID.make("ses_promise_tool"), agent: AgentV2.ID.make("build"), messageID: SessionMessage.ID.make("msg_promise_tool"), progress: (update) => Effect.sync(() => progress.push(update)), call: { type: "tool-call", id: "call_promise_tool", name: "hello", input: { name: "world" } }, }), - ).toMatchObject({ result: { type: "text", value: "Hello, world!" } }) - expect(progress).toEqual([{ structured: { phase: "greeting" }, content: [] }]) + ).toMatchObject({ + status: "completed", + output: "Hello, world!", + content: [{ type: "text", text: "Hello, world!" }], + }) + expect(progress).toEqual([{ phase: "greeting" }]) }), ) }) diff --git a/packages/core/test/session-generate.test.ts b/packages/core/test/session-generate.test.ts index e78f41bdac0f..7471564610bf 100644 --- a/packages/core/test/session-generate.test.ts +++ b/packages/core/test/session-generate.test.ts @@ -95,10 +95,10 @@ const references = Layer.mock(ReferenceInstructions.Service, { load: () => Effec const mcp = Layer.mock(McpInstructions.Service, { load: () => Effect.succeed(Instructions.empty) }) const plugins = Layer.mock(PluginSupervisor.Service, { flush: Effect.void }) const tools = Layer.mock(ToolRegistry.Service, { - materialize: () => + snapshot: () => Effect.succeed({ definitions: [ToolDefinition.make({ name: "lookup", description: "Lookup", inputSchema: { type: "object" } })], - settle: () => Effect.die(new Error("unused")), + execute: () => Effect.die(new Error("unused")), }), register: () => Effect.die(new Error("unused")), registerBatch: () => Effect.die(new Error("unused")), diff --git a/packages/core/test/session-instructions.test.ts b/packages/core/test/session-instructions.test.ts index f0720cfc12f7..9d40f95f4b56 100644 --- a/packages/core/test/session-instructions.test.ts +++ b/packages/core/test/session-instructions.test.ts @@ -34,7 +34,7 @@ import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { tempLocationLayer } from "./fixture/location" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { testEffect } from "./lib/effect" -import { registerToolPlugin, settleTool } from "./lib/tool" +import { executeTool, registerToolPlugin } from "./lib/tool" const readToolNode = makeLocationNode({ name: "test/read-tool-plugin", @@ -163,7 +163,7 @@ describe("SessionInstructions", () => { // A read deep under sub/ discovers deep and sub AGENTS.md, walking up to but // excluding the Location root (already supplied by core initial instructions). - yield* settleTool(registry, readCall(sessionID, "call-deep", "sub/deep/file.txt")) + yield* executeTool(registry, readCall(sessionID, "call-deep", "sub/deep/file.txt")) const firstInjected = yield* synthetics(sessionID) expect(firstInjected).toHaveLength(1) @@ -179,7 +179,7 @@ describe("SessionInstructions", () => { // A sibling read under sub/other discovers only the new AGENTS.md; sub is already // injected for this session so it is not re-emitted, and the root is still excluded. - yield* settleTool(registry, readCall(sessionID, "call-other", "sub/other/file2.txt")) + yield* executeTool(registry, readCall(sessionID, "call-other", "sub/other/file2.txt")) const secondInjected = yield* synthetics(sessionID) expect(secondInjected).toHaveLength(2) @@ -210,7 +210,7 @@ describe("SessionInstructions", () => { yield* seedSynthetic(sessionID, [subPath]) expect(yield* synthetics(sessionID)).toHaveLength(1) - yield* settleTool(registry, readCall(sessionID, "call-sub", "sub/file.txt")) + yield* executeTool(registry, readCall(sessionID, "call-sub", "sub/file.txt")) // The durable claim on the prior synthetic prevents re-injection; no new synthetic. expect(yield* synthetics(sessionID)).toHaveLength(1) @@ -236,7 +236,7 @@ describe("SessionInstructions", () => { // Listing packages/foo/ discovers its own AGENTS.md, walking up to but excluding // the Location root (already supplied by core initial instructions). - yield* settleTool(registry, readCall(sessionID, "call-list", "packages/foo")) + yield* executeTool(registry, readCall(sessionID, "call-list", "packages/foo")) const firstInjected = yield* synthetics(sessionID) expect(firstInjected).toHaveLength(1) @@ -247,7 +247,7 @@ describe("SessionInstructions", () => { // A subsequent file read under the listed directory is a dedup: pkg's AGENTS.md is // already injected for this session, so nothing new is emitted. - yield* settleTool(registry, readCall(sessionID, "call-file", "packages/foo/file.txt")) + yield* executeTool(registry, readCall(sessionID, "call-file", "packages/foo/file.txt")) expect(yield* synthetics(sessionID)).toHaveLength(1) }), @@ -269,7 +269,7 @@ describe("SessionInstructions", () => { // The walk starts and stops at the Location root: the root AGENTS.md is searched but // dropped by the dirname filter, and up() only walks upward so nested dirs are unseen. - yield* settleTool(registry, readCall(sessionID, "call-root-list", ".")) + yield* executeTool(registry, readCall(sessionID, "call-root-list", ".")) expect(yield* synthetics(sessionID)).toHaveLength(0) }), diff --git a/packages/core/test/session-runner-message.test.ts b/packages/core/test/session-runner-message.test.ts index a79b351cfa3d..3cebdcb985d1 100644 --- a/packages/core/test/session-runner-message.test.ts +++ b/packages/core/test/session-runner-message.test.ts @@ -367,8 +367,7 @@ Recent work state: SessionMessage.ToolStateRunning.make({ status: "running", input: { path: "README.md" }, - content: [], - structured: { type: "media", mime: "image/png" }, + metadata: { type: "media", mime: "image/png" }, }), time: { created }, }), @@ -388,7 +387,6 @@ Recent work name: "hello.png", }, ], - structured: {}, }), time: { created, completed: created }, }), @@ -403,7 +401,6 @@ Recent work status: "completed", input: { query: "Effect" }, content: [{ type: "text", text: "Found it" }], - structured: {}, }), time: { created, completed: created }, }), @@ -416,8 +413,6 @@ Recent work state: SessionMessage.ToolStateError.make({ status: "error", input: { path: "README.md" }, - content: [], - structured: {}, error: { type: "unknown", message: "Denied" }, }), time: { created, completed: created }, @@ -473,7 +468,7 @@ Recent work providerMetadata: { provider: { continuation: "failed" } }, result: { type: "error", - value: { error: { type: "unknown", message: "Denied" }, content: [], structured: {} }, + value: { error: { type: "unknown", message: "Denied" }, content: [] }, }, }, ]) @@ -575,9 +570,7 @@ Recent work state: SessionMessage.ToolStateCompleted.make({ status: "completed", input: { query: "Effect" }, - content: [], - structured: {}, - result: { type: "json", value: { found: true } }, + content: [{ type: "text", text: '{"found":true}' }], }), time: { created, completed: created }, }), @@ -592,8 +585,6 @@ Recent work status: "error", input: { query: "Effect" }, error: { type: "unknown", message: "Step interrupted" }, - content: [], - structured: {}, }), time: { created, completed: created }, }), @@ -620,8 +611,10 @@ Recent work type: "tool-result", id: "hosted-completed", name: "web_search", - result: { type: "json", value: { found: true } }, + result: { type: "text", value: '{"found":true}' }, providerExecuted: true, + cache: undefined, + metadata: undefined, providerMetadata: { provider: { itemId: "result_completed" } }, }, { @@ -630,7 +623,7 @@ Recent work name: "web_search", input: { query: "Effect" }, providerExecuted: true, - providerMetadata: undefined, + providerMetadata: { provider: { itemId: "call_failed" } }, }, { type: "tool-result", @@ -641,18 +634,17 @@ Recent work value: { error: { type: "unknown", message: "Step interrupted" }, content: [], - structured: {}, }, }, providerExecuted: true, cache: undefined, metadata: undefined, - providerMetadata: undefined, + providerMetadata: { provider: { itemId: "result_failed" } }, }, ]) }) - test("drops provider-native continuation metadata after a model switch", () => { + test("drops model-scoped continuation metadata after a model switch but keeps hosted result payloads", () => { const messages = toLLMMessages( [ SessionMessage.Assistant.make({ @@ -676,9 +668,7 @@ Recent work state: SessionMessage.ToolStateCompleted.make({ status: "completed", input: { query: "Effect" }, - content: [], - structured: {}, - result: { type: "json", value: { status: "completed" } }, + content: [{ type: "text", text: '{"status":"completed"}' }], }), time: { created, completed: created }, }), @@ -692,8 +682,7 @@ Recent work state: SessionMessage.ToolStateCompleted.make({ status: "completed", input: { path: "README.md" }, - content: [], - structured: { text: "Hello" }, + content: [{ type: "text", text: "Hello" }], }), time: { created, completed: created }, }), @@ -718,11 +707,13 @@ Recent work type: "tool-result", id: "hosted-old-model", name: "web_search", - result: { type: "json", value: { status: "completed" } }, + result: { type: "text", value: '{"status":"completed"}' }, providerExecuted: true, cache: undefined, metadata: undefined, - providerMetadata: undefined, + // Hosted result payloads are provider-format state and must survive a + // model switch within the same provider for replay to stay valid. + providerMetadata: { provider: { itemId: "hosted-old-model" } }, }, { type: "tool-call", @@ -738,7 +729,7 @@ Recent work type: "tool-result", id: "local-old-model", name: "read", - result: { type: "json", value: { text: "Hello" } }, + result: { type: "text", value: "Hello" }, providerExecuted: false, cache: undefined, metadata: undefined, diff --git a/packages/core/test/session-runner-tool-events.test.ts b/packages/core/test/session-runner-tool-events.test.ts index 66a0a43f545f..2ba096b306a3 100644 --- a/packages/core/test/session-runner-tool-events.test.ts +++ b/packages/core/test/session-runner-tool-events.test.ts @@ -50,7 +50,7 @@ const capture = (providerMetadataKey = "anthropic", options?: { readonly interru } const call = LLMEvent.toolCall({ id: "call-image", name: "read", input: { path: "pixel.png" } }) -const result = LLMEvent.toolResult({ +const hostedResult = LLMEvent.toolResult({ id: "call-image", name: "read", result: { @@ -60,25 +60,28 @@ const result = LLMEvent.toolResult({ { type: "file", uri: `data:image/png;base64,${base64}`, mime: "image/png", name: "pixel.png" }, ], }, - output: { - structured: { type: "media", mime: "image/png" }, - content: [ - { type: "text", text: "Image read successfully" }, - { type: "file", uri: `data:image/png;base64,${base64}`, mime: "image/png", name: "pixel.png" }, - ], - }, }) -test("local tool success serializes media base64 once and reconstructs from structured content", async () => { +test("local tool success serializes media base64 once through canonical content", async () => { const { published, publisher } = capture() await Effect.runPromise(publisher.publish(call)) - await Effect.runPromise(publisher.publish(result)) + await Effect.runPromise( + publisher.toolExecution(call.id, call.name, { + status: "completed", + output: { type: "media", mime: "image/png" }, + content: [ + { type: "text", text: "Image read successfully" }, + { type: "file", uri: `data:image/png;base64,${base64}`, mime: "image/png", name: "pixel.png" }, + ], + }), + ) - const success = published.find((event) => event.type === "session.tool.success.1") + const success = published.find((event) => event.type === "session.tool.success.2") expect(success).toBeDefined() const serialized = JSON.stringify(success) expect(serialized.split(base64)).toHaveLength(2) expect(success?.data).not.toHaveProperty("result") + expect(success?.data).not.toHaveProperty("output") expect(success?.data).toMatchObject({ content: [ @@ -88,29 +91,41 @@ test("local tool success serializes media base64 once and reconstructs from stru }) }) -test("provider-executed success retains its raw provider result", async () => { +test("provider-executed success derives content and retains provider result state", async () => { const { published, publisher } = capture() await Effect.runPromise(publisher.publish(LLMEvent.toolCall({ ...call, providerExecuted: true }))) - await Effect.runPromise(publisher.publish(LLMEvent.toolResult({ ...result, providerExecuted: true }))) - const success = published.find((event) => event.type === "session.tool.success.1") - expect(success?.data).toHaveProperty("result") + await Effect.runPromise( + publisher.publish( + LLMEvent.toolResult({ + ...hostedResult, + providerExecuted: true, + providerMetadata: { anthropic: { result: { type: "content", value: [] } } }, + }), + ), + ) + const success = published.find((event) => event.type === "session.tool.success.2") + expect(success?.data).not.toHaveProperty("result") + expect(success?.data).toMatchObject({ + executed: true, + content: [ + { type: "text", text: "Image read successfully" }, + { type: "file", uri: `data:image/png;base64,${base64}`, mime: "image/png" }, + ], + resultState: { result: { type: "content" } }, + }) }) -test("interrupted progress publication remains in the terminal failure snapshot", async () => { +test("interrupted progress metadata remains in the terminal failure snapshot", async () => { const { published, publisher } = capture("anthropic", { interruptProgress: true }) await Effect.runPromise(publisher.publish(call)) const exit = await Effect.runPromiseExit( - publisher.progress(call.id, { - structured: { phase: "visible" }, - content: [{ type: "text", text: "visible" }], - }), + publisher.progress(call.id, { phase: "visible" }), ) expect(Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)).toBe(true) await Effect.runPromise(publisher.failUnsettledTools({ type: "aborted", message: "interrupted" })) - expect(published.find((event) => event.type === "session.tool.failed.1")?.data).toMatchObject({ + expect(published.find((event) => event.type === "session.tool.failed.2")?.data).toMatchObject({ metadata: { phase: "visible" }, - content: [{ type: "text", text: "visible" }], }) }) @@ -119,7 +134,7 @@ test("failure before progress omits partial output fields", async () => { await Effect.runPromise(publisher.publish(call)) await Effect.runPromise(publisher.failUnsettledTools({ type: "aborted", message: "interrupted" })) - const failed = published.find((event) => event.type === "session.tool.failed.1")?.data + const failed = published.find((event) => event.type === "session.tool.failed.2")?.data expect(failed).not.toHaveProperty("content") expect(failed).not.toHaveProperty("metadata") }) @@ -192,7 +207,7 @@ test("provider-executed tool metadata is flattened using the route key", async ( expect(published.find((event) => event.type === "session.tool.called.1")?.data).toMatchObject({ state: { itemId: "call" }, }) - expect(published.find((event) => event.type === "session.tool.success.1")?.data).toMatchObject({ + expect(published.find((event) => event.type === "session.tool.success.2")?.data).toMatchObject({ resultState: { itemId: "result" }, }) }) @@ -201,29 +216,30 @@ test("binary failure emits no success event", async () => { const { published, publisher } = capture() await Effect.runPromise(publisher.publish(call)) await Effect.runPromise( - publisher.publish( - LLMEvent.toolResult({ - id: call.id, - name: call.name, - result: { type: "error", value: "Cannot read binary file" }, - }), - ), + publisher.toolExecution(call.id, call.name, { + status: "error", + error: { type: "tool.execution", message: "Cannot read binary file" }, + }), ) - expect(published.some((event) => event.type === "session.tool.success.1")).toBe(false) - expect(published.some((event) => event.type === "session.tool.failed.1")).toBe(true) + expect(published.some((event) => event.type === "session.tool.success.2")).toBe(false) + expect(published.some((event) => event.type === "session.tool.failed.2")).toBe(true) }) -test("success event data can carry a provider-executed result", () => { +test("success event data can carry provider-executed result state", () => { const decoded = Schema.decodeUnknownSync(SessionEvent.Tool.Success.data)({ sessionID, assistantMessageID: SessionMessage.ID.create(), callID: "call-old", - structured: { type: "media", mime: "image/png" }, content: [{ type: "file", uri: `data:image/png;base64,${base64}`, mime: "image/png" }], - result: { type: "content", value: [{ type: "file", uri: `data:image/png;base64,${base64}`, mime: "image/png" }] }, executed: true, + resultState: { + result: { + type: "content", + value: [{ type: "file", uri: `data:image/png;base64,${base64}`, mime: "image/png" }], + }, + }, }) - expect(decoded.result).toMatchObject({ type: "content" }) + expect(decoded.resultState).toMatchObject({ result: { type: "content" } }) }) test("step finish records settlement without publishing step ended", async () => { diff --git a/packages/core/test/session-runner-tool-registry.test.ts b/packages/core/test/session-runner-tool-registry.test.ts index 053b2152698c..29e1e17c145e 100644 --- a/packages/core/test/session-runner-tool-registry.test.ts +++ b/packages/core/test/session-runner-tool-registry.test.ts @@ -8,23 +8,24 @@ import { SessionV2 } from "@opencode-ai/core/session" import { SessionMessage } from "@opencode-ai/core/session/message" import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { executeTool, settleTool, toolDefinitions } from "./lib/tool" +import { executeTool, toolDefinitions } from "./lib/tool" import { Cause, Deferred, Effect, Exit, Fiber, Layer, Option, Schema, SchemaGetter, SchemaIssue, Scope } from "effect" import { testEffect } from "./lib/effect" const bounds: ToolOutputStore.BoundInput[] = [] const retentionFailure = new ToolOutputStore.StorageError({ operation: "write", cause: new Error("disk full") }) const outputStore = Layer.mock(ToolOutputStore.Service, { + limits: () => Effect.succeed({ maxLines: ToolOutputStore.MAX_LINES, maxBytes: ToolOutputStore.MAX_BYTES }), bound: (input) => { if (input.callID === "call-retention-failure") return Effect.fail(retentionFailure) return Effect.sync(() => bounds.push(input)).pipe( Effect.as( input.callID === "call-bounded" ? { - output: { structured: {}, content: [{ type: "text" as const, text: "bounded reference" }] }, + content: [{ type: "text" as const, text: "bounded reference" }], outputPaths: ["/managed/generic"], } - : { output: input.output, outputPaths: [] }, + : { content: input.content, outputPaths: [] }, ), ) }, @@ -63,24 +64,20 @@ const call = (name: string, id = `call-${name}`): ToolRegistry.ExecuteInput => ( call: { type: "tool-call", id, name, input: { text: name } }, }) -const make = (permission?: string) => { - const tool = Tool.make({ +const make = () => + Tool.make({ description: "Echo text", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), - execute: ({ text }) => Effect.succeed({ text }), - toModelOutput: ({ output }) => [{ type: "text", text: output.text }], + execute: ({ text }) => Effect.succeed({ output: { text }, content: text }), }) - return permission ? Tool.withPermission(tool, permission) : tool -} const constant = (text: string) => Tool.make({ description: "Return text", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), - execute: () => Effect.succeed({ text }), - toModelOutput: ({ output }) => [{ type: "text" as const, text: output.text }], + execute: () => Effect.succeed({ output: { text }, content: text }), }) describe("ToolRegistry", () => { @@ -91,7 +88,21 @@ describe("ToolRegistry", () => { expect(error).toBeInstanceOf(Tool.RegistrationError) expect(error.message).toBe('Invalid tool namespace: "slack..admin"') - expect((yield* service.materialize()).definitions).toEqual([]) + expect((yield* service.snapshot()).definitions).toEqual([]) + }), + ) + + it.effect("rejects invalid and colliding normalized names", () => + Effect.gen(function* () { + const service = yield* ToolRegistry.Service + const invalid = yield* service.register({ "123": make() }, { codemode: false }).pipe(Effect.flip) + expect(invalid.message).toBe("Invalid tool name: 123") + + const collision = yield* service + .register({ "echo.tool": make(), echo_tool: make() }, { codemode: false }) + .pipe(Effect.flip) + expect(collision.message).toBe("Duplicate normalized tool name: echo_tool") + expect((yield* service.snapshot()).definitions).toEqual([]) }), ) @@ -106,19 +117,15 @@ describe("ToolRegistry", () => { .pipe(Effect.flip) expect(error).toBeInstanceOf(Tool.RegistrationError) - expect((yield* service.materialize()).definitions).toEqual([]) + expect((yield* service.snapshot()).definitions).toEqual([]) }), ) it.effect("filters disabled tools with edit aliases and ordered wildcard precedence", () => Effect.gen(function* () { const service = yield* ToolRegistry.Service - yield* service.register({ - question: make(), - bash: make(), - edit: make("edit"), - write: make("edit"), - }, { codemode: false }) + yield* service.register({ question: make(), bash: make() }, { codemode: false }) + yield* service.register({ edit: make(), write: make() }, { codemode: false, permission: "edit" }) const names = (permissions: PermissionV2.Ruleset) => toolDefinitions(service, permissions).pipe(Effect.map((definitions) => definitions.map((tool) => tool.name))) @@ -139,18 +146,15 @@ describe("ToolRegistry", () => { }), ) - it.effect("keeps permission decoration isolated between registrations", () => + it.effect("keeps permission options isolated between registrations", () => Effect.gen(function* () { const service = yield* ToolRegistry.Service const shared = make() yield* service.register({ first: shared }, { codemode: false }) - yield* service.register({ second: Tool.withPermission(shared, "edit") }, { codemode: false }) - Tool.withPermission(shared, "question") + yield* service.register({ second: shared }, { codemode: false, permission: "edit" }) expect( - (yield* toolDefinitions(service, [{ action: "edit", resource: "*", effect: "deny" }])).map( - (definition) => definition.name, - ), + (yield* toolDefinitions(service, [{ action: "edit", resource: "*", effect: "deny" }])).map((tool) => tool.name), ).toEqual(["first"]) }), ) @@ -191,41 +195,47 @@ describe("ToolRegistry", () => { it.effect("returns model errors without swallowing interruption or defects", () => Effect.gen(function* () { const service = yield* ToolRegistry.Service - yield* service.register({ - failed: Tool.make({ - description: "Failed", - input: Schema.Struct({}), - output: Schema.Struct({ ok: Schema.Boolean }), - execute: () => Effect.fail(new Tool.Failure({ message: "Denied" })), - }), - }, { codemode: false }) + yield* service.register( + { + failed: Tool.make({ + description: "Failed", + input: Schema.Struct({}), + output: Schema.Struct({ ok: Schema.Boolean }), + execute: () => Effect.fail(new Tool.Failure({ message: "Denied" })), + }), + }, + { codemode: false }, + ) expect( yield* executeTool(service, { sessionID, ...identity, call: { type: "tool-call", id: "failed", name: "failed", input: {} }, }), - ).toEqual({ type: "error", value: "Denied" }) + ).toEqual({ status: "error", error: { type: "tool.execution", message: "Denied" } }) expect( yield* executeTool(service, { sessionID, ...identity, call: { type: "tool-call", id: "missing", name: "missing", input: {} }, }), - ).toEqual({ type: "error", value: "Unknown tool: missing" }) + ).toEqual({ status: "error", error: { type: "tool.unknown", message: "Unknown tool: missing" } }) - yield* service.register({ - defect: Tool.make({ - description: "Defect", - input: Schema.Struct({}), - output: Schema.Struct({}), - execute: () => Effect.die("unexpected executor defect"), - }), - }, { codemode: false }) + yield* service.register( + { + defect: Tool.make({ + description: "Defect", + input: Schema.Struct({}), + output: Schema.Struct({}), + execute: () => Effect.die("unexpected executor defect"), + }), + }, + { codemode: false }, + ) expect( - yield* service.materialize().pipe( - Effect.flatMap((materialized) => - materialized.settle({ + yield* service.snapshot().pipe( + Effect.flatMap((toolSet) => + toolSet.execute({ sessionID, ...identity, call: { type: "tool-call", id: "defect", name: "defect", input: {} }, @@ -237,12 +247,12 @@ describe("ToolRegistry", () => { }), ) - it.effect("propagates retention failures through settlement", () => + it.effect("propagates retention failures through execution", () => Effect.gen(function* () { const service = yield* ToolRegistry.Service yield* service.register({ echo: make() }, { codemode: false }) - const materialized = yield* service.materialize() - const exit = yield* materialized.settle(call("echo", "call-retention-failure")).pipe(Effect.exit) + const toolSet = yield* service.snapshot() + const exit = yield* toolSet.execute(call("echo", "call-retention-failure")).pipe(Effect.exit) expect(Exit.isFailure(exit)).toBe(true) if (Exit.isFailure(exit)) expect(Option.getOrUndefined(Cause.findErrorOption(exit.cause))).toBe(retentionFailure) @@ -250,79 +260,88 @@ describe("ToolRegistry", () => { }), ) - it.effect("exposes settlement only through materialization", () => + it.effect("exposes execution only through a snapshot", () => Effect.gen(function* () { const service = yield* ToolRegistry.Service expect("definitions" in service).toBe(false) expect("execute" in service).toBe(false) expect("settle" in service).toBe(false) - expect(typeof service.materialize).toBe("function") + expect(typeof service.snapshot).toBe("function") }), ) - it.effect("passes complete invocation identity to the canonical handler", () => + it.effect("passes complete call identity to tool execution", () => Effect.gen(function* () { const service = yield* ToolRegistry.Service const contexts: Tool.Context[] = [] - yield* service.register({ - context: Tool.make({ - description: "Context", - input: Schema.Struct({}), - output: Schema.Struct({ ok: Schema.Boolean }), - execute: (_, context) => Effect.sync(() => contexts.push(context)).pipe(Effect.as({ ok: true })), - }), - }, { codemode: false }) + yield* service.register( + { + context: Tool.make({ + description: "Context", + input: Schema.Struct({}), + output: Schema.Struct({ ok: Schema.Boolean }), + execute: (_, context) => + Effect.sync(() => contexts.push(context)).pipe(Effect.as({ output: { ok: true } })), + }), + }, + { codemode: false }, + ) yield* executeTool(service, { sessionID, ...identity, call: { type: "tool-call", id: "call-context", name: "context", input: {} }, }) - expect(contexts).toEqual([ - { sessionID, ...identity, callID: "call-context", progress: expect.any(Function) }, - ]) + expect(contexts).toEqual([{ sessionID, ...identity, callID: "call-context", progress: expect.any(Function) }]) }), ) - it.effect("encodes output and applies generic settlement bounding", () => + it.effect("encodes output and applies generic execution bounding", () => Effect.gen(function* () { bounds.length = 0 const service = yield* ToolRegistry.Service yield* service.register({ bounded: make() }, { codemode: false }) expect( - yield* settleTool(service, { + yield* executeTool(service, { sessionID, ...identity, call: { type: "tool-call", id: "call-bounded", name: "bounded", input: { text: "complete" } }, }), ).toEqual({ - result: { type: "text", value: "bounded reference" }, - output: { structured: {}, content: [{ type: "text", text: "bounded reference" }] }, + status: "completed", + output: { text: "complete" }, + content: [{ type: "text", text: "bounded reference" }], outputPaths: ["/managed/generic"], }) expect(bounds).toHaveLength(1) }), ) - it.effect("normalizes image tool output at settlement and drops unresizable images", () => + it.effect("normalizes image tool output at execution and drops unresizable images", () => Effect.gen(function* () { const service = yield* ToolRegistry.Service - yield* service.register({ - snapshot: Tool.make({ - description: "Return images", - input: Schema.Struct({ text: Schema.String }), - output: Schema.Struct({ text: Schema.String }), - execute: ({ text }) => Effect.succeed({ text }), - toModelOutput: ({ output }) => [ - { type: "file", data: "aW1hZ2U=", mime: "image/png", name: "frame.png" }, - { type: "file", data: "aW1hZ2U=", mime: "image/png", name: "too-large.png" }, - { type: "file", data: "aW1hZ2U=", mime: "image/png", name: "corrupt.png" }, - { type: "text", text: output.text }, - ], - }), - }, { codemode: false }) + yield* service.register( + { + snapshot: Tool.make({ + description: "Return images", + input: Schema.Struct({ text: Schema.String }), + output: Schema.Struct({ text: Schema.String }), + execute: ({ text }) => + Effect.succeed({ + output: { text }, + content: [ + { type: "file", data: "aW1hZ2U=", mime: "image/png", name: "frame.png" }, + { type: "file", data: "aW1hZ2U=", mime: "image/png", name: "too-large.png" }, + { type: "file", data: "aW1hZ2U=", mime: "image/png", name: "corrupt.png" }, + { type: "text", text }, + ], + }), + }), + }, + { codemode: false }, + ) - const settlement = yield* settleTool(service, call("snapshot")) - expect(settlement.output?.content).toEqual([ + const execution = yield* executeTool(service, call("snapshot")) + expect(execution.content).toEqual([ { type: "file", uri: "data:image/jpeg;base64,bm9ybWFsaXplZA==", mime: "image/jpeg", name: "frame.png" }, { type: "text", text: "snapshot" }, { type: "text", text: "[1 image omitted: could not be decoded.]" }, @@ -331,44 +350,31 @@ describe("ToolRegistry", () => { }), ) - it.effect("normalizes image progress content before it is published", () => + it.effect("publishes progress metadata unchanged", () => Effect.gen(function* () { const service = yield* ToolRegistry.Service - yield* service.register({ - progressive: Tool.make({ - description: "Emit image progress", - input: Schema.Struct({ text: Schema.String }), - output: Schema.Struct({ text: Schema.String }), - execute: ({ text }, context) => - context - .progress({ - structured: { stage: "capture" }, - content: [ - { type: "file", data: "aW1hZ2U=", mime: "image/png", name: "frame.png" }, - { type: "file", data: "aW1hZ2U=", mime: "image/png", name: "too-large.png" }, - ], - }) - .pipe(Effect.as({ text })), - }), - }, { codemode: false }) + yield* service.register( + { + progressive: Tool.make({ + description: "Emit image progress", + input: Schema.Struct({ text: Schema.String }), + output: Schema.Struct({ text: Schema.String }), + execute: ({ text }, context) => + context.progress({ stage: "capture" }).pipe(Effect.as({ output: { text } })), + }), + }, + { codemode: false }, + ) const updates: ToolRegistry.Progress[] = [] - yield* settleTool(service, { + yield* executeTool(service, { ...call("progressive"), progress: (update) => Effect.sync(() => { updates.push(update) }), }) - expect(updates).toEqual([ - { - structured: { stage: "capture" }, - content: [ - { type: "file", uri: "data:image/jpeg;base64,bm9ybWFsaXplZA==", mime: "image/jpeg", name: "frame.png" }, - { type: "text", text: "[1 image omitted: could not be resized below the image size limit.]" }, - ], - }, - ]) + expect(updates).toEqual([{ stage: "capture" }]) }), ) @@ -382,23 +388,31 @@ describe("ToolRegistry", () => { encode: SchemaGetter.transform((value) => value === "yes"), }), ) - yield* service.register({ - transformed: Tool.make({ - description: "Transform values", - input: Schema.Struct({ value: Transformed }), - output: Schema.Struct({ value: Transformed }), - execute: ({ value }) => Effect.sync(() => executed.push(value)).pipe(Effect.as({ value })), - toModelOutput: ({ output }) => [{ type: "text", text: String(output.value) }], - }), - }, { codemode: false }) + yield* service.register( + { + transformed: Tool.make({ + description: "Transform values", + input: Schema.Struct({ value: Transformed }), + output: Schema.Struct({ value: Transformed }), + execute: ({ value }) => + Effect.sync(() => executed.push(value)).pipe(Effect.as({ output: { value }, content: String(value) })), + }), + }, + { codemode: false }, + ) + // Canonical content observes the decoded domain value; Code Mode observes the encoded value. expect( yield* executeTool(service, { sessionID, ...identity, call: { type: "tool-call", id: "transformed", name: "transformed", input: { value: true } }, }), - ).toEqual({ type: "text", value: "true" }) + ).toEqual({ + status: "completed", + output: { value: true }, + content: [{ type: "text", text: "yes" }], + }) expect(executed).toEqual(["yes"]) expect( yield* executeTool(service, { @@ -406,35 +420,44 @@ describe("ToolRegistry", () => { ...identity, call: { type: "tool-call", id: "invalid-input", name: "transformed", input: { value: "yes" } }, }), - ).toMatchObject({ type: "error", value: expect.stringContaining("Invalid tool input") }) + ).toMatchObject({ + status: "error", + error: { type: "tool.execution", message: expect.stringContaining("Invalid tool input") }, + }) expect(executed).toEqual(["yes"]) - yield* service.register({ - invalid_output: Tool.make({ - description: "Return invalid output", - input: Schema.Struct({}), - output: Schema.Struct({ - value: Schema.Boolean.pipe( - Schema.decodeTo(Schema.String, { - decode: SchemaGetter.transform((value) => String(value)), - encode: SchemaGetter.transformOrFail((value) => - value === "valid" - ? Effect.succeed(true) - : Effect.fail(new SchemaIssue.InvalidValue(Option.some(value), { message: "invalid output" })), - ), - }), - ), + yield* service.register( + { + invalid_output: Tool.make({ + description: "Return invalid output", + input: Schema.Struct({}), + output: Schema.Struct({ + value: Schema.Boolean.pipe( + Schema.decodeTo(Schema.String, { + decode: SchemaGetter.transform((value) => String(value)), + encode: SchemaGetter.transformOrFail((value) => + value === "valid" + ? Effect.succeed(true) + : Effect.fail(new SchemaIssue.InvalidValue(Option.some(value), { message: "invalid output" })), + ), + }), + ), + }), + execute: () => Effect.succeed({ output: { value: "invalid" } }), }), - execute: () => Effect.succeed({ value: "invalid" }), - }), - }, { codemode: false }) + }, + { codemode: false }, + ) expect( yield* executeTool(service, { sessionID, ...identity, call: { type: "tool-call", id: "invalid-output", name: "invalid_output", input: {} }, }), - ).toMatchObject({ type: "error", value: expect.stringContaining("invalid value for its output schema") }) + ).toMatchObject({ + status: "error", + error: { type: "tool.execution", message: expect.stringContaining("invalid value for its output schema") }, + }) }), ) @@ -443,12 +466,12 @@ describe("ToolRegistry", () => { const service = yield* ToolRegistry.Service const scope = yield* Scope.make() yield* service.register({ echo: constant("advertised") }, { codemode: false }).pipe(Scope.provide(scope)) - const request = yield* service.materialize() + const request = yield* service.snapshot() yield* Scope.close(scope, Exit.void) yield* service.register({ echo: constant("replacement") }, { codemode: false }) - expect((yield* request.settle(call("echo"))).result).toEqual({ type: "text", value: "advertised" }) - expect(yield* executeTool(service, call("echo"))).toEqual({ type: "text", value: "replacement" }) + expect((yield* request.execute(call("echo"))).content).toEqual([{ type: "text", text: "advertised" }]) + expect((yield* executeTool(service, call("echo"))).content).toEqual([{ type: "text", text: "replacement" }]) }), ) @@ -459,9 +482,9 @@ describe("ToolRegistry", () => { const overlay = yield* Scope.make() yield* service.register({ echo: constant("overlay") }, { codemode: false }).pipe(Scope.provide(overlay)) - expect(yield* executeTool(service, call("echo"))).toEqual({ type: "text", value: "overlay" }) + expect((yield* executeTool(service, call("echo"))).content).toEqual([{ type: "text", text: "overlay" }]) yield* Scope.close(overlay, Exit.void) - expect(yield* executeTool(service, call("echo"))).toEqual({ type: "text", value: "base" }) + expect((yield* executeTool(service, call("echo"))).content).toEqual([{ type: "text", text: "base" }]) }), ) @@ -476,12 +499,13 @@ describe("ToolRegistry", () => { description: "Echo text", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), - execute: ({ text }) => Effect.sync(() => executed.push(`old:${text}`)).pipe(Effect.as({ text })), + execute: ({ text }) => + Effect.sync(() => executed.push(`old:${text}`)).pipe(Effect.as({ output: { text } })), }), }) .pipe(Scope.provide(scope)) - const materialized = yield* service.materialize() - const execute = materialized.definitions.find((tool) => tool.name === "execute") + const toolSet = yield* service.snapshot() + const execute = toolSet.definitions.find((tool) => tool.name === "execute") expect(execute?.description).toContain("confined Code Mode runtime") expect(execute?.description).not.toContain("Echo text") yield* Scope.close(scope, Exit.void) @@ -490,11 +514,11 @@ describe("ToolRegistry", () => { description: "Echo text", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), - execute: ({ text }) => Effect.sync(() => executed.push(`new:${text}`)).pipe(Effect.as({ text })), + execute: ({ text }) => Effect.sync(() => executed.push(`new:${text}`)).pipe(Effect.as({ output: { text } })), }), }) - const settlement = yield* materialized.settle({ + const execution = yield* toolSet.execute({ ...call("execute"), call: { type: "tool-call", @@ -504,7 +528,7 @@ describe("ToolRegistry", () => { }, }) - expect(settlement.result).toMatchObject({ type: "text" }) + expect(execution).toMatchObject({ status: "completed", content: [{ type: "text" }] }) expect(executed).toEqual(["old:request"]) }), ) diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 86e9d7c576db..77046d6d9fff 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -52,6 +52,7 @@ import { AgentV2 } from "@opencode-ai/core/agent" import { Config } from "@opencode-ai/core/config" import { ConfigCompaction } from "@opencode-ai/core/config/compaction" import { Tool } from "@opencode-ai/core/tool/tool" +import { ToolHooks } from "@opencode-ai/core/tool/hooks" import { InstructionStateTable, SessionPendingTable, @@ -238,43 +239,45 @@ const permission = Layer.succeed( ) const echo = Layer.effectDiscard( ToolRegistry.Service.use((registry) => - registry.register({ - echo: Tool.make({ - description: "Echo text", - input: Schema.Struct({ text: Schema.String }), - output: Schema.Struct({ text: Schema.String }), - toModelOutput: ({ output }) => [{ type: "text", text: output.text }], - execute: ({ text }, context) => - Effect.gen(function* () { - authorizations.push(context) - executions.push(text) - activeToolExecutions++ - maxActiveToolExecutions = Math.max(maxActiveToolExecutions, activeToolExecutions) - if (activeToolExecutions === toolExecutionsReady && toolExecutionsStarted) { - yield* Deferred.succeed(toolExecutionsStarted, undefined) - } - if (toolExecutionGate) yield* Deferred.await(toolExecutionGate) - return { text } - }).pipe(Effect.ensuring(Effect.sync(() => activeToolExecutions--))), - }), - defect: Tool.make({ - description: "Fail unexpectedly", - input: Schema.Struct({}), - output: Schema.Struct({}), - execute: () => - (toolExecutionGate ? Deferred.await(toolExecutionGate) : Effect.void).pipe( - Effect.andThen(Effect.die("unexpected tool defect")), - ), - }), - // BigInt output with no model content forces ToolOutputStore.bound onto its - // JSON.stringify encode path, which fails with a typed StorageError. - storefail: Tool.make({ - description: "Produce output that cannot be persisted", - input: Schema.Struct({}), - output: Schema.Any, - execute: () => Effect.succeed({ big: 1n }), - }), - }, { codemode: false }), + registry.register( + { + echo: Tool.make({ + description: "Echo text", + input: Schema.Struct({ text: Schema.String }), + output: Schema.Struct({ text: Schema.String }), + execute: ({ text }, context) => + Effect.gen(function* () { + authorizations.push(context) + executions.push(text) + activeToolExecutions++ + maxActiveToolExecutions = Math.max(maxActiveToolExecutions, activeToolExecutions) + if (activeToolExecutions === toolExecutionsReady && toolExecutionsStarted) { + yield* Deferred.succeed(toolExecutionsStarted, undefined) + } + if (toolExecutionGate) yield* Deferred.await(toolExecutionGate) + return { output: { text }, content: text } + }).pipe(Effect.ensuring(Effect.sync(() => activeToolExecutions--))), + }), + defect: Tool.make({ + description: "Fail unexpectedly", + input: Schema.Struct({}), + output: Schema.Struct({}), + execute: () => + (toolExecutionGate ? Deferred.await(toolExecutionGate) : Effect.void).pipe( + Effect.andThen(Effect.die("unexpected tool defect")), + ), + }), + // The wrapped ToolOutputStore below fails bound for this call ID with a + // typed StorageError, exercising the infrastructure failure channel. + storefail: Tool.make({ + description: "Produce output that cannot be persisted", + input: Schema.Struct({}), + output: Schema.Struct({}), + execute: () => Effect.succeed({ output: {} }), + }), + }, + { codemode: false }, + ), ), ) const echoNode = makeLocationNode({ name: "test/session-runner-tools", layer: echo, deps: [ToolRegistry.node] }) @@ -379,6 +382,15 @@ const promptCatalog = Layer.mock(Catalog.Service, { small: () => Effect.succeed(undefined), }, }) +// Pass-through bounding that fails "call-storefail" with a typed StorageError so +// runner tests can exercise the infrastructure failure channel deterministically. +const toolOutputStore = Layer.mock(ToolOutputStore.Service, { + limits: () => Effect.succeed({ maxLines: ToolOutputStore.MAX_LINES, maxBytes: ToolOutputStore.MAX_BYTES }), + bound: (input) => + input.callID === "call-storefail" + ? Effect.fail(new ToolOutputStore.StorageError({ operation: "write", cause: new Error("disk full") })) + : Effect.succeed({ content: input.content, outputPaths: [] }), +}) const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [ [Snapshot.node, Snapshot.noopLayer], [LayerNodePlatform.llmClient, client], @@ -391,7 +403,7 @@ const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [ [PermissionV2.node, permission], [Config.node, config], [McpInstructions.node, mcpInstructions], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + [ToolOutputStore.node, toolOutputStore], [PluginSupervisor.node, pluginSupervisor], ]) const execution = Layer.effect( @@ -422,6 +434,7 @@ const it = testEffect( Catalog.node, ToolRegistry.node, ToolRegistry.toolsNode, + ToolHooks.node, PluginHooks.node, echoNode, SessionRunnerModel.node, @@ -449,7 +462,7 @@ const it = testEffect( [Snapshot.node, Snapshot.noopLayer], [SessionExecution.node, execution], [Config.node, config], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + [ToolOutputStore.node, toolOutputStore], [PluginSupervisor.node, pluginSupervisor], ], ), @@ -586,8 +599,8 @@ const recordedStepSettlementEvents = (id: SessionV2.ID, assistantMessageID: Sess const settlementTypes = new Set([ "session.step.started.1", "session.tool.called.1", - "session.tool.success.1", - "session.tool.failed.1", + "session.tool.success.2", + "session.tool.failed.2", "session.step.ended.1", "session.step.failed.1", ]) @@ -827,12 +840,26 @@ describe("SessionRunnerLLM", () => { yield* session.resume(sessionID) - expect(requests).toHaveLength(1) + // A hook-removed call fails independently and continues while step allowance remains. + expect(requests).toHaveLength(2) expect(requests[0]?.system.map((part) => part.text)).toEqual(["Hooked system"]) expect(requests[0]?.messages).toEqual([Message.user("Hooked message")]) expect(requests[0]?.tools.map((tool) => tool.name)).not.toContain("echo") expect(requests[0]?.tools.map((tool) => tool.name)).not.toContain("unregistered") expect(executions).toEqual([]) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Original message" }, + { + type: "assistant", + content: [ + { + type: "tool", + id: "call-removed", + state: { status: "error", error: { type: "tool.unknown" } }, + }, + ], + }, + ]) }), ) @@ -841,19 +868,22 @@ describe("SessionRunnerLLM", () => { const session = yield* setup const registry = yield* ToolRegistry.Service const contexts: Tool.Context[] = [] - yield* registry.register({ - location_context: Tool.make({ - description: "Read application context", - input: Schema.Struct({ query: Schema.String }), - output: Schema.Struct({ answer: Schema.String }), - execute: ({ query }, context) => - Effect.gen(function* () { - contexts.push(context) - yield* context.progress({ structured: { phase: "reading" } }) - return { answer: query.toUpperCase() } - }), - }), - }, { codemode: false }) + yield* registry.register( + { + location_context: Tool.make({ + description: "Read application context", + input: Schema.Struct({ query: Schema.String }), + output: Schema.Struct({ answer: Schema.String }), + execute: ({ query }, context) => + Effect.gen(function* () { + contexts.push(context) + yield* context.progress({ phase: "reading" }) + return { output: { answer: query.toUpperCase() } } + }), + }), + }, + { codemode: false }, + ) yield* admit(session, "Use application context") responses = [reply.tool("call-location", "location_context", { query: "hello" }), []] const events = yield* EventV2.Service @@ -876,7 +906,7 @@ describe("SessionRunnerLLM", () => { progress: expect.any(Function), }, ]) - expect(Array.from(yield* Fiber.join(progressFiber))[0]?.data.structured).toEqual({ phase: "reading" }) + expect(Array.from(yield* Fiber.join(progressFiber))[0]?.data.metadata).toEqual({ phase: "reading" }) expect(yield* session.context(sessionID)).toMatchObject([ { type: "user", text: "Use application context" }, { @@ -885,7 +915,7 @@ describe("SessionRunnerLLM", () => { { type: "tool", id: "call-location", - state: { status: "completed", structured: { answer: "HELLO" } }, + state: { status: "completed", content: [{ type: "text", text: '{"answer":"HELLO"}' }] }, }, ], }, @@ -893,25 +923,29 @@ describe("SessionRunnerLLM", () => { }), ) - it.effect("persists the latest partial snapshot when a tool fails", () => + it.effect("prefers failure outcome metadata over retained progress", () => Effect.gen(function* () { const session = yield* setup const registry = yield* ToolRegistry.Service - yield* registry.register({ - failing_progress: Tool.make({ - description: "Report progress and fail", - input: Schema.Struct({}), - output: Schema.Struct({}), - execute: (_, context) => - Effect.gen(function* () { - yield* context.progress({ - structured: { phase: "running" }, - content: [{ type: "text", text: "before failure" }], - }) - return yield* new ToolFailure({ message: "failed after progress" }) - }), - }), - }, { codemode: false }) + const hooks = yield* ToolHooks.Service + yield* hooks.hook.after((event) => { + if (event.status === "error") event.metadata = { phase: "failed" } + }) + yield* registry.register( + { + failing_progress: Tool.make({ + description: "Report progress and fail", + input: Schema.Struct({}), + output: Schema.Struct({}), + execute: (_, context) => + Effect.gen(function* () { + yield* context.progress({ phase: "running" }) + return yield* new ToolFailure({ message: "failed after progress" }) + }), + }), + }, + { codemode: false }, + ) yield* admit(session, "Run failing progress") responses = [reply.tool("call-failing-progress", "failing_progress", {}), reply.stop()] @@ -927,8 +961,7 @@ describe("SessionRunnerLLM", () => { id: "call-failing-progress", state: { status: "error", - structured: { phase: "running" }, - content: [{ type: "text", text: "before failure" }], + metadata: { phase: "failed" }, error: { message: "failed after progress" }, }, }, @@ -946,14 +979,20 @@ describe("SessionRunnerLLM", () => { const scope = yield* Scope.make() const executions: string[] = [] yield* registry - .register({ - reloaded: Tool.make({ - description: "Record the advertised tool", - input: Schema.Struct({}), - output: Schema.Struct({ value: Schema.String }), - execute: () => Effect.sync(() => executions.push("advertised")).pipe(Effect.as({ value: "advertised" })), - }), - }, { codemode: false }) + .register( + { + reloaded: Tool.make({ + description: "Record the advertised tool", + input: Schema.Struct({}), + output: Schema.Struct({ value: Schema.String }), + execute: () => + Effect.sync(() => executions.push("advertised")).pipe( + Effect.as({ output: { value: "advertised" } }), + ), + }), + }, + { codemode: false }, + ) .pipe(Scope.provide(scope)) yield* admit(session, "Use the reloaded tool") responses = [ @@ -971,14 +1010,20 @@ describe("SessionRunnerLLM", () => { const run = yield* session.resume(sessionID).pipe(Effect.forkChild) yield* Deferred.await(streamStarted) yield* Scope.close(scope, Exit.void) - yield* registry.register({ - reloaded: Tool.make({ - description: "Record the replacement tool", - input: Schema.Struct({}), - output: Schema.Struct({ value: Schema.String }), - execute: () => Effect.sync(() => executions.push("replacement")).pipe(Effect.as({ value: "replacement" })), - }), - }, { codemode: false }) + yield* registry.register( + { + reloaded: Tool.make({ + description: "Record the replacement tool", + input: Schema.Struct({}), + output: Schema.Struct({ value: Schema.String }), + execute: () => + Effect.sync(() => executions.push("replacement")).pipe( + Effect.as({ output: { value: "replacement" } }), + ), + }), + }, + { codemode: false }, + ) yield* Deferred.succeed(streamGate, undefined) yield* Fiber.join(run) @@ -991,7 +1036,7 @@ describe("SessionRunnerLLM", () => { { type: "tool", id: "call-reloaded", - state: { status: "completed", structured: { value: "advertised" } }, + state: { status: "completed", content: [{ type: "text", text: '{"value":"advertised"}' }] }, }, ], }, @@ -2377,7 +2422,6 @@ describe("SessionRunnerLLM", () => { state: { status: "completed", input: { query: "hello" }, - structured: {}, content: [ { type: "text", text: "Hello" }, { type: "file", mime: "image/png", uri: "data:image/png;base64,aGVsbG8=", name: "hello.png" }, @@ -2417,7 +2461,6 @@ describe("SessionRunnerLLM", () => { state: { status: "completed", input: { text: "hello" }, - structured: { text: "hello" }, content: [{ type: "text", text: "hello" }], }, }, @@ -2429,7 +2472,7 @@ describe("SessionRunnerLLM", () => { expect((yield* recordedStepSettlementEvents(sessionID, assistant.id)).map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", - "session.tool.success.1", + "session.tool.success.2", "session.step.ended.1", ]) }), @@ -2581,7 +2624,8 @@ describe("SessionRunnerLLM", () => { type: "tool-result", id: "hosted-search", name: "web_search", - result: { type: "json", value: [{ title: "Effect" }] }, + // The generic replay result derives from canonical stored content. + result: { type: "text", value: '[{"title":"Effect"}]' }, providerExecuted: true, providerMetadata: { openai: { blockType: "web_search_tool_result" } }, }, @@ -2667,7 +2711,7 @@ describe("SessionRunnerLLM", () => { { type: "tool", id: "tool_0", - state: { status: "completed", structured: { text: "first" }, content: [{ type: "text", text: "first" }] }, + state: { status: "completed", content: [{ type: "text", text: "first" }] }, }, ], }, @@ -2677,11 +2721,7 @@ describe("SessionRunnerLLM", () => { { type: "tool", id: "tool_0", - state: { - status: "completed", - structured: { text: "second" }, - content: [{ type: "text", text: "second" }], - }, + state: { status: "completed", content: [{ type: "text", text: "second" }] }, }, ], }, @@ -2697,7 +2737,7 @@ describe("SessionRunnerLLM", () => { { type: "tool", id: "tool_0", - state: { status: "completed", structured: { text: "first" }, content: [{ type: "text", text: "first" }] }, + state: { status: "completed", content: [{ type: "text", text: "first" }] }, }, ], }, @@ -2707,11 +2747,7 @@ describe("SessionRunnerLLM", () => { { type: "tool", id: "tool_0", - state: { - status: "completed", - structured: { text: "second" }, - content: [{ type: "text", text: "second" }], - }, + state: { status: "completed", content: [{ type: "text", text: "second" }] }, }, ], }, @@ -3404,7 +3440,7 @@ describe("SessionRunnerLLM", () => { expect((yield* recordedStepSettlementEvents(sessionID, assistant.id)).map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", - "session.tool.failed.1", + "session.tool.failed.2", "session.step.ended.1", ]) }), @@ -3414,17 +3450,20 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { const session = yield* setup const registry = yield* ToolRegistry.Service - yield* registry.register({ - blocked: Tool.make({ - description: "Fail because policy blocked execution", - input: Schema.Struct({}), - output: Schema.Struct({}), - execute: () => - Effect.fail(new PermissionV2.BlockedError({ rules: [], permission: "blocked", resources: ["*"] })).pipe( - Effect.mapError(() => new Tool.Failure({ message: "Permission blocked" })), - ), - }), - }, { codemode: false }) + yield* registry.register( + { + blocked: Tool.make({ + description: "Fail because policy blocked execution", + input: Schema.Struct({}), + output: Schema.Struct({}), + execute: () => + Effect.fail(new PermissionV2.BlockedError({ rules: [], permission: "blocked", resources: ["*"] })).pipe( + Effect.mapError(() => new Tool.Failure({ message: "Permission blocked" })), + ), + }), + }, + { codemode: false }, + ) yield* admit(session, "Call blocked") responses = [reply.tool("call-blocked", "blocked", {}), reply.stop()] @@ -3449,14 +3488,17 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { const session = yield* setup const registry = yield* ToolRegistry.Service - yield* registry.register({ - declined: Tool.make({ - description: "Fail because the user declined approval", - input: Schema.Struct({}), - output: Schema.Struct({}), - execute: () => Effect.die(new PermissionV2.DeclinedError()), - }), - }, { codemode: false }) + yield* registry.register( + { + declined: Tool.make({ + description: "Fail because the user declined approval", + input: Schema.Struct({}), + output: Schema.Struct({}), + execute: () => Effect.die(new PermissionV2.DeclinedError()), + }), + }, + { codemode: false }, + ) yield* admit(session, "Call declined") response = reply.tool("call-declined", "declined", {}) @@ -3486,17 +3528,20 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { const session = yield* setup const registry = yield* ToolRegistry.Service - yield* registry.register({ - corrected: Tool.make({ - description: "Fail with user correction feedback", - input: Schema.Struct({}), - output: Schema.Struct({}), - execute: () => - Effect.fail(new PermissionV2.CorrectedError({ feedback: "Use another tool" })).pipe( - Effect.mapError(() => new Tool.Failure({ message: "Use another tool" })), - ), - }), - }, { codemode: false }) + yield* registry.register( + { + corrected: Tool.make({ + description: "Fail with user correction feedback", + input: Schema.Struct({}), + output: Schema.Struct({}), + execute: () => + Effect.fail(new PermissionV2.CorrectedError({ feedback: "Use another tool" })).pipe( + Effect.mapError(() => new Tool.Failure({ message: "Use another tool" })), + ), + }), + }, + { codemode: false }, + ) yield* admit(session, "Call corrected") responses = [reply.tool("call-corrected", "corrected", {}), reply.stop()] @@ -3540,13 +3585,13 @@ describe("SessionRunnerLLM", () => { status: "error", error: { type: "unknown", - message: expect.stringContaining("Failed to encode tool output"), + message: expect.stringContaining("Failed to write tool output"), }, }, }, ], finish: "error", - error: { type: "unknown", message: expect.stringContaining("Failed to encode tool output") }, + error: { type: "unknown", message: expect.stringContaining("Failed to write tool output") }, }, ]) }), @@ -3594,14 +3639,17 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { const session = yield* setup const registry = yield* ToolRegistry.Service - yield* registry.register({ - question: Tool.make({ - description: "Ask the user", - input: Schema.Struct({}), - output: Schema.Struct({}), - execute: () => Effect.die(new QuestionTool.CancelledError()), - }), - }, { codemode: false }) + yield* registry.register( + { + question: Tool.make({ + description: "Ask the user", + input: Schema.Struct({}), + output: Schema.Struct({}), + execute: () => Effect.die(new QuestionTool.CancelledError()), + }), + }, + { codemode: false }, + ) yield* admit(session, "Ask then stop") responses = [reply.tool("call-question", "question", {}), []] @@ -3655,7 +3703,11 @@ describe("SessionRunnerLLM", () => { { type: "assistant", content: [ - { type: "tool", id: "call-before-failure", state: { status: "completed", structured: { text: "settle" } } }, + { + type: "tool", + id: "call-before-failure", + state: { status: "completed", content: [{ type: "text", text: "settle" }] }, + }, ], }, ]) @@ -3663,7 +3715,7 @@ describe("SessionRunnerLLM", () => { expect((yield* recordedStepSettlementEvents(sessionID, assistant.id)).map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", - "session.tool.success.1", + "session.tool.success.2", "session.step.failed.1", ]) }), @@ -3707,7 +3759,7 @@ describe("SessionRunnerLLM", () => { expect((yield* recordedStepSettlementEvents(sessionID, assistant.id)).map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", - "session.tool.failed.1", + "session.tool.failed.2", "session.step.failed.1", ]) @@ -3808,7 +3860,8 @@ describe("SessionRunnerLLM", () => { expect(requests).toHaveLength(2) expect(requests[0]?.toolChoice).toBeUndefined() expect(requests[1]?.toolChoice).toMatchObject({ type: "none" }) - expect(requests[1]?.tools).toEqual([]) + // Protocols with native "none" keep these definitions for prompt caching. + expect(requests[1]?.tools.map((tool) => tool.name)).toContain("echo") expect(requests[1]?.messages.at(-1)).toMatchObject({ role: "assistant", content: [{ type: "text", text: expect.stringContaining("MAXIMUM STEPS REACHED") }], @@ -3953,7 +4006,7 @@ describe("SessionRunnerLLM", () => { expect(events.map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", - "session.tool.success.1", + "session.tool.success.2", "session.step.failed.1", ]) expect( @@ -4146,7 +4199,8 @@ describe("SessionRunnerLLM", () => { content: [{ type: "text", text: expect.stringContaining("MAXIMUM STEPS REACHED") }], }) expect(requests[2]?.toolChoice).toMatchObject({ type: "none" }) - expect(requests[2]?.tools).toEqual([]) + // The final step keeps tool definitions to preserve provider prompt caching. + expect(requests[2]?.tools.map((tool) => tool.name)).toContain("echo") expect(requests[2]?.messages.at(-1)).toMatchObject({ role: "assistant", content: [{ type: "text", text: expect.stringContaining("MAXIMUM STEPS REACHED") }], @@ -4197,7 +4251,7 @@ describe("SessionRunnerLLM", () => { expect(yield* recordedStepSettlementEvents(sessionID, assistant.id)).toMatchObject([ { type: "session.step.started.1" }, { - type: "session.tool.failed.1", + type: "session.tool.failed.2", data: { callID: "call-malformed", error: { type: "provider.invalid-output", message: "Invalid JSON input for tool call echo" }, @@ -4292,7 +4346,7 @@ describe("SessionRunnerLLM", () => { expect(failed.error).toBeUndefined() expect((yield* recordedStepSettlementEvents(sessionID, failed.id)).map((event) => event.type)).toEqual([ "session.step.started.1", - "session.tool.failed.1", + "session.tool.failed.2", "session.step.ended.1", ]) const database = (yield* Database.Service).db @@ -4521,7 +4575,7 @@ describe("SessionRunnerLLM", () => { expect(requests).toHaveLength(2) expect(requests[0]?.toolChoice).toBeUndefined() expect(requests[1]?.toolChoice).toMatchObject({ type: "none" }) - expect((yield* recordedEventTypes(sessionID)).filter((type) => type === "session.tool.failed.1")).toHaveLength(2) + expect((yield* recordedEventTypes(sessionID)).filter((type) => type === "session.tool.failed.2")).toHaveLength(2) }), ) @@ -4553,7 +4607,7 @@ describe("SessionRunnerLLM", () => { expect((yield* recordedStepSettlementEvents(sessionID, assistant.id)).map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", - "session.tool.success.1", + "session.tool.success.2", "session.step.failed.1", ]) }), @@ -4585,7 +4639,7 @@ describe("SessionRunnerLLM", () => { expect((yield* recordedStepSettlementEvents(sessionID, assistant.id)).map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", - "session.tool.failed.1", + "session.tool.failed.2", "session.step.failed.1", ]) }), @@ -4609,7 +4663,7 @@ describe("SessionRunnerLLM", () => { expect(events.map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", - "session.tool.failed.1", + "session.tool.failed.2", "session.step.failed.1", ]) expect(events[2]?.data.error).toMatchObject({ type: "unknown", message: "unexpected tool defect" }) @@ -4646,7 +4700,7 @@ describe("SessionRunnerLLM", () => { expect(events.map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", - "session.tool.failed.1", + "session.tool.failed.2", "session.step.failed.1", ]) expect( @@ -4684,7 +4738,7 @@ describe("SessionRunnerLLM", () => { expect(events.map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", - "session.tool.failed.1", + "session.tool.failed.2", "session.step.ended.1", ]) expect( @@ -4721,8 +4775,8 @@ describe("SessionRunnerLLM", () => { { type: "session.step.started.1", callID: undefined }, { type: "session.tool.called.1", callID: "call-local-raw-failure" }, { type: "session.tool.called.1", callID: "call-hosted-raw-failure-pair" }, - { type: "session.tool.failed.1", callID: "call-local-raw-failure" }, - { type: "session.tool.failed.1", callID: "call-hosted-raw-failure-pair" }, + { type: "session.tool.failed.2", callID: "call-local-raw-failure" }, + { type: "session.tool.failed.2", callID: "call-hosted-raw-failure-pair" }, { type: "session.step.failed.1", callID: undefined }, ]) expect( @@ -4748,7 +4802,7 @@ describe("SessionRunnerLLM", () => { expect(events.map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", - "session.tool.failed.1", + "session.tool.failed.2", "session.step.failed.1", ]) expect( diff --git a/packages/core/test/session-tool-progress.test.ts b/packages/core/test/session-tool-progress.test.ts index cc81820e39f1..414e260e9840 100644 --- a/packages/core/test/session-tool-progress.test.ts +++ b/packages/core/test/session-tool-progress.test.ts @@ -84,30 +84,29 @@ describe("Tool.Progress", () => { yield* start("call-success") expect((yield* readAssistant).content[0]).toMatchObject({ - state: { status: "running", structured: {}, content: [] }, + state: { status: "running", metadata: {} }, }) const progress = yield* service.publish(SessionEvent.Tool.Progress, { sessionID, assistantMessageID, callID: "call-success", - structured: { phase: "checkpoint" }, - content: content("saved"), + metadata: { phase: "checkpoint" }, }) expect((yield* readAssistant).content[0]).toMatchObject({ - state: { status: "running", structured: {}, content: [] }, + state: { status: "running", metadata: {} }, }) const success = yield* service.publish(SessionEvent.Tool.Success, { sessionID, assistantMessageID, callID: "call-success", - structured: { phase: "done" }, + metadata: { phase: "done" }, content: content("complete"), executed: false, }) expect((yield* readAssistant).content[0]).toMatchObject({ - state: { status: "completed", structured: { phase: "done" }, content: content("complete") }, + state: { status: "completed", metadata: { phase: "done" }, content: content("complete") }, }) yield* start("call-failed") @@ -115,8 +114,7 @@ describe("Tool.Progress", () => { sessionID, assistantMessageID, callID: "call-failed", - structured: { phase: "checkpoint" }, - content: content("before failure"), + metadata: { phase: "checkpoint" }, }) const failed = yield* service.publish(SessionEvent.Tool.Failed, { sessionID, @@ -130,7 +128,7 @@ describe("Tool.Progress", () => { expect((yield* readAssistant).content[1]).toMatchObject({ state: { status: "error", - structured: { phase: "checkpoint" }, + metadata: { phase: "checkpoint" }, content: content("before failure"), error: { type: "unknown", message: "boom" }, }, @@ -147,8 +145,8 @@ describe("Tool.Progress", () => { .all() .pipe(Effect.orDie) expect(rows.map((row) => row.type)).not.toContain(EventV2.versionedType(SessionEvent.Tool.Progress.type, 1)) - expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Success.type, 1)) - expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Failed.type, 1)) + expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Success.type, 2)) + expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Failed.type, 2)) }), ) }) diff --git a/packages/core/test/tool-edit.test.ts b/packages/core/test/tool-edit.test.ts index aaecf9766fdd..446e6aecf55d 100644 --- a/packages/core/test/tool-edit.test.ts +++ b/packages/core/test/tool-edit.test.ts @@ -18,7 +18,7 @@ import { location } from "./fixture/location" import { tmpdir } from "./fixture/tmpdir" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { testEffect } from "./lib/effect" -import { toolIdentity, executeTool, registerToolPlugin, settleTool, toolDefinitions } from "./lib/tool" +import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from "./lib/tool" const editToolNode = makeLocationNode({ name: "test/edit-tool-plugin", @@ -141,15 +141,23 @@ describe("EditTool", () => { expect(yield* toolDefinitions(registry, [{ action: "edit", resource: "*", effect: "deny" }])).toEqual( [], ) - const settled = yield* settleTool( + const settled = yield* executeTool( registry, call({ path: "hello.txt", oldString: "before", newString: "after" }), ) - expect(settled.result).toEqual({ - type: "text", - value: "Edited file successfully: hello.txt\nReplacements: 1\n```diff\n-before\n+after\n```", + expect(settled.status).toBe("completed") + if (settled.status !== "completed") return + expect(settled.content).toEqual([ + { + type: "text", + text: "Edited file successfully: hello.txt\nReplacements: 1\n```diff\n-before\n+after\n```", + }, + ]) + // Compact UI metadata carries the file diffs the TUI renders. + expect(settled.metadata).toMatchObject({ + files: [{ file: "hello.txt", status: "modified", additions: 1, deletions: 1 }], }) - expect(settled.output?.structured).toEqual({ + expect(settled.output).toEqual({ replacements: 1, files: [ { @@ -187,7 +195,7 @@ describe("EditTool", () => { ), Effect.andThen((result) => Effect.gen(function* () { - expect(result.type).toBe("text") + expect(result.status).toBe("completed") expect(assertions.map((input) => input.action)).toEqual(["edit"]) expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("after") }), @@ -217,7 +225,7 @@ describe("EditTool", () => { ), Effect.andThen((result) => Effect.sync(() => { - expect(result.type).toBe("text") + expect(result.status).toBe("completed") expect(assertions.map((input) => input.action)).toEqual(["edit"]) expect(assertions[0]?.resources).toEqual(["link.txt"]) }), @@ -247,7 +255,7 @@ describe("EditTool", () => { ), Effect.andThen((result) => Effect.gen(function* () { - expect(result.type).toBe("text") + expect(result.status).toBe("completed") expect(assertions.map((input) => input.action)).toEqual(["external_directory", "edit"]) expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("after") expect(writes).toHaveLength(1) @@ -276,8 +284,8 @@ describe("EditTool", () => { executeTool(registry, call({ path: external, oldString: "before", newString: "after" })), ), ).toEqual({ - type: "error", - value: `Unable to edit ${external}`, + status: "error", + error: { type: "permission.rejected", message: "Permission denied: external_directory" }, }) expect(assertions.map((input) => input.action)).toEqual(["external_directory"]) expect(reads).toBe(0) @@ -290,8 +298,8 @@ describe("EditTool", () => { executeTool(registry, call({ path: external, oldString: "before", newString: "after" })), ), ).toEqual({ - type: "error", - value: `Unable to edit ${external}`, + status: "error", + error: { type: "permission.rejected", message: "Permission denied: edit" }, }) expect(assertions.map((input) => input.action)).toEqual(["external_directory", "edit"]) expect(reads).toBe(0) @@ -325,7 +333,10 @@ describe("EditTool", () => { call({ path: "secret.txt", oldString: "not present", newString: "replacement" }), ) - expect(matching).toEqual({ type: "error", value: "Unable to edit secret.txt" }) + expect(matching).toEqual({ + status: "error", + error: { type: "permission.rejected", message: "Permission denied: edit" }, + }) expect(missing).toEqual(matching) expect(assertions.map((input) => input.action)).toEqual(["edit", "edit"]) expect(reads).toBe(0) @@ -352,28 +363,40 @@ describe("EditTool", () => { expect( yield* executeTool(registry, call({ path: "matches.txt", oldString: "same", newString: "same" })), ).toEqual({ - type: "error", - value: "No changes to apply: oldString and newString are identical.", + status: "error", + error: { + type: "tool.execution", + message: "No changes to apply: oldString and newString are identical.", + }, }) expect( yield* executeTool(registry, call({ path: "matches.txt", oldString: "", newString: "after" })), ).toEqual({ - type: "error", - value: "oldString must not be empty. Use write to create or overwrite a file.", + status: "error", + error: { + type: "tool.execution", + message: "oldString must not be empty. Use write to create or overwrite a file.", + }, }) expect( yield* executeTool(registry, call({ path: "matches.txt", oldString: "missing", newString: "after" })), ).toEqual({ - type: "error", - value: - "Could not find oldString in the file. It must match exactly, including whitespace and indentation.", + status: "error", + error: { + type: "tool.execution", + message: + "Could not find oldString in the file. It must match exactly, including whitespace and indentation.", + }, }) expect( yield* executeTool(registry, call({ path: "matches.txt", oldString: "same", newString: "after" })), ).toEqual({ - type: "error", - value: - "Found multiple exact matches for oldString. Provide more surrounding context or set replaceAll to true.", + status: "error", + error: { + type: "tool.execution", + message: + "Found multiple exact matches for oldString. Provide more surrounding context or set replaceAll to true.", + }, }) expect(writes).toEqual([]) }), @@ -394,12 +417,14 @@ describe("EditTool", () => { return Effect.promise(() => fs.writeFile(target, "same same same")).pipe( Effect.andThen( withTool(tmp.path, (registry) => - settleTool(registry, call({ path: "all.txt", oldString: "same", newString: "after", replaceAll: true })), + executeTool(registry, call({ path: "all.txt", oldString: "same", newString: "after", replaceAll: true })), ), ), Effect.andThen((settled) => Effect.gen(function* () { - expect(settled.output?.structured).toMatchObject({ replacements: 3 }) + expect(settled.status).toBe("completed") + if (settled.status !== "completed") return + expect(settled.output).toMatchObject({ replacements: 3 }) expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("after after after") expect(writes).toHaveLength(1) }), @@ -445,9 +470,14 @@ describe("EditTool", () => { ), Effect.andThen((result) => Effect.gen(function* () { + // The message-less StaleContentError cause must not erase the tool's + // curated failure message; the canonical error is the sole authority. expect(result).toEqual({ - type: "error", - value: "File changed after permission approval. Read it again before editing.", + status: "error", + error: { + type: "tool.execution", + message: "File changed after permission approval. Read it again before editing.", + }, }) expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("newer\n") expect(writes).toEqual([]) diff --git a/packages/core/test/tool-execute.test.ts b/packages/core/test/tool-execute.test.ts index 66254ec408db..84df02243ecb 100644 --- a/packages/core/test/tool-execute.test.ts +++ b/packages/core/test/tool-execute.test.ts @@ -6,6 +6,69 @@ import { Session } from "@opencode-ai/schema/session" import { SessionMessage } from "@opencode-ai/schema/session-message" import { Effect, Schema } from "effect" +const context = { + sessionID: Session.ID.make("ses_execute"), + agent: Agent.ID.make("build"), + messageID: SessionMessage.ID.make("msg_execute"), + callID: "call_execute", + progress: () => Effect.void, +} + +test("canonical execution distinguishes declared, model-only, and raw schema outputs", async () => { + const declared = Tool.make({ + description: "Declared", + input: Schema.Struct({ value: Schema.String }), + output: Schema.Struct({ value: Schema.String }), + execute: ({ value }) => Effect.succeed({ output: { value } }), + }) + const modelOnly = Tool.make({ + description: "Model only", + input: Schema.Struct({}), + execute: () => Effect.succeed({ content: "visible only", metadata: { kind: "model" } }), + }) + const raw = Tool.make({ + description: "Raw", + input: {}, + output: {}, + execute: (input) => Effect.succeed({ output: input, content: "raw" }), + }) + + expect(await Effect.runPromise(Tool.execute(declared, { value: "encoded" }, context))).toEqual({ + output: { value: "encoded" }, + content: [{ type: "text", text: '{"value":"encoded"}' }], + }) + expect(await Effect.runPromise(Tool.execute(modelOnly, {}, context))).toEqual({ + content: [{ type: "text", text: "visible only" }], + metadata: { kind: "model" }, + }) + expect(await Effect.runPromise(Tool.execute(raw, { unchecked: true }, context))).toEqual({ + output: { unchecked: true }, + content: [{ type: "text", text: "raw" }], + }) +}) + +test("declared outputs cannot bypass validation and raw outputs stay JSON-compatible", async () => { + const missing: Tool.Any = { + description: "Missing output", + input: Schema.Struct({}), + output: Schema.String, + execute: () => Effect.succeed({ content: "not an output" }), + } + const invalid: Tool.Any = { + description: "Invalid raw output", + input: {}, + output: {}, + execute: () => Effect.succeed({ output: 1n, content: "not JSON" }), + } + + expect((await Effect.runPromiseExit(Tool.execute(missing, {}, context))).toString()).toContain( + "Tool did not return its declared output", + ) + expect((await Effect.runPromiseExit(Tool.execute(invalid, {}, context))).toString()).toContain( + "Tool returned a non-JSON value", + ) +}) + test("execute preserves successful results with visible unhandled rejections", async () => { const child = Tool.make({ description: "Always fail", @@ -13,27 +76,10 @@ test("execute preserves successful results with visible unhandled rejections", a output: Schema.String, execute: () => Effect.fail(new Tool.Failure({ message: "Lookup refused" })), }) - const execute = ExecuteTool.create(new Map([["fail", { tool: child, name: "fail" }]])) - const result = await Effect.runPromise( - Tool.settle( - execute, - { - type: "tool-call", - id: "call_execute", - name: "execute", - input: { code: `tools.fail({}); return "done"` }, - }, - { - sessionID: Session.ID.make("ses_execute"), - agent: Agent.ID.make("build"), - messageID: SessionMessage.ID.make("msg_execute"), - callID: "call_execute", - progress: () => Effect.void, - }, - ), - ) + const execute = ExecuteTool.create(new Map([["fail", { tool: child, name: "fail", permission: "fail" }]])) + const result = await Effect.runPromise(Tool.execute(execute, { code: `tools.fail({}); return "done"` }, context)) - expect(result.structured).toEqual({ toolCalls: [{ tool: "fail", status: "error" }] }) + expect(result.metadata).toEqual({ toolCalls: [{ tool: "fail", status: "error" }] }) expect(result.content).toEqual([ { type: "text", @@ -52,40 +98,32 @@ test("execute supports callable namespace tools", async () => { description: "Administer Slack", input: Schema.Struct({}), output: Schema.String, - execute: () => Effect.succeed("admin"), + execute: () => Effect.succeed({ output: "admin" }), }) const child = Tool.make({ description: "Create a Slack resource", input: Schema.Struct({}), output: Schema.String, - execute: () => Effect.succeed("created"), + execute: () => Effect.succeed({ output: "created" }), }) const execute = ExecuteTool.create( new Map([ - ["slack_admin", { tool: callable, name: "admin", namespace: "slack" }], - ["slack_admin_create", { tool: child, name: "create", namespace: "slack.admin" }], + ["slack_admin", { tool: callable, name: "admin", namespace: "slack", permission: "slack_admin" }], + [ + "slack_admin_create", + { tool: child, name: "create", namespace: "slack.admin", permission: "slack_admin_create" }, + ], ]), ) const result = await Effect.runPromise( - Tool.settle( + Tool.execute( execute, - { - type: "tool-call", - id: "call_execute", - name: "execute", - input: { code: "return [await tools.slack.admin({}), await tools.slack.admin.create({})]" }, - }, - { - sessionID: Session.ID.make("ses_execute"), - agent: Agent.ID.make("build"), - messageID: SessionMessage.ID.make("msg_execute"), - callID: "call_execute", - progress: () => Effect.void, - }, + { code: "return [await tools.slack.admin({}), await tools.slack.admin.create({})]" }, + context, ), ) - expect(result.structured).toEqual({ + expect(result.metadata).toEqual({ toolCalls: [ { tool: "slack.admin", status: "completed" }, { tool: "slack.admin.create", status: "completed" }, diff --git a/packages/core/test/tool-output-store.test.ts b/packages/core/test/tool-output-store.test.ts index d8a872ce602e..2e22402e104c 100644 --- a/packages/core/test/tool-output-store.test.ts +++ b/packages/core/test/tool-output-store.test.ts @@ -53,52 +53,31 @@ describe("ToolOutputStore", () => { const result = yield* store.bound({ sessionID, callID: "call-aggregate", - output: { - structured: { kind: "report" }, - content: [ - { type: "text", text: first }, - { type: "text", text: second }, - ], - }, + content: [ + { type: "text", text: first }, + { type: "text", text: second }, + ], }) - expect(result.output.structured).toEqual({ kind: "report" }) expect(result.outputPaths).toHaveLength(1) expect(yield* fs.readFileString(result.outputPaths[0])).toBe(first + second) - if (result.output.content[0]?.type !== "text") throw new Error("expected text preview") - expect(Buffer.byteLength(result.output.content[0].text)).toBeLessThanOrEqual(ToolOutputStore.MAX_BYTES) + if (result.content[0]?.type !== "text") throw new Error("expected text preview") + expect(Buffer.byteLength(result.content[0].text)).toBeLessThanOrEqual(ToolOutputStore.MAX_BYTES) }), ), ) - it.live("uses bounded text for oversized structured-only output", () => - withStore(({ store, fs }) => - Effect.gen(function* () { - const structured = { text: "x".repeat(ToolOutputStore.MAX_BYTES) } - const result = yield* store.bound({ sessionID, callID: "call-json", output: { structured, content: [] } }) - expect(result.output.structured).toEqual(structured) - expect(result.outputPaths).toHaveLength(1) - expect(JSON.parse(yield* fs.readFileString(result.outputPaths[0]))).toEqual(structured) - expect(result.output.content).toHaveLength(1) - }), - ), - ) - - it.live("preserves native media and structured metadata without applying a settlement media limit", () => + it.live("preserves native media without applying an execution media limit", () => withStore(({ store }) => Effect.gen(function* () { const data = "a".repeat(6 * 1024 * 1024) const result = yield* store.bound({ sessionID, callID: "call-file", - output: { - structured: { caption: "pixel" }, - content: [{ type: "file", uri: `data:image/png;base64,${data}`, mime: "image/png", name: "pixel.png" }], - }, + content: [{ type: "file", uri: `data:image/png;base64,${data}`, mime: "image/png", name: "pixel.png" }], }) expect(result.outputPaths).toEqual([]) - expect(result.output.structured).toEqual({ caption: "pixel" }) - expect(result.output.content).toHaveLength(1) - expect(result.output.content[0]).toEqual({ + expect(result.content).toHaveLength(1) + expect(result.content[0]).toEqual({ type: "file", uri: `data:image/png;base64,${data}`, mime: "image/png", @@ -108,7 +87,7 @@ describe("ToolOutputStore", () => { ), ) - it.live("preserves structured metadata and native media when bounding text", () => + it.live("preserves native media when bounding text", () => withStore(({ store, fs }) => Effect.gen(function* () { const text = "x".repeat(ToolOutputStore.MAX_BYTES + 1) @@ -121,30 +100,29 @@ describe("ToolOutputStore", () => { const result = yield* store.bound({ sessionID, callID: "call-text-and-media", - output: { structured: { caption: "pixel" }, content: [{ type: "text", text }, media] }, + content: [{ type: "text", text }, media], }) - expect(result.output.structured).toEqual({ caption: "pixel" }) - expect(result.output.content[1]).toEqual(media) + expect(result.content[1]).toEqual(media) expect(yield* fs.readFileString(result.outputPaths[0])).toBe(text) }), ), ) - it.live("does not double-count structured data duplicated in projected text", () => + it.live("returns content within the limits unchanged", () => withStore(({ store }) => Effect.gen(function* () { const text = "x".repeat(30_000) - const output = { structured: { output: text }, content: [{ type: "text" as const, text }] } - expect(yield* store.bound({ sessionID, callID: "call-duplicated", output })).toEqual({ - output, + const content = [{ type: "text" as const, text }] + expect(yield* store.bound({ sessionID, callID: "call-duplicated", content })).toEqual({ + content, outputPaths: [], }) }), ), ) - it.live("fails oversized settlement when complete retention cannot be written", () => + it.live("fails oversized execution when complete retention cannot be written", () => withStore(({ root, store, fs }) => Effect.gen(function* () { yield* fs.writeFileString(path.join(root, "tool-output"), "not a directory") @@ -152,7 +130,7 @@ describe("ToolOutputStore", () => { .bound({ sessionID, callID: "call-lossy", - output: { structured: {}, content: [{ type: "text", text: "x".repeat(ToolOutputStore.MAX_BYTES + 1) }] }, + content: [{ type: "text", text: "x".repeat(ToolOutputStore.MAX_BYTES + 1) }], }) .pipe(Effect.exit) expect(Exit.isFailure(exit)).toBe(true) @@ -162,18 +140,6 @@ describe("ToolOutputStore", () => { ), ) - it.live("does not encode ignored structured metadata when projected content exists", () => - withStore(({ store }) => - Effect.gen(function* () { - const output = { structured: { value: 1n }, content: [{ type: "text" as const, text: "readable text" }] } - expect(yield* store.bound({ sessionID, callID: "call-unencodable", output })).toEqual({ - output, - outputPaths: [], - }) - }), - ), - ) - it.live("preserves interruption while retaining complete output", () => Effect.gen(function* () { const root = yield* Effect.promise(() => tmpdir()) @@ -198,7 +164,7 @@ describe("ToolOutputStore", () => { .bound({ sessionID, callID: "call-interrupted", - output: { structured: {}, content: [{ type: "text", text: "x".repeat(ToolOutputStore.MAX_BYTES + 1) }] }, + content: [{ type: "text", text: "x".repeat(ToolOutputStore.MAX_BYTES + 1) }], }) .pipe(Effect.forkChild) yield* Fiber.interrupt(fiber) @@ -217,7 +183,7 @@ describe("ToolOutputStore", () => { const result = yield* store.bound({ sessionID, callID: "call-config", - output: { structured: {}, content: [{ type: "text", text: "one\ntwo\nthree" }] }, + content: [{ type: "text", text: "one\ntwo\nthree" }], }) expect(result.outputPaths).toHaveLength(1) }), diff --git a/packages/core/test/tool-patch.test.ts b/packages/core/test/tool-patch.test.ts index 86ae95e18372..cfae81eef86c 100644 --- a/packages/core/test/tool-patch.test.ts +++ b/packages/core/test/tool-patch.test.ts @@ -16,7 +16,7 @@ import { location } from "./fixture/location" import { tmpdir } from "./fixture/tmpdir" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { testEffect } from "./lib/effect" -import { toolIdentity, executeTool, registerToolPlugin, settleTool, toolDefinitions } from "./lib/tool" +import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from "./lib/tool" const patchToolNode = makeLocationNode({ name: "test/patch-tool-plugin", @@ -96,29 +96,19 @@ const withTool = ( const activeLocation = Layer.succeed( Location.Service, Location.Service.of( - location( - { directory: AbsolutePath.make(directory) }, - { projectDirectory: AbsolutePath.make(projectDirectory) }, - ), + location({ directory: AbsolutePath.make(directory) }, { projectDirectory: AbsolutePath.make(projectDirectory) }), ), ) return Effect.gen(function* () { return yield* body(yield* ToolRegistry.Service) }).pipe( Effect.provide( - AppNodeBuilder.build( - LayerNode.group([ - ToolRegistry.node, - ToolRegistry.toolsNode, - patchToolNode, - ]), - [ - [FSUtil.node, filesystem], - [Location.node, activeLocation], - [PermissionV2.node, permission], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], - ], - ), + AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, patchToolNode]), [ + [FSUtil.node, filesystem], + [Location.node, activeLocation], + [PermissionV2.node, permission], + [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + ]), ), ) } @@ -162,18 +152,23 @@ describe("PatchTool", () => { withTool(tmp.path, (registry) => Effect.gen(function* () { expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["patch"]) - const settled = yield* settleTool( + const settled = yield* executeTool( registry, call( "*** Begin Patch\n*** Add File: nested/new.txt\n+created\n*** Update File: update.txt\n@@\n-before\n+after\n*** Delete File: remove.txt\n*** End Patch", ), ) - expect(settled.result).toEqual({ - type: "text", - value: "Success. Updated the following files:\nA nested/new.txt\nM update.txt\nD remove.txt", - }) - if (process.platform === "win32") expect(settled.result.value).not.toContain("\\") - expect(settled.output?.structured).toMatchObject({ + expect(settled.status).toBe("completed") + if (settled.status !== "completed") return + expect(settled.content).toEqual([ + { + type: "text", + text: "Success. Updated the following files:\nA nested/new.txt\nM update.txt\nD remove.txt", + }, + ]) + const modelText = settled.content[0]?.type === "text" ? settled.content[0].text : "" + if (process.platform === "win32") expect(modelText).not.toContain("\\") + expect(settled.output).toMatchObject({ applied: [ { type: "add", resource: "nested/new.txt" }, { type: "update", resource: "update.txt" }, @@ -248,9 +243,11 @@ describe("PatchTool", () => { "*** Begin Patch\n*** Add File: created.txt\n+created\n*** Update File: old.txt\n*** Move to: moved.txt\n@@\n-before\n+after\n*** End Patch", ), ), - ).toEqual({ - type: "text", - value: "Success. Updated the following files:\nA created.txt\nM moved.txt", + ).toMatchObject({ + status: "completed", + content: [ + { type: "text", text: "Success. Updated the following files:\nA created.txt\nM moved.txt" }, + ], }) expect(yield* exists(source)).toBe(false) expect(yield* Effect.promise(() => fs.readFile(path.join(tmp.path, "moved.txt"), "utf8"))).toBe( @@ -278,7 +275,9 @@ describe("PatchTool", () => { return Effect.promise(() => Promise.all([ fs.writeFile(source, "before\n"), - fs.mkdir(path.dirname(destination), { recursive: true }).then(() => fs.writeFile(destination, "existing\n")), + fs + .mkdir(path.dirname(destination), { recursive: true }) + .then(() => fs.writeFile(destination, "existing\n")), ]), ).pipe( Effect.andThen( @@ -291,7 +290,7 @@ describe("PatchTool", () => { "*** Begin Patch\n*** Update File: old.txt\n*** Move to: nested/moved.txt\n@@\n-before\n+after\n*** End Patch", ), ), - ).toMatchObject({ type: "text" }) + ).toMatchObject({ status: "completed" }) expect(yield* exists(source)).toBe(false) expect(yield* Effect.promise(() => fs.readFile(destination, "utf8"))).toBe("after\n") }), @@ -325,19 +324,21 @@ describe("PatchTool", () => { ), ) - it.live("includes move file info in structured output", () => + it.live("includes move file info in output and metadata", () => withTempTool((directory, registry) => Effect.gen(function* () { const source = path.join(directory, "old", "name.txt") yield* Effect.promise(() => fs.mkdir(path.dirname(source), { recursive: true })) yield* Effect.promise(() => fs.writeFile(source, "old content\n")) - const settled = yield* settleTool( + const settled = yield* executeTool( registry, call( "*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/dir/name.txt\n@@\n-old content\n+new content\n*** End Patch", ), ) - expect(settled.output?.structured).toMatchObject({ + expect(settled.status).toBe("completed") + if (settled.status !== "completed") return + expect(settled.output).toMatchObject({ applied: [{ type: "update", resource: "renamed/dir/name.txt" }], files: [ { @@ -393,7 +394,7 @@ describe("PatchTool", () => { yield* Effect.promise(() => fs.mkdir(path.join(directory, "dir"))) expect( yield* executeTool(registry, call("*** Begin Patch\n*** Delete File: dir\n*** End Patch")), - ).toMatchObject({ type: "error" }) + ).toMatchObject({ status: "error" }) expect(yield* exists(path.join(directory, "dir"))).toBe(true) }), ), @@ -407,11 +408,9 @@ describe("PatchTool", () => { expect( yield* executeTool( registry, - call( - "*** Begin Patch\n*** Update File: two-chunks.txt\n@@\n-b\n+B\n\n-d\n+D\n*** End Patch", - ), + call("*** Begin Patch\n*** Update File: two-chunks.txt\n@@\n-b\n+B\n\n-d\n+D\n*** End Patch"), ), - ).toMatchObject({ type: "error" }) + ).toMatchObject({ status: "error" }) expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("a\nb\nc\nd\n") }), ), @@ -420,7 +419,10 @@ describe("PatchTool", () => { it.live("requires patchText", () => withTempTool((_directory, registry) => Effect.gen(function* () { - expect(yield* executeTool(registry, call(""))).toEqual({ type: "error", value: "patchText is required" }) + expect(yield* executeTool(registry, call(""))).toEqual({ + status: "error", + error: { type: "tool.execution", message: "patchText is required" }, + }) }), ), ) @@ -429,12 +431,18 @@ describe("PatchTool", () => { withTempTool((_directory, registry) => Effect.gen(function* () { expect(yield* executeTool(registry, call("invalid patch"))).toEqual({ - type: "error", - value: "patch verification failed: The first line of the patch must be '*** Begin Patch'", + status: "error", + error: { + type: "tool.execution", + message: "patch verification failed: The first line of the patch must be '*** Begin Patch'", + }, }) expect(yield* executeTool(registry, call("*** Begin Patch\n*** Add File: foo\n+hello"))).toEqual({ - type: "error", - value: "patch verification failed: The last line of the patch must be '*** End Patch'", + status: "error", + error: { + type: "tool.execution", + message: "patch verification failed: The last line of the patch must be '*** End Patch'", + }, }) }), ), @@ -444,8 +452,8 @@ describe("PatchTool", () => { withTempTool((_directory, registry) => Effect.gen(function* () { expect(yield* executeTool(registry, call("*** Begin Patch\n*** End Patch"))).toEqual({ - type: "error", - value: "patch rejected: empty patch", + status: "error", + error: { type: "tool.execution", message: "patch rejected: empty patch" }, }) }), ), @@ -454,15 +462,13 @@ describe("PatchTool", () => { it.live("rejects an invalid hunk header", () => withTempTool((_directory, registry) => Effect.gen(function* () { - expect( - yield* executeTool( - registry, - call("*** Begin Patch\n*** Frobnicate File: foo\n*** End Patch"), - ), - ).toEqual({ - type: "error", - value: - "patch verification failed: Invalid hunk at line 2: '*** Frobnicate File: foo' is not a valid hunk header. Valid hunk headers: '*** Add File: {path}', '*** Delete File: {path}', '*** Update File: {path}'", + expect(yield* executeTool(registry, call("*** Begin Patch\n*** Frobnicate File: foo\n*** End Patch"))).toEqual({ + status: "error", + error: { + type: "tool.execution", + message: + "patch verification failed: Invalid hunk at line 2: '*** Frobnicate File: foo' is not a valid hunk header. Valid hunk headers: '*** Add File: {path}', '*** Delete File: {path}', '*** Update File: {path}'", + }, }) }), ), @@ -490,13 +496,13 @@ describe("PatchTool", () => { const bom = "\uFEFF" const target = path.join(directory, "example.cs") yield* Effect.promise(() => fs.writeFile(target, `${bom}using System;\n\nclass Test {}\n`)) - const settled = yield* settleTool( + const settled = yield* executeTool( registry, - call( - "*** Begin Patch\n*** Update File: example.cs\n@@\n class Test {}\n+class Next {}\n*** End Patch", - ), + call("*** Begin Patch\n*** Update File: example.cs\n@@\n class Test {}\n+class Next {}\n*** End Patch"), ) - const output = Schema.decodeUnknownSync(PatchTool.Output)(settled.output?.structured) + expect(settled.status).toBe("completed") + if (settled.status !== "completed") return + const output = Schema.decodeUnknownSync(PatchTool.Output)(settled.output) expect(output.files[0]?.patch).not.toContain(bom) expect(output.files[0]?.patch).not.toContain("-using System;") expect(output.files[0]?.patch).not.toContain("+using System;") @@ -517,7 +523,10 @@ describe("PatchTool", () => { registry, call("*** Begin Patch\n*** Update File: unchanged.txt\n@@\n-missing\n+changed\n*** End Patch"), ), - ).toMatchObject({ type: "error", value: expect.stringContaining("Failed to find expected lines") }) + ).toMatchObject({ + status: "error", + error: { message: expect.stringContaining("Failed to find expected lines") }, + }) expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("line1\nline2\n") }), ), @@ -532,10 +541,12 @@ describe("PatchTool", () => { call("*** Begin Patch\n*** Update File: missing.txt\n@@\n-old\n+new\n*** End Patch"), ), ).toMatchObject({ - type: "error", - value: expect.stringContaining( - `patch verification failed: Failed to read file to update ${path.join(directory, "missing.txt")}: `, - ), + status: "error", + error: { + message: expect.stringContaining( + `patch verification failed: Failed to read file to update ${path.join(directory, "missing.txt")}: `, + ), + }, }) }), ), @@ -548,8 +559,11 @@ describe("PatchTool", () => { expect( yield* executeTool(registry, call("*** Begin Patch\n*** Update File: nested\n@@\n-old\n+new\n*** End Patch")), ).toEqual({ - type: "error", - value: `patch verification failed: Failed to read file to update ${path.join(directory, "nested")}: path is a directory`, + status: "error", + error: { + type: "tool.execution", + message: `patch verification failed: Failed to read file to update ${path.join(directory, "nested")}: path is a directory`, + }, }) }), ), @@ -560,7 +574,7 @@ describe("PatchTool", () => { Effect.gen(function* () { expect( yield* executeTool(registry, call("*** Begin Patch\n*** Delete File: missing.txt\n*** End Patch")), - ).toMatchObject({ type: "error", value: expect.stringContaining("patch verification failed") }) + ).toMatchObject({ status: "error", error: { message: expect.stringContaining("patch verification failed") } }) }), ), ) @@ -580,7 +594,7 @@ describe("PatchTool", () => { registry, call(`*** Begin Patch\n*** Update File: ${target}\n@@\n-before\n+after\n*** End Patch`), ), - ).toMatchObject({ type: "text" }) + ).toMatchObject({ status: "completed" }) expect(assertions.map((input) => input.action)).toEqual(["external_directory", "edit"]) expect(readsBeforeEditApproval).toBe(1) expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("after\n") @@ -614,7 +628,7 @@ describe("PatchTool", () => { registry, call(`*** Begin Patch\n*** Update File: ${target}\n@@\n-before\n+after\n*** End Patch`), ), - ).toMatchObject({ type: "error" }) + ).toMatchObject({ status: "error" }) expect(assertions.map((input) => input.action)).toEqual(["external_directory"]) expect(readsBeforeEditApproval).toBe(0) expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("before\n") @@ -649,7 +663,7 @@ describe("PatchTool", () => { registry, call("*** Begin Patch\n*** Update File: ../sibling.txt\n@@\n-before\n+after\n*** End Patch"), ), - ).toMatchObject({ type: "text" }) + ).toMatchObject({ status: "completed" }) expect(assertions.map((input) => input.action)).toEqual(["edit"]) expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("after\n") }), @@ -680,7 +694,7 @@ describe("PatchTool", () => { registry, call("*** Begin Patch\n*** Update File: link.txt\n@@\n-before\n+after\n*** End Patch"), ), - ).toMatchObject({ type: "text" }) + ).toMatchObject({ status: "completed" }) expect(assertions.map((input) => input.action)).toEqual(["edit"]) expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("after\n") }), @@ -711,7 +725,7 @@ describe("PatchTool", () => { registry, call(`*** Begin Patch\n*** Update File: ${relative}\n@@\n-before\n+after\n*** End Patch`), ), - ).toMatchObject({ type: "text" }) + ).toMatchObject({ status: "completed" }) expect(assertions.map((input) => input.action)).toEqual(["external_directory", "edit"]) expect(readsBeforeEditApproval).toBe(1) expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("after\n") @@ -747,7 +761,7 @@ describe("PatchTool", () => { `*** Begin Patch\n*** Update File: ${first}\n@@\n-before\n+after\n*** Update File: ${second}\n@@\n-before\n+after\n*** End Patch`, ), ), - ).toMatchObject({ type: "text" }) + ).toMatchObject({ status: "completed" }) expect(assertions.map((input) => input.action)).toEqual([ "external_directory", "external_directory", @@ -786,8 +800,10 @@ describe("PatchTool", () => { ), ), ).toMatchObject({ - type: "error", - value: expect.stringContaining("patch verification failed: Failed to read file to update"), + status: "error", + error: { + message: expect.stringContaining("patch verification failed: Failed to read file to update"), + }, }) expect(yield* exists(path.join(tmp.path, "created.txt"))).toBe(false) }), @@ -812,7 +828,7 @@ describe("PatchTool", () => { registry, call("*** Begin Patch\n*** Add File: existing.txt\n+replacement\n*** End Patch"), ), - ).toMatchObject({ type: "text" }) + ).toMatchObject({ status: "completed" }) expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("replacement\n") }), ), @@ -837,7 +853,7 @@ describe("PatchTool", () => { registry, call("*** Begin Patch\n*** Add File: appeared.txt\n+replacement\n*** End Patch"), ), - ).toMatchObject({ type: "text" }) + ).toMatchObject({ status: "completed" }) expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("replacement\n") }), ) @@ -876,5 +892,4 @@ describe("PatchTool", () => { (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ), ) - }) diff --git a/packages/core/test/tool-question.test.ts b/packages/core/test/tool-question.test.ts index 002806ec49c5..9a72bca1b5e4 100644 --- a/packages/core/test/tool-question.test.ts +++ b/packages/core/test/tool-question.test.ts @@ -12,7 +12,7 @@ import { Image } from "@opencode-ai/core/image" import { testEffect } from "./lib/effect" import { imagePassthrough } from "./lib/image" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { toolIdentity, executeTool, registerToolPlugin, settleTool, toolDefinitions } from "./lib/tool" +import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from "./lib/tool" const sessionID = SessionV2.ID.make("ses_question_tool_test") const assertions: PermissionV2.AssertInput[] = [] @@ -99,13 +99,13 @@ describe("QuestionTool", () => { expect(yield* toolDefinitions(registry, [{ action: "question", resource: "*", effect: "deny" }])).toEqual([]) expect( - yield* settleTool(registry, { + yield* executeTool(registry, { sessionID, ...toolIdentity, call: { type: "tool-call", id: "call-question-denied", name: "question", input: questionInput }, }), ).toEqual({ - result: { type: "error", value: "Permission denied: question" }, + status: "error", error: { type: "permission.rejected", message: "Permission denied: question", @@ -144,26 +144,21 @@ describe("QuestionTool", () => { expect((yield* toolDefinitions(registry)).map((definition) => definition.name)).toEqual(["question"]) expect( - yield* settleTool(registry, { + yield* executeTool(registry, { sessionID, ...toolIdentity, call: { type: "tool-call", id: "call-question", name: "question", input: { questions } }, }), ).toEqual({ - result: { - type: "text", - value: - 'User has answered your questions: "What should happen?"="Build", "Which environment?"="Dev", "Anything else?"="Unanswered". You can now continue with the user\'s answers in mind.', - }, - output: { - structured: { answers: [["Build"], ["Dev"], []] }, - content: [ - { - type: "text", - text: 'User has answered your questions: "What should happen?"="Build", "Which environment?"="Dev", "Anything else?"="Unanswered". You can now continue with the user\'s answers in mind.', - }, - ], - }, + status: "completed", + output: { answers: [["Build"], ["Dev"], []] }, + content: [ + { + type: "text", + text: 'User has answered your questions: "What should happen?"="Build", "Which environment?"="Dev", "Anything else?"="Unanswered". You can now continue with the user\'s answers in mind.', + }, + ], + metadata: { answers: [["Build"], ["Dev"], []] }, }) expect(assertions).toMatchObject([{ sessionID, action: "question", resources: ["*"] }]) expect(capturedInput()).toEqual({ diff --git a/packages/core/test/tool-read.test.ts b/packages/core/test/tool-read.test.ts index 6d309e38348c..6b3d084a10a2 100644 --- a/packages/core/test/tool-read.test.ts +++ b/packages/core/test/tool-read.test.ts @@ -22,7 +22,7 @@ import { ReadToolFileSystem } from "@opencode-ai/core/tool/read-filesystem" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { SessionInstructions } from "@opencode-ai/core/session/instructions" import { testEffect } from "./lib/effect" -import { toolIdentity, executeTool, registerToolPlugin, settleTool, toolDefinitions } from "./lib/tool" +import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from "./lib/tool" const readToolNode = makeLocationNode({ name: "test/read-tool-plugin", @@ -199,21 +199,19 @@ describe("ReadTool", () => { expect(yield* toolDefinitions(registry)).toMatchObject([{ name: "read" }]) expect(yield* toolDefinitions(registry, [{ action: "read", resource: "*", effect: "deny" }])).toEqual([]) - expect( - yield* executeTool(registry, { - sessionID, - ...toolIdentity, - call: { type: "tool-call", id: "call-read", name: "read", input: { path: "README.md" } }, - }), - ).toEqual({ - type: "json", - value: { - uri: "file:///README.md", - name: "README.md", - content: "hello", - encoding: "utf8", - mime: "text/plain", - }, + const execution = yield* executeTool(registry, { + sessionID, + ...toolIdentity, + call: { type: "tool-call", id: "call-read", name: "read", input: { path: "README.md" } }, + }) + expect(execution.status).toBe("completed") + if (execution.status !== "completed") return + expect(execution.output).toEqual({ + uri: "file:///README.md", + name: "README.md", + content: "hello", + encoding: "utf8", + mime: "text/plain", }) expect(assertions).toMatchObject([{ sessionID, action: "read", resources: ["README.md"], save: ["*"] }]) expect(readCalls).toEqual([ @@ -236,7 +234,7 @@ describe("ReadTool", () => { ...toolIdentity, call: { type: "tool-call", id: "call-external-read", name: "read", input: { path: external } }, }), - ).toMatchObject({ type: "json" }) + ).toMatchObject({ status: "completed" }) expect(assertions).toMatchObject([ { sessionID, @@ -261,19 +259,17 @@ describe("ReadTool", () => { } const registry = yield* ToolRegistry.Service - expect( - yield* executeTool(registry, { - sessionID, - ...toolIdentity, - call: { type: "tool-call", id: "call-image", name: "read", input: { path: "pixel.png" } }, - }), - ).toEqual({ - type: "content", - value: [ - { type: "text", text: "Image read successfully" }, - { type: "file", uri: `data:image/png;base64,${png}`, mime: "image/png", name: "pixel.png" }, - ], + const execution = yield* executeTool(registry, { + sessionID, + ...toolIdentity, + call: { type: "tool-call", id: "call-image", name: "read", input: { path: "pixel.png" } }, }) + expect(execution.status).toBe("completed") + if (execution.status !== "completed") return + expect(execution.content).toEqual([ + { type: "text", text: "Image read successfully" }, + { type: "file", uri: `data:image/png;base64,${png}`, mime: "image/png", name: "pixel.png" }, + ]) expect(readCalls).toEqual([ { input: AbsolutePath.make(path.join(process.cwd(), "pixel.png")), @@ -281,21 +277,17 @@ describe("ReadTool", () => { }, ]) - const settled = yield* settleTool(registry, { + const settled = yield* executeTool(registry, { sessionID, ...toolIdentity, call: { type: "tool-call", id: "call-image-settle", name: "read", input: { path: "pixel.png" } }, }) - expect(settled.output?.structured).toMatchObject({ - uri: "file:///pixel.png", - name: "pixel.png", - mime: "image/png", - encoding: "base64", - // Image base64 is carried by the content file item only; structured is slimmed - // so the original bytes are never persisted twice. - content: "", - }) - expect(settled.output?.content).toMatchObject([ + expect(settled.status).toBe("completed") + if (settled.status !== "completed") return + // Image base64 is carried by the content file item only; read produces no + // metadata, so the original bytes are never persisted twice. + expect(settled.metadata).toBeUndefined() + expect(settled.content).toMatchObject([ { type: "text", text: "Image read successfully" }, { type: "file", mime: "image/png", uri: `data:image/png;base64,${png}` }, ]) @@ -319,26 +311,25 @@ describe("ReadTool", () => { } const registry = yield* ToolRegistry.Service - const settled = yield* settleTool(registry, { + const settled = yield* executeTool(registry, { sessionID, ...toolIdentity, call: { type: "tool-call", id: "call-large-image", name: "read", input: { path: "large.png" } }, }) expect(settled.outputPaths).toBeUndefined() - expect(settled.output?.structured).toMatchObject({ + expect(settled.status).toBe("completed") + if (settled.status !== "completed") return + expect(settled.output).toMatchObject({ uri: "file:///large.png", name: "large.png", mime: "image/png", encoding: "base64", }) - expect(settled.result).toEqual({ - type: "content", - value: [ - { type: "text", text: "Image read successfully" }, - { type: "file", uri: `data:image/png;base64,${png}`, mime: "image/png", name: "large.png" }, - ], - }) + expect(settled.content).toEqual([ + { type: "text", text: "Image read successfully" }, + { type: "file", uri: `data:image/png;base64,${png}`, mime: "image/png", name: "large.png" }, + ]) }), ) @@ -361,13 +352,13 @@ describe("ReadTool", () => { call: { type: "tool-call", id: "call-image-fallback", name: "read", input: { path: "pixel.png" } }, }), ).toMatchObject({ - type: "content", - value: [{ type: "text" }, { type: "file", uri: `data:image/png;base64,${png}`, mime: "image/png" }], + status: "completed", + content: [{ type: "text" }, { type: "file", uri: `data:image/png;base64,${png}`, mime: "image/png" }], }) }), ) - it.effect("drops undecodable image data at settlement", () => + it.effect("drops undecodable image data from the outcome", () => Effect.gen(function* () { readResult = { uri: "file:///truncated.png", @@ -384,9 +375,9 @@ describe("ReadTool", () => { ...toolIdentity, call: { type: "tool-call", id: "call-truncated-image", name: "read", input: { path: "truncated.png" } }, }), - ).toEqual({ - type: "content", - value: [ + ).toMatchObject({ + status: "completed", + content: [ { type: "text", text: "Image read successfully" }, { type: "text", text: "[1 image omitted: could not be decoded.]" }, ], @@ -394,7 +385,7 @@ describe("ReadTool", () => { }), ) - it.effect("drops oversized images at settlement when resizing is disabled", () => + it.effect("drops oversized images from the outcome when resizing is disabled", () => Effect.gen(function* () { const photon = yield* Effect.promise(() => import("@silvia-odwyer/photon-node")) const source = new photon.PhotonImage(new Uint8Array(Array.from({ length: 16 * 4 }, () => 255)), 16, 1) @@ -425,9 +416,9 @@ describe("ReadTool", () => { ...toolIdentity, call: { type: "tool-call", id: "call-wide-image", name: "read", input: { path: "wide.png" } }, }), - ).toEqual({ - type: "content", - value: [ + ).toMatchObject({ + status: "completed", + content: [ { type: "text", text: "Image read successfully" }, { type: "text", text: "[1 image omitted: could not be resized below the image size limit.]" }, ], @@ -463,9 +454,9 @@ describe("ReadTool", () => { call: { type: "tool-call", id: "call-resize-image", name: "read", input: { path: "wide.png" } }, }) - expect(result.type).toBe("content") - if (result.type !== "content") return - const media = result.value[1] + expect(result.status).toBe("completed") + if (result.status !== "completed") return + const media = result.content[1] expect(media?.type).toBe("file") if (media?.type !== "file") return const resized = photon.PhotonImage.new_from_byteslice(Buffer.from(media.uri.split(",")[1] ?? "", "base64")) @@ -503,9 +494,9 @@ describe("ReadTool", () => { ...toolIdentity, call: { type: "tool-call", id: "call-max-bytes", name: "read", input: { path: "pixel.png" } }, }), - ).toEqual({ - type: "content", - value: [ + ).toMatchObject({ + status: "completed", + content: [ { type: "text", text: "Image read successfully" }, { type: "text", text: "[1 image omitted: could not be resized below the image size limit.]" }, ], @@ -532,8 +523,8 @@ describe("ReadTool", () => { call: { type: "tool-call", id: "call-disguised-image", name: "read", input: { path: "pixel.bin" } }, }), ).toMatchObject({ - type: "content", - value: [{ type: "text" }, { type: "file", mime: "image/png", name: "pixel.bin" }], + status: "completed", + content: [{ type: "text" }, { type: "file", mime: "image/png", name: "pixel.bin" }], }) }), ) @@ -554,7 +545,7 @@ describe("ReadTool", () => { input: { path: "archive.dat", offset: 2, limit: 1 }, }, }), - ).toEqual({ type: "error", value: "Cannot read binary file: archive.dat" }) + ).toEqual({ status: "error", error: { type: "unknown", message: "Cannot read binary file: archive.dat" } }) expect(readCalls).toEqual([ { input: AbsolutePath.make(path.join(process.cwd(), "archive.dat")), page: { offset: 2, limit: 1 } }, ]) @@ -589,7 +580,7 @@ describe("ReadTool", () => { ...toolIdentity, call: { type: "tool-call", id: "call-read", name: "read", input: { path: "README.md" } }, }), - ).toEqual({ type: "error", value: "Unable to read README.md" }) + ).toEqual({ status: "error", error: { type: "permission.rejected", message: "Permission denied: read" } }) expect(readCalls).toEqual([]) }), ) @@ -604,7 +595,9 @@ describe("ReadTool", () => { ...toolIdentity, call: { type: "tool-call", id: "call-missing-path", name: "read", input: { path: missingPath } }, }), - ).toEqual({ type: "error", value: `Unable to read ${missingPath}` }) + // The message-less PathError cause must not erase the tool's curated + // failure message; the canonical error is the sole authority. + ).toEqual({ status: "error", error: { type: "tool.execution", message: `Unable to read ${missingPath}` } }) expect(assertions).toEqual([]) expect(readCalls).toEqual([]) }), @@ -626,7 +619,7 @@ describe("ReadTool", () => { input: { path: "src", offset: 2, limit: 10 }, }, }), - ).toEqual({ type: "json", value: { entries: [], truncated: false } }) + ).toMatchObject({ status: "completed", output: { entries: [], truncated: false } }) expect(assertions).toMatchObject([{ sessionID, action: "read", resources: ["src"], save: ["*"] }]) expect(listCalls).toEqual([{ offset: 2, limit: 10 }]) }), @@ -644,7 +637,7 @@ describe("ReadTool", () => { ...toolIdentity, call: { type: "tool-call", id: "call-read-directory-denied", name: "read", input: { path: "src" } }, }), - ).toEqual({ type: "error", value: "Unable to read src" }) + ).toEqual({ status: "error", error: { type: "permission.rejected", message: "Permission denied: read" } }) expect(listCalls).toEqual([]) }), ) @@ -691,9 +684,9 @@ describe("ReadTool", () => { input: { path: "large.txt", offset: 2, limit: 1 }, }, }), - ).toEqual({ - type: "json", - value: { type: "text-page", content: "hello", mime: "text/plain", offset: 2, truncated: true, next: 3 }, + ).toMatchObject({ + status: "completed", + output: { type: "text-page", content: "hello", mime: "text/plain", offset: 2, truncated: true, next: 3 }, }) expect(readCalls).toEqual([ { input: AbsolutePath.make(path.join(process.cwd(), "large.txt")), page: { offset: 2, limit: 1 } }, @@ -718,7 +711,7 @@ describe("ReadTool", () => { ...toolIdentity, call: { type: "tool-call", id: "call-direct-binary", name: "read", input: { path: "late-binary" } }, }), - ).toEqual({ type: "error", value: "Cannot read binary file: late-binary" }) + ).toEqual({ status: "error", error: { type: "unknown", message: "Cannot read binary file: late-binary" } }) }), ) }) diff --git a/packages/core/test/tool-search.test.ts b/packages/core/test/tool-search.test.ts index 4367d1b82e66..9958b0dfb4b7 100644 --- a/packages/core/test/tool-search.test.ts +++ b/packages/core/test/tool-search.test.ts @@ -19,7 +19,7 @@ import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { location } from "./fixture/location" import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" -import { executeTool, registerToolPlugin, settleTool, toolIdentity } from "./lib/tool" +import { executeTool, registerToolPlugin, toolIdentity } from "./lib/tool" const globToolNode = makeLocationNode({ name: "test/glob-tool-plugin", @@ -83,15 +83,17 @@ describe("search tools", () => { ) yield* withTools(tmp.path, (registry) => Effect.gen(function* () { - const glob = yield* settleTool(registry, call("glob", { pattern: "*" })) - const grep = yield* settleTool(registry, call("grep", { pattern: "needle" })) + const glob = yield* executeTool(registry, call("glob", { pattern: "*" })) + const grep = yield* executeTool(registry, call("grep", { pattern: "needle" })) - expect(glob.output?.structured).toEqual({ count: FileSystem.DEFAULT_SEARCH_LIMIT }) - expect(grep.output?.structured).toEqual({ matches: FileSystem.DEFAULT_SEARCH_LIMIT }) - expect(glob.output?.content).toEqual([{ type: "text", text: String(glob.result.value) }]) - expect(grep.output?.content).toEqual([{ type: "text", text: String(grep.result.value) }]) - expect(String(glob.result.value).split("\n")).toHaveLength(FileSystem.DEFAULT_SEARCH_LIMIT) - expect(grep.result.value).toStartWith(`Found ${FileSystem.DEFAULT_SEARCH_LIMIT} matches\n`) + expect(glob.metadata).toEqual({ count: FileSystem.DEFAULT_SEARCH_LIMIT }) + expect(grep.metadata).toEqual({ matches: FileSystem.DEFAULT_SEARCH_LIMIT }) + expect(glob.content).toHaveLength(1) + expect(grep.content).toHaveLength(1) + const globText = glob.content?.[0]?.type === "text" ? glob.content[0].text : "" + const grepText = grep.content?.[0]?.type === "text" ? grep.content[0].text : "" + expect(globText.split("\n")).toHaveLength(FileSystem.DEFAULT_SEARCH_LIMIT) + expect(grepText).toStartWith(`Found ${FileSystem.DEFAULT_SEARCH_LIMIT} matches\n`) }), ) }), @@ -110,7 +112,10 @@ describe("search tools", () => { registry, call(name, { path: "missing", pattern: name === "glob" ? "*" : "needle" }), ) - expect(result).toEqual({ type: "error", value: "Search path does not exist: missing" }) + expect(result).toEqual({ + status: "error", + error: { type: "tool.execution", message: "Search path does not exist: missing" }, + }) }), ), (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), diff --git a/packages/core/test/tool-shell.test.ts b/packages/core/test/tool-shell.test.ts index 039680a8d186..69af78d5693a 100644 --- a/packages/core/test/tool-shell.test.ts +++ b/packages/core/test/tool-shell.test.ts @@ -33,7 +33,7 @@ import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" -import { toolIdentity, executeTool, settleTool, toolDefinitions, waitForTool } from "./lib/tool" +import { toolIdentity, executeTool, toolDefinitions, waitForTool } from "./lib/tool" const sessionID = SessionV2.ID.make("ses_shell_tool_test") const sessionModel = ModelV2.Ref.make({ id: ModelV2.ID.make("test"), providerID: ProviderV2.ID.make("test") }) @@ -204,17 +204,19 @@ describe("ShellTool", () => { const definitions = yield* toolDefinitions(registry) const shell = definitions.find((tool) => tool.name === "shell") expect(shell).toBeDefined() - expect(shell?.outputSchema).not.toHaveProperty("properties.output") + // Code Mode receives the declared output schema, including the command output text. + expect(shell?.outputSchema).toHaveProperty("properties.output") expect( (yield* toolDefinitions(registry, [{ action: "shell", resource: "*", effect: "deny" }])).map( (tool) => tool.name, ), ).not.toContain("shell") - const settled = yield* settleTool(registry, call({ command: helloCommand })) - expect(settled.output?.structured).toMatchObject({ exit: 0, truncated: false }) - expect(settled.output?.content[0]).toEqual({ type: "text", text: "hello" }) - expect(settled.output?.content[1]).toMatchObject({ + const settled = yield* executeTool(registry, call({ command: helloCommand })) + expect(settled.status).toBe("completed") + expect(settled.metadata).toMatchObject({ exit: 0, truncated: false }) + expect(settled.content?.[0]).toEqual({ type: "text", text: "hello" }) + expect(settled.content?.[1]).toMatchObject({ type: "text", text: expect.stringContaining("Command exited with code 0."), }) @@ -233,11 +235,11 @@ describe("ShellTool", () => { reset() return Effect.promise(() => fs.mkdir(path.join(tmp.path, "src"))).pipe( Effect.andThen( - withSession(tmp.path, (registry) => settleTool(registry, call({ command: cwdCommand, workdir: "src" }))), + withSession(tmp.path, (registry) => executeTool(registry, call({ command: cwdCommand, workdir: "src" }))), ), Effect.andThen((settled) => Effect.sync(() => - expect(settled.output?.content[0]).toMatchObject({ + expect(settled.content?.[0]).toMatchObject({ type: "text", text: expect.stringContaining(realpathSync(path.join(tmp.path, "src"))), }), @@ -256,13 +258,13 @@ describe("ShellTool", () => { reset() return withSession(tmp.path, (registry) => Effect.gen(function* () { - const stderr = yield* settleTool(registry, call({ command: stderrCommand }, "call-stderr")) - expect(stderr.output?.structured).toMatchObject({ exit: 0, truncated: false }) - expect(stderr.output?.content[0]).toEqual({ type: "text", text: "stderr only" }) + const stderr = yield* executeTool(registry, call({ command: stderrCommand }, "call-stderr")) + expect(stderr.metadata).toMatchObject({ exit: 0, truncated: false }) + expect(stderr.content?.[0]).toEqual({ type: "text", text: "stderr only" }) - const mixed = yield* settleTool(registry, call({ command: mixedOutputCommand }, "call-mixed")) - expect(mixed.output?.structured).toMatchObject({ exit: 0, truncated: false }) - const output = mixed.output?.content[0]?.type === "text" ? mixed.output.content[0].text : "" + const mixed = yield* executeTool(registry, call({ command: mixedOutputCommand }, "call-mixed")) + expect(mixed.metadata).toMatchObject({ exit: 0, truncated: false }) + const output = mixed.content?.[0]?.type === "text" ? mixed.content[0].text : "" expect(output).toContain("stdout") expect(output).toContain("stderr") }), @@ -352,12 +354,12 @@ describe("ShellTool", () => { reset() denyAction = "external_directory" const target = path.join(outside.path, "secret.txt") - return withSession(active.path, (registry) => settleTool(registry, call({ command: `cat ${target}` }))).pipe( + return withSession(active.path, (registry) => executeTool(registry, call({ command: `cat ${target}` }))).pipe( Effect.andThen((settled) => Effect.sync(() => { expect(assertions.map((item) => item.action)).toEqual(["shell"]) - expect(settled.output?.structured).not.toHaveProperty("warnings") - expect(settled.output?.content[1]).toMatchObject({ + expect(settled.metadata).not.toHaveProperty("warnings") + expect(settled.content?.[1]).toMatchObject({ type: "text", text: expect.stringContaining("Warnings:"), }) @@ -378,13 +380,14 @@ describe("ShellTool", () => { (tmp) => { reset() return withSession(tmp.path, (registry) => - settleTool(registry, call({ command: bodyExitCommand }, "call-nonzero")), + executeTool(registry, call({ command: bodyExitCommand }, "call-nonzero")), ).pipe( Effect.andThen((settled) => Effect.sync(() => { - expect(settled.output?.structured).toMatchObject({ exit: 7, truncated: false }) - expect(settled.output?.content[0]).toEqual({ type: "text", text: "body" }) - expect(settled.output?.content[1]).toMatchObject({ + expect(settled.status).toBe("completed") + expect(settled.metadata).toMatchObject({ exit: 7, truncated: false }) + expect(settled.content?.[0]).toEqual({ type: "text", text: "body" }) + expect(settled.content?.[1]).toMatchObject({ type: "text", text: expect.stringContaining("Command exited with code 7"), }) @@ -403,12 +406,12 @@ describe("ShellTool", () => { reset() const bytes = ShellTool.MAX_CAPTURE_BYTES + 1024 return withSession(tmp.path, (registry) => - settleTool(registry, call({ command: overflowCommand(bytes) }, "call-overflow")), + executeTool(registry, call({ command: overflowCommand(bytes) }, "call-overflow")), ).pipe( Effect.andThen((settled) => Effect.sync(() => { - expect(settled.output?.structured).toMatchObject({ exit: 0, truncated: true }) - expect(settled.output?.content[0]).toMatchObject({ + expect(settled.metadata).toMatchObject({ exit: 0, truncated: true }) + expect(settled.content?.[0]).toMatchObject({ type: "text", text: expect.stringContaining("output truncated; full output saved to:"), }) @@ -421,7 +424,7 @@ describe("ShellTool", () => { ) it.live( - "reports bounded output progress for a running command", + "reports the shell ID for a running command", () => Effect.acquireUseRelease( Effect.promise(() => tmpdir()), @@ -431,32 +434,21 @@ describe("ShellTool", () => { const releasePath = path.join(tmp.path, release) return withSession(tmp.path, (registry) => Effect.gen(function* () { - const observed = yield* Deferred.make() - yield* settleTool(registry, { + const observed = yield* Deferred.make() + yield* executeTool(registry, { ...call( { command: progressOverflowCommand(ShellTool.MAX_CAPTURE_BYTES + 1024, release) }, "call-progress", ), progress: (update) => Effect.gen(function* () { - if (update.structured.truncated !== true) return - const content = update.content[0] - if (content?.type !== "text") return - if (content.text.indexOf("\n\n[output truncated; full output saved to:") !== ShellTool.MAX_CAPTURE_BYTES) - return - yield* Deferred.succeed(observed, update) + if (typeof update.shellID !== "string") return + yield* Deferred.succeed(observed, update.shellID) yield* Effect.promise(() => fs.writeFile(releasePath, "")) }), }) - const progress = yield* Deferred.await(observed) - expect(progress.structured).toEqual({ truncated: true }) - const content = progress.content[0] - expect(content?.type).toBe("text") - if (content?.type !== "text") return - expect(content.text.indexOf("\n\n[output truncated; full output saved to:")).toBe( - ShellTool.MAX_CAPTURE_BYTES, - ) + expect(yield* Deferred.await(observed)).toMatch(/^sh_/) }).pipe(Effect.ensuring(Effect.promise(() => fs.writeFile(releasePath, "")).pipe(Effect.ignore))), ) }, @@ -466,7 +458,7 @@ describe("ShellTool", () => { ) it.live( - "does not repeat unchanged shell progress", + "does not repeat shell ID progress", () => Effect.acquireUseRelease( Effect.promise(() => tmpdir()), @@ -475,16 +467,12 @@ describe("ShellTool", () => { return withSession(tmp.path, (registry) => Effect.gen(function* () { const updates: ToolRegistry.Progress[] = [] - yield* settleTool(registry, { + yield* executeTool(registry, { ...call({ command: steadyProgressCommand }, "call-steady-progress"), progress: (update) => Effect.sync(() => updates.push(update)), }) - expect(updates).toEqual([ - { - structured: { truncated: false }, - content: [{ type: "text", text: "steady" }], - }, - ]) + expect(updates).toHaveLength(1) + expect(updates[0]?.shellID).toMatch(/^sh_/) }), ) }, @@ -493,18 +481,18 @@ describe("ShellTool", () => { { timeout: 10_000 }, ) - it.live("returns a useful timeout settlement", () => + it.live("returns a useful timeout outcome", () => Effect.acquireUseRelease( Effect.promise(() => tmpdir()), (tmp) => { reset() return withSession(tmp.path, (registry) => - settleTool(registry, call({ command: idleCommand, timeout: 50 })), + executeTool(registry, call({ command: idleCommand, timeout: 50 })), ).pipe( Effect.andThen((settled) => Effect.sync(() => { - expect(settled.output?.structured).toMatchObject({ timeout: true, truncated: false }) - expect(settled.output?.content[1]).toMatchObject({ + expect(settled.metadata).toMatchObject({ timeout: true, truncated: false }) + expect(settled.content?.[1]).toMatchObject({ type: "text", text: expect.stringContaining("Command timed out"), }) @@ -529,10 +517,9 @@ describe("ShellTool", () => { Stream.runHead, Effect.forkScoped({ startImmediately: true }), ) - const settled = yield* settleTool(registry, call({ command: idleCommand, timeout: 50, background: true })) - const structured = settled.output?.structured as Record | undefined - const shellID = typeof structured?.shellID === "string" ? structured.shellID : undefined - expect(settled.output?.structured).toMatchObject({ truncated: false }) + const settled = yield* executeTool(registry, call({ command: idleCommand, timeout: 50, background: true })) + const shellID = typeof settled.metadata?.shellID === "string" ? settled.metadata.shellID : undefined + expect(settled.metadata).toMatchObject({ truncated: false }) expect(shellID).toStartWith("sh_") const shell = yield* Shell.Service @@ -562,22 +549,22 @@ describe("ShellTool", () => { return withSession(tmp.path, (registry) => Effect.gen(function* () { const shell = yield* Shell.Service - const timed = yield* settleTool( + const timed = yield* executeTool( registry, call({ command: idleCommand, background: true }, "call-updated-timeout"), ) - const timedID = (timed.output?.structured as Record | undefined)?.shellID + const timedID = timed.metadata?.shellID expect(typeof timedID).toBe("string") if (typeof timedID !== "string") return const timedShellID = ShellSchema.ID.make(timedID) yield* shell.timeout(timedShellID, 50) expect((yield* shell.wait(timedShellID)).status).toBe("timeout") - const cleared = yield* settleTool( + const cleared = yield* executeTool( registry, call({ command: idleCommand, timeout: 50, background: true }, "call-cleared-timeout"), ) - const clearedID = (cleared.output?.structured as Record | undefined)?.shellID + const clearedID = cleared.metadata?.shellID expect(typeof clearedID).toBe("string") if (typeof clearedID !== "string") return const clearedShellID = ShellSchema.ID.make(clearedID) @@ -601,7 +588,7 @@ describe("ShellTool", () => { Effect.gen(function* () { const jobs = yield* Job.Service const scope = yield* Scope.Scope - const waiting = yield* settleTool( + const waiting = yield* executeTool( registry, call({ command: idleCommand, timeout: 50 }, "call-background-signal"), ).pipe(Effect.forkIn(scope, { startImmediately: true })) @@ -616,14 +603,13 @@ describe("ShellTool", () => { }) expect(yield* backgroundWhenReady()).toMatchObject([{ id: "call-background-signal", type: "shell" }]) const settled = yield* Fiber.join(waiting) - const structured = settled.output?.structured as Record | undefined - const shellID = typeof structured?.shellID === "string" ? structured.shellID : undefined - expect(settled.output?.structured).toMatchObject({ truncated: false }) - expect(settled.output?.content[0]).toEqual({ + const shellID = typeof settled.metadata?.shellID === "string" ? settled.metadata.shellID : undefined + expect(settled.metadata).toMatchObject({ truncated: false }) + expect(settled.content?.[0]).toEqual({ type: "text", text: "The command was moved to the background.", }) - expect(settled.output?.content[1]).toMatchObject({ + expect(settled.content?.[1]).toMatchObject({ type: "text", text: expect.stringContaining("DO NOT sleep, poll"), }) diff --git a/packages/core/test/tool-skill.test.ts b/packages/core/test/tool-skill.test.ts index 72f5565deec4..eab58f75d71d 100644 --- a/packages/core/test/tool-skill.test.ts +++ b/packages/core/test/tool-skill.test.ts @@ -17,7 +17,7 @@ import { it } from "./lib/effect" import { imagePassthrough } from "./lib/image" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { FSUtil } from "@opencode-ai/util/fs-util" -import { toolIdentity, executeTool, registerToolPlugin, settleTool, toolDefinitions } from "./lib/tool" +import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from "./lib/tool" const skillToolNode = makeLocationNode({ name: "test/skill-tool-plugin", @@ -108,23 +108,22 @@ describe("SkillTool", () => { ...toolIdentity, call: { type: "tool-call", id: "call-skill", name: "skill", input: { id: "effect" } }, }), - ).toEqual({ - type: "text", - value: SkillTool.toModelOutput(info, [reference]), + ).toMatchObject({ + status: "completed", + content: [{ type: "text", text: SkillTool.toModelOutput(info, [reference]) }], }) expect(SkillTool.toModelOutput(info, [reference])).toContain(`Base directory for this skill: ${directory}`) expect( - yield* settleTool(registry, { + yield* executeTool(registry, { sessionID, ...toolIdentity, call: { type: "tool-call", id: "call-skill-overflow", name: "skill", input: { id: "effect" } }, }), ).toEqual({ - result: { type: "text", value: SkillTool.toModelOutput(info, [reference]) }, - output: { - structured: { name: "Effect", directory }, - content: [{ type: "text", text: SkillTool.toModelOutput(info, [reference]) }], - }, + status: "completed", + output: { name: "Effect", directory, output: SkillTool.toModelOutput(info, [reference]) }, + content: [{ type: "text", text: SkillTool.toModelOutput(info, [reference]) }], + metadata: { name: "Effect", directory }, }) expect(assertions).toMatchObject([ { sessionID, action: "skill", resources: ["effect"], save: ["effect"] }, @@ -136,7 +135,10 @@ describe("SkillTool", () => { ...toolIdentity, call: { type: "tool-call", id: "call-missing-skill", name: "skill", input: { id: "missing" } }, }), - ).toEqual({ type: "error", value: "Unable to load skill missing" }) + ).toEqual({ + status: "error", + error: { type: "tool.execution", message: "Unable to load skill missing" }, + }) deny = true expect( yield* executeTool(registry, { @@ -144,7 +146,10 @@ describe("SkillTool", () => { ...toolIdentity, call: { type: "tool-call", id: "call-denied-skill", name: "skill", input: { id: "effect" } }, }), - ).toEqual({ type: "error", value: "Unable to load skill effect" }) + ).toEqual({ + status: "error", + error: { type: "permission.rejected", message: "Permission denied: skill" }, + }) deny = false const flat = SkillV2.Info.make({ id: SkillV2.ID.make("public"), @@ -166,7 +171,10 @@ describe("SkillTool", () => { ...toolIdentity, call: { type: "tool-call", id: "call-flat-skill", name: "skill", input: { id: "public" } }, }), - ).toEqual({ type: "text", value: SkillTool.toModelOutput(flat, []) }) + ).toMatchObject({ + status: "completed", + content: [{ type: "text", text: SkillTool.toModelOutput(flat, []) }], + }) }).pipe(Effect.provide(skillToolLayer)) }), ), diff --git a/packages/core/test/tool-subagent.test.ts b/packages/core/test/tool-subagent.test.ts index 8c70525a9ab0..8ec8ecec162e 100644 --- a/packages/core/test/tool-subagent.test.ts +++ b/packages/core/test/tool-subagent.test.ts @@ -28,7 +28,7 @@ import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" -import { executeTool, settleTool, toolIdentity, waitForTool } from "./lib/tool" +import { executeTool, toolIdentity, waitForTool } from "./lib/tool" const childText = "child final response" const childModel = ModelV2.Ref.make({ id: ModelV2.ID.make("child"), providerID: ProviderV2.ID.make("test") }) @@ -148,7 +148,7 @@ describe("SubagentTool", () => { const locations = yield* LocationServiceMap.Service const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location))) yield* waitForTool(registry, SubagentTool.name) - expect((yield* registry.materialize()).definitions.map((tool) => tool.name)).toContain(SubagentTool.name) + expect((yield* registry.snapshot()).definitions.map((tool) => tool.name)).toContain(SubagentTool.name) expect( yield* executeTool(registry, { sessionID: parent.id, @@ -160,7 +160,10 @@ describe("SubagentTool", () => { input: { agent: "primary", description: "primary", prompt: "should fail" }, }, }), - ).toEqual({ type: "error", value: "Agent primary cannot run as a subagent" }) + ).toEqual({ + status: "error", + error: { type: "tool.execution", message: "Agent primary cannot run as a subagent" }, + }) }), ), ), @@ -193,7 +196,13 @@ describe("SubagentTool", () => { input: { agent: "reviewer", description: "nested", prompt: "should fail" }, }, }), - ).toEqual({ type: "error", value: expect.stringContaining("Subagent depth limit reached (1)") }) + ).toEqual({ + status: "error", + error: { + type: "tool.execution", + message: expect.stringContaining("Subagent depth limit reached (1)"), + }, + }) expect((yield* sessions.list({ parentID: parent.id })).data).toHaveLength(0) }), ), @@ -219,7 +228,7 @@ describe("SubagentTool", () => { const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location))) yield* waitForTool(registry, SubagentTool.name) - const settled = yield* settleTool(registry, { + const settled = yield* executeTool(registry, { sessionID: parent.id, ...toolIdentity, call: { @@ -231,17 +240,15 @@ describe("SubagentTool", () => { }) expect(settled).toMatchObject({ - result: { type: "text", value: childText }, - output: { - structured: { status: "completed" }, - content: [{ type: "text", text: childText }], - }, + status: "completed", + metadata: { status: "completed" }, + content: [{ type: "text", text: childText }], }) - expect(settled.output?.structured).toEqual({ - sessionID: outputSessionID(settled.output?.structured), + expect(settled.metadata).toEqual({ + sessionID: outputSessionID(settled.metadata), status: "completed", }) - expect((yield* sessions.get(outputSessionID(settled.output?.structured))).parentID).toBe(parent.id) + expect((yield* sessions.get(outputSessionID(settled.metadata))).parentID).toBe(parent.id) }), ), ), @@ -263,7 +270,7 @@ describe("SubagentTool", () => { yield* waitForTool(registry, SubagentTool.name) const progress: ToolRegistry.Progress[] = [] - const settled = yield* settleTool(registry, { + const settled = yield* executeTool(registry, { sessionID: parent.id, ...toolIdentity, progress: (update) => Effect.sync(() => progress.push(update)), @@ -276,15 +283,13 @@ describe("SubagentTool", () => { }) expect(settled).toMatchObject({ - result: { type: "text", value: childText }, - output: { - structured: { status: "completed" }, - content: [{ type: "text", text: childText }], - }, + status: "completed", + metadata: { status: "completed" }, + content: [{ type: "text", text: childText }], }) - const child = yield* sessions.get(outputSessionID(settled.output?.structured)) - expect(settled.output?.structured).toEqual({ sessionID: child.id, status: "completed" }) - expect(progress[0]?.structured).toEqual({ sessionID: child.id, status: "running" }) + const child = yield* sessions.get(outputSessionID(settled.metadata)) + expect(settled.metadata).toEqual({ sessionID: child.id, status: "completed" }) + expect(progress[0]?.metadata).toEqual({ sessionID: child.id, status: "running" }) expect(child).toMatchObject({ parentID: parent.id, location: parent.location, @@ -295,7 +300,7 @@ describe("SubagentTool", () => { "You are a subagent spawned by another session.\nreview this", ) - const fallback = yield* settleTool(registry, { + const fallback = yield* executeTool(registry, { sessionID: parent.id, ...toolIdentity, call: { @@ -305,7 +310,7 @@ describe("SubagentTool", () => { input: { agent: "fallback", description: "fallback", prompt: "fallback" }, }, }) - const fallbackChild = yield* sessions.get(outputSessionID(fallback.output?.structured)) + const fallbackChild = yield* sessions.get(outputSessionID(fallback.metadata)) expect(fallbackChild).toMatchObject({ parentID: parent.id, model: parentModel }) }), ), @@ -338,7 +343,13 @@ describe("SubagentTool", () => { input: { agent: "reviewer", description: "fail review", prompt: "please fail" }, }, }), - ).toEqual({ type: "error", value: expect.stringContaining("No model is available for session") }) + ).toEqual({ + status: "error", + error: { + type: "tool.execution", + message: expect.stringContaining("No model is available for session"), + }, + }) }), ), ), @@ -366,7 +377,7 @@ describe("SubagentTool", () => { Effect.forkScoped({ startImmediately: true }), ) - const settled = yield* settleTool(registry, { + const settled = yield* executeTool(registry, { sessionID: parent.id, ...toolIdentity, call: { @@ -376,13 +387,12 @@ describe("SubagentTool", () => { input: { agent: "reviewer", description: "background review", prompt: "review", background: true }, }, }) - const childID = outputSessionID(settled.output?.structured) - expect(settled.output?.structured).toMatchObject({ + const childID = outputSessionID(settled.metadata) + expect(settled.metadata).toMatchObject({ status: "running", }) - expect(settled.output?.structured).toEqual({ sessionID: childID, status: "running" }) - expect(settled.result).toEqual({ type: "text", value: expect.stringContaining(`id: ${childID}`) }) - expect(settled.output?.content).toEqual([{ type: "text", text: expect.stringContaining(`id: ${childID}`) }]) + expect(settled.metadata).toEqual({ sessionID: childID, status: "running" }) + expect(settled.content).toEqual([{ type: "text", text: expect.stringContaining(`id: ${childID}`) }]) const admission = Array.from(yield* Fiber.join(admitted))[0] expect(admission?.data.input.data.text).toContain(` { const url = "http://example.com/public" expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["webfetch"]) - expect(yield* settleTool(registry, call({ url, format: "text", timeout: 4 }))).toEqual({ - result: { type: "text", value: "hello" }, - output: { - structured: { contentType: "text/plain" }, - content: [{ type: "text", text: "hello" }], - }, + expect(yield* executeTool(registry, call({ url, format: "text", timeout: 4 }))).toEqual({ + status: "completed", + output: { url, contentType: "text/plain", format: "text", output: "hello" }, + content: [{ type: "text", text: "hello" }], + metadata: { contentType: "text/plain" }, }) expect(assertions).toMatchObject([ { sessionID, action: "webfetch", resources: [url], save: ["*"], metadata: { url, format: "text", timeout: 4 } }, @@ -113,9 +112,9 @@ describe("WebFetchTool registration", () => { const registry = yield* ToolRegistry.Service const url = "http://localhost/private" - expect(yield* executeTool(registry, call({ url, format: "text" }))).toEqual({ - type: "text", - value: "hello", + expect(yield* executeTool(registry, call({ url, format: "text" }))).toMatchObject({ + status: "completed", + content: [{ type: "text", text: "hello" }], }) expect(assertions).toMatchObject([ { sessionID, action: "webfetch", resources: [url], save: ["*"], metadata: { url, format: "text" } }, @@ -141,9 +140,9 @@ describe("WebFetchTool registration", () => { const registry = yield* ToolRegistry.Service const url = new URL("/redirect", server.url).toString() - expect(yield* executeTool(registry, call({ url, format: "text" }))).toEqual({ - type: "text", - value: "redirected", + expect(yield* executeTool(registry, call({ url, format: "text" }))).toMatchObject({ + status: "completed", + content: [{ type: "text", text: "redirected" }], }) expect(assertions).toMatchObject([ { sessionID, action: "webfetch", resources: [url], save: ["*"], metadata: { url, format: "text" } }, @@ -158,9 +157,10 @@ describe("WebFetchTool registration", () => { reset() const registry = yield* ToolRegistry.Service + // toSessionError unwraps the "Unable to fetch " ToolFailure to its cause message. expect(yield* executeTool(registry, call({ url: "file:///etc/passwd", format: "text" }))).toEqual({ - type: "error", - value: "Unable to fetch file:///etc/passwd", + status: "error", + error: { type: "unknown", message: "URL must use http:// or https://" }, }) expect(assertions).toEqual([]) expect(requests).toEqual([]) @@ -178,13 +178,13 @@ describe("WebFetchTool registration", () => { ) const registry = yield* ToolRegistry.Service - expect(yield* executeTool(registry, call({ url: "https://1.1.1.1", format: "markdown" }))).toEqual({ - type: "text", - value: "# Hello\n\nworld", + expect(yield* executeTool(registry, call({ url: "https://1.1.1.1", format: "markdown" }))).toMatchObject({ + status: "completed", + content: [{ type: "text", text: "# Hello\n\nworld" }], }) - expect(yield* executeTool(registry, call({ url: "https://1.1.1.1", format: "text" }))).toEqual({ - type: "text", - value: "Helloworld", + expect(yield* executeTool(registry, call({ url: "https://1.1.1.1", format: "text" }))).toMatchObject({ + status: "completed", + content: [{ type: "text", text: "Helloworld" }], }) }), ) @@ -201,9 +201,9 @@ describe("WebFetchTool registration", () => { const registry = yield* ToolRegistry.Service const url = "https://1.1.1.1/deep-html" - expect(yield* executeTool(registry, call({ url, format: "markdown" }))).toEqual({ - type: "error", - value: `Unable to fetch ${url}`, + expect(yield* executeTool(registry, call({ url, format: "markdown" }))).toMatchObject({ + status: "error", + error: { type: "unknown" }, }) }), ) @@ -219,8 +219,11 @@ describe("WebFetchTool registration", () => { }), ) expect(yield* executeTool(registry, call({ url: "https://1.1.1.1/declared", format: "text" }))).toEqual({ - type: "error", - value: "Unable to fetch https://1.1.1.1/declared", + status: "error", + error: { + type: "unknown", + message: `Response too large (exceeds ${WebFetchTool.MAX_RESPONSE_BYTES} byte limit)`, + }, }) respond = () => @@ -228,26 +231,29 @@ describe("WebFetchTool registration", () => { new Response("x".repeat(WebFetchTool.MAX_RESPONSE_BYTES + 1), { headers: { "content-type": "text/plain" } }), ) expect(yield* executeTool(registry, call({ url: "https://1.1.1.1/streamed", format: "text" }))).toEqual({ - type: "error", - value: "Unable to fetch https://1.1.1.1/streamed", + status: "error", + error: { + type: "unknown", + message: `Response too large (exceeds ${WebFetchTool.MAX_RESPONSE_BYTES} byte limit)`, + }, }) }), ) - it.effect("keeps images and files unsupported until typed settlement can carry attachments", () => + it.effect("keeps images and files unsupported until typed outcomes can carry attachments", () => Effect.gen(function* () { reset() const registry = yield* ToolRegistry.Service respond = () => Effect.succeed(new Response("png", { headers: { "content-type": "image/png" } })) expect(yield* executeTool(registry, call({ url: "https://1.1.1.1/image", format: "html" }))).toEqual({ - type: "error", - value: "Unable to fetch https://1.1.1.1/image", + status: "error", + error: { type: "unknown", message: "Unsupported fetched image content type: image/png" }, }) respond = () => Effect.succeed(new Response("pdf", { headers: { "content-type": "application/pdf" } })) expect(yield* executeTool(registry, call({ url: "https://1.1.1.1/file", format: "html" }))).toEqual({ - type: "error", - value: "Unable to fetch https://1.1.1.1/file", + status: "error", + error: { type: "unknown", message: "Unsupported fetched file content type: application/pdf" }, }) }), ) @@ -264,9 +270,9 @@ describe("WebFetchTool registration", () => { ) const registry = yield* ToolRegistry.Service - expect(yield* executeTool(registry, call({ url: "https://1.1.1.1", format: "text" }))).toEqual({ - type: "text", - value: "ok", + expect(yield* executeTool(registry, call({ url: "https://1.1.1.1", format: "text" }))).toMatchObject({ + status: "completed", + content: [{ type: "text", text: "ok" }], }) expect(requests).toHaveLength(2) expect(requests[0]?.headers["user-agent"]).toContain("Mozilla/5.0") @@ -285,7 +291,10 @@ describe("WebFetchTool registration", () => { ).pipe(Effect.forkChild) yield* TestClock.adjust(Duration.seconds(1)) - expect(yield* Fiber.join(fiber)).toEqual({ type: "error", value: "Unable to fetch https://1.1.1.1/slow" }) + expect(yield* Fiber.join(fiber)).toEqual({ + status: "error", + error: { type: "unknown", message: "Request timed out" }, + }) }), ) }) diff --git a/packages/core/test/tool-websearch.test.ts b/packages/core/test/tool-websearch.test.ts index 37d3960ab7c9..7a8c2bb09c12 100644 --- a/packages/core/test/tool-websearch.test.ts +++ b/packages/core/test/tool-websearch.test.ts @@ -13,7 +13,7 @@ import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Image } from "@opencode-ai/core/image" import { testEffect } from "./lib/effect" import { imagePassthrough } from "./lib/image" -import { toolIdentity, executeTool, registerToolPlugin, settleTool, toolDefinitions } from "./lib/tool" +import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from "./lib/tool" const webSearchToolNode = makeLocationNode({ name: "test/websearch-tool-plugin", @@ -172,7 +172,10 @@ describe("WebSearchTool registration", () => { }, }, }), - ).toEqual({ type: "text", value: "exa results" }) + ).toMatchObject({ + status: "completed", + content: [{ type: "text", text: "exa results" }], + }) expect(assertions).toMatchObject([ { sessionID, @@ -221,7 +224,7 @@ describe("WebSearchTool registration", () => { config = { provider: "parallel", enableExa: false, enableParallel: false, parallelApiKey: "parallel-secret" } const registry = yield* ToolRegistry.Service - const settled = yield* settleTool(registry, { + const settled = yield* executeTool(registry, { sessionID, ...toolIdentity, call: { type: "tool-call", id: "call-parallel", name: "websearch", input: { query: "effect layers" } }, @@ -242,11 +245,10 @@ describe("WebSearchTool registration", () => { }) expect(requests[0]?.body).not.toHaveProperty("params.arguments.model_name") expect(settled).toEqual({ - result: { type: "text", value: "parallel results" }, - output: { - structured: { provider: "parallel" }, - content: [{ type: "text", text: "parallel results" }], - }, + status: "completed", + output: { provider: "parallel", text: "parallel results" }, + content: [{ type: "text", text: "parallel results" }], + metadata: { provider: "parallel" }, }) expect(JSON.stringify(settled)).not.toContain("parallel-secret") }), @@ -260,7 +262,7 @@ describe("WebSearchTool registration", () => { config = { provider: "exa", enableExa: false, enableParallel: false, exaApiKey: "exa secret" } const registry = yield* ToolRegistry.Service - const settled = yield* settleTool(registry, { + const settled = yield* executeTool(registry, { sessionID, ...toolIdentity, call: { type: "tool-call", id: "call-exa-key", name: "websearch", input: { query: "effect schema" } }, @@ -285,7 +287,10 @@ describe("WebSearchTool registration", () => { ...toolIdentity, call: { type: "tool-call", id: "call-empty", name: "websearch", input: { query: "nothing" } }, }), - ).toEqual({ type: "text", value: WebSearchTool.NO_RESULTS }) + ).toMatchObject({ + status: "completed", + content: [{ type: "text", text: WebSearchTool.NO_RESULTS }], + }) }), ) @@ -318,7 +323,12 @@ describe("WebSearchTool registration", () => { ...toolIdentity, call: { type: "tool-call", id: "call-large-response", name: "websearch", input: { query: "too much" } }, }), - ).toEqual({ type: "error", value: "Unable to search the web for too much" }) + // toSessionError unwraps the "Unable to search the web for " ToolFailure + // to its byte-limit cause message. + ).toEqual({ + status: "error", + error: { type: "unknown", message: expect.stringContaining("response exceeded") }, + }) expect(chunksRead).toBeLessThan(10) expect(cancelled).toBe(true) }), diff --git a/packages/core/test/tool-write.test.ts b/packages/core/test/tool-write.test.ts index 67a001e0eb08..3cd3dcb4b617 100644 --- a/packages/core/test/tool-write.test.ts +++ b/packages/core/test/tool-write.test.ts @@ -18,7 +18,7 @@ import { location } from "./fixture/location" import { tmpdir } from "./fixture/tmpdir" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { testEffect } from "./lib/effect" -import { toolIdentity, executeTool, registerToolPlugin, settleTool, toolDefinitions } from "./lib/tool" +import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from "./lib/tool" const writeToolNode = makeLocationNode({ name: "test/write-tool-plugin", @@ -119,18 +119,16 @@ describe("WriteTool", () => { return withTool(tmp.path, (registry) => Effect.gen(function* () { expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["write"]) - const settled = yield* settleTool(registry, call({ path: "src/new.txt", content: "created" })) + const settled = yield* executeTool(registry, call({ path: "src/new.txt", content: "created" })) expect(settled).toEqual({ - result: { type: "text", value: "Created file successfully: src/new.txt" }, + status: "completed", output: { - structured: { - operation: "write", - target: path.join(yield* Effect.promise(() => fs.realpath(tmp.path)), "src", "new.txt"), - resource: "src/new.txt", - existed: false, - }, - content: [{ type: "text", text: "Created file successfully: src/new.txt" }], + operation: "write", + target: path.join(yield* Effect.promise(() => fs.realpath(tmp.path)), "src", "new.txt"), + resource: "src/new.txt", + existed: false, }, + content: [{ type: "text", text: "Created file successfully: src/new.txt" }], }) expect(yield* Effect.promise(() => fs.readFile(path.join(tmp.path, "src", "new.txt"), "utf8"))).toBe( "created", @@ -151,12 +149,14 @@ describe("WriteTool", () => { reset() return Effect.promise(() => fs.writeFile(path.join(tmp.path, "existing.txt"), "before")).pipe( Effect.andThen( - withTool(tmp.path, (registry) => settleTool(registry, call({ path: "existing.txt", content: "after" }))), + withTool(tmp.path, (registry) => executeTool(registry, call({ path: "existing.txt", content: "after" }))), ), Effect.andThen((settled) => Effect.gen(function* () { - expect(settled.result).toEqual({ type: "text", value: "Wrote file successfully: existing.txt" }) - expect(settled.output?.structured).toMatchObject({ resource: "existing.txt", existed: true }) + expect(settled.status).toBe("completed") + if (settled.status !== "completed") return + expect(settled.content).toEqual([{ type: "text", text: "Wrote file successfully: existing.txt" }]) + expect(settled.output).toMatchObject({ resource: "existing.txt", existed: true }) expect(yield* Effect.promise(() => fs.readFile(path.join(tmp.path, "existing.txt"), "utf8"))).toBe( "after", ) @@ -182,8 +182,8 @@ describe("WriteTool", () => { Effect.andThen( withTool(tmp.path, (registry) => Effect.gen(function* () { - yield* settleTool(registry, call({ path: "preserved.txt", content: "after" }, "call-preserved")) - yield* settleTool( + yield* executeTool(registry, call({ path: "preserved.txt", content: "after" }, "call-preserved")) + yield* executeTool( registry, call({ path: "deduplicated.txt", content: "\uFEFFafter" }, "call-deduplicated"), ) @@ -208,7 +208,10 @@ describe("WriteTool", () => { return withTool(tmp.path, (registry) => executeTool(registry, call({ path: target, content: "inside" }))).pipe( Effect.andThen((result) => Effect.gen(function* () { - expect(result).toEqual({ type: "text", value: "Created file successfully: absolute.txt" }) + expect(result).toMatchObject({ + status: "completed", + content: [{ type: "text", text: "Created file successfully: absolute.txt" }], + }) expect(assertions.map((input) => input.action)).toEqual(["edit"]) expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("inside") }), @@ -236,7 +239,7 @@ describe("WriteTool", () => { ), Effect.andThen((result) => Effect.sync(() => { - expect(result.type).toBe("text") + expect(result.status).toBe("completed") expect(assertions.map((input) => input.action)).toEqual(["edit"]) expect(assertions[0]?.resources).toEqual(["link.txt"]) }), @@ -259,7 +262,7 @@ describe("WriteTool", () => { reset() const target = path.join(outside.path, "external.txt") return withTool(active.path, (registry) => - settleTool(registry, call({ path: target, content: "external" })), + executeTool(registry, call({ path: target, content: "external" })), ).pipe( Effect.andThen((settled) => Effect.gen(function* () { @@ -271,10 +274,13 @@ describe("WriteTool", () => { ], }) expect(assertions[1]).toMatchObject({ resources: [canonicalTarget.replaceAll("\\", "/")], save: ["*"] }) - expect(settled.output?.structured).toMatchObject({ - target: canonicalTarget, - resource: canonicalTarget.replaceAll("\\", "/"), - existed: false, + expect(settled).toMatchObject({ + status: "completed", + output: { + target: canonicalTarget, + resource: canonicalTarget.replaceAll("\\", "/"), + existed: false, + }, }) expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("external") expect(writes).toEqual([canonicalTarget]) @@ -336,8 +342,8 @@ describe("WriteTool", () => { executeTool(registry, call({ path: external, content: "blocked" })), ), ).toEqual({ - type: "error", - value: `Unable to write ${external}`, + status: "error", + error: { type: "permission.rejected", message: "Permission denied: external_directory" }, }) expect(assertions.map((input) => input.action)).toEqual(["external_directory"]) expect(writes).toEqual([]) @@ -349,8 +355,8 @@ describe("WriteTool", () => { executeTool(registry, call({ path: "denied.txt", content: "blocked" })), ), ).toEqual({ - type: "error", - value: "Unable to write denied.txt", + status: "error", + error: { type: "permission.rejected", message: "Permission denied: edit" }, }) expect(assertions.map((input) => input.action)).toEqual(["edit"]) expect(writes).toEqual([]) diff --git a/packages/docs/build/plugins.mdx b/packages/docs/build/plugins.mdx index 01a8b5746b9c..e358a3ccae9f 100644 --- a/packages/docs/build/plugins.mdx +++ b/packages/docs/build/plugins.mdx @@ -248,7 +248,7 @@ mutable fields: | `ctx.aisdk.hook("language", callback)` | `language`, after inspecting `model`, `sdk`, and `options` | | `ctx.session.hook("request", callback)` | `system`, `messages`, and the `tools` record immediately before model dispatch | | `ctx.tool.hook("execute.before", callback)` | `input`, before the selected tool executes | -| `ctx.tool.hook("execute.after", callback)` | `result`, `output`, and `outputPaths`, after execution settles | +| `ctx.tool.hook("execute.after", callback)` | Terminal `content`, `metadata`, and `outputPaths`; `error` on failure | For example, remove a tool from selected model requests and normalize another tool's input: @@ -278,52 +278,66 @@ handle expected errors inside the callback. ### Add a tool -Pass a tool declaration to `tools.add`. Define its input with JSON Schema and -use an async executor: +Create an executable tool with `Tool.make`, then register it with a name +and registration options. Define its input with JSON Schema and use an async +executor: ```js title=".opencode/plugins/greeting.js" import { Plugin } from "@opencode-ai/plugin/v2" +import { Tool } from "@opencode-ai/plugin/v2/tool" export default Plugin.define({ id: "acme.greeting", setup: async (ctx) => { await ctx.tool.transform((tools) => { - tools.add({ - name: "greeting", - description: "Create a greeting", - jsonSchema: { - type: "object", - properties: { - name: { type: "string" }, + tools.add( + "greeting", + Tool.make({ + description: "Create a greeting", + input: { + type: "object", + properties: { + name: { type: "string" }, + }, + required: ["name"], + additionalProperties: false, }, - required: ["name"], - additionalProperties: false, - }, - execute: async ({ name }) => { - const text = `Hello, ${name}!` - return { - structured: { greeting: text }, - content: [{ type: "text", text }], - } - }, - }) + output: { + type: "object", + properties: { greeting: { type: "string" } }, + required: ["greeting"], + additionalProperties: false, + }, + execute: async ({ name }) => { + const text = `Hello, ${name}!` + return { + output: { greeting: text }, + content: text, + } + }, + }), + ) }) }, }) ``` -Unsupported characters in tool and group names are normalized to underscores. -The resulting exposed key must begin with a letter and contain at most 64 -letters, digits, underscores, or hyphens. Set `options` on the declaration to -configure registration with `{ group, codemode }`: +Unsupported characters in tool names are normalized to underscores. Namespace +segments must begin with a letter, contain at most 64 letters, digits, +underscores, or hyphens, and are joined with dots. Pass the optional third +argument to `tools.add` to configure the registration with +`{ namespace, codemode }`: -- `group` prefixes and groups the exposed tool name. +- `namespace` prefixes and groups the exposed tool name. - `codemode` defaults to `true` and makes the tool available through the `execute` CodeMode tool. Set `codemode: false` to expose it directly to the provider. The executor receives a second context argument containing `sessionID`, -`agent`, `assistantMessageID`, and `toolCallID`. +`agent`, `messageID`, `callID`, and `progress`. A tool with `output` +must return `output`; Effect and Standard Schema codecs validate it, while raw +JSON Schema definitions enforce JSON compatibility only. A tool +without `output` returns model-visible `content` instead. ### Add a command @@ -426,6 +440,7 @@ fibers, and registrations are released when the plugin reloads or unloads. OpenCode does not expose its private Core services to the plugin; use the capabilities on `ctx`. -Typed tools can use `Schema` from `effect` and the contracts exported from -`@opencode-ai/plugin/v2/effect/tool`. Their executors return an Effect and may -fail with the typed tool failure channel. +Typed tools can use `Schema` from `effect` and `Tool.make` from +`@opencode-ai/plugin/v2/effect/tool`. Effect and Promise plugins use the same +`tools.add(name, tool, options?)` registration shape. Effect executors +return an Effect and may fail with the typed tool failure channel. diff --git a/packages/plugin/src/v2/effect/internal/tool.ts b/packages/plugin/src/v2/effect/internal/tool.ts new file mode 100644 index 000000000000..abc92c6a7d22 --- /dev/null +++ b/packages/plugin/src/v2/effect/internal/tool.ts @@ -0,0 +1,315 @@ +import { Agent } from "@opencode-ai/schema/agent" +import { LLM } from "@opencode-ai/schema/llm" +import { Session } from "@opencode-ai/schema/session" +import { SessionError } from "@opencode-ai/schema/session-error" +import { SessionMessage } from "@opencode-ai/schema/session-message" +import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec" +import { Effect, JsonSchema, Schema } from "effect" +import type { Hooks, Transform } from "../registration.js" + +// Tools + +/** A JSON-compatible value. Tool metadata and encoded outputs must be JSON. */ +export type JsonValue = typeof Schema.Json.Type + +/** Compact JSON metadata for tool-specific UI and client behavior. */ +export type Metadata = Readonly> + +export interface Context { + readonly sessionID: Session.ID + readonly agent: Agent.ID + readonly messageID: SessionMessage.ID + readonly callID: string + readonly progress: (update: Progress) => Effect.Effect +} + +/** Live replacement metadata for a running tool. */ +export type Progress = Metadata + +export type StandardSchemaType = StandardSchemaV1 & + StandardJSONSchemaV1 +export type SchemaType = Schema.Codec | StandardSchemaType | JsonSchema.JsonSchema +type IsAny = 0 extends 1 & A ? true : false +export type InputValue = + IsAny extends true + ? any + : S extends Schema.Codec + ? A + : S extends StandardSchemaV1 + ? A + : unknown +export type OutputValue = + IsAny extends true + ? any + : S extends Schema.Codec + ? A + : S extends StandardSchemaV1 + ? A + : unknown +export type EncodedValue = + IsAny extends true + ? any + : S extends Schema.Codec + ? A + : S extends StandardSchemaV1 + ? A + : unknown + +type ToolDefinition = { + readonly name: string + readonly description: string + readonly inputSchema: JsonSchema.JsonSchema + readonly outputSchema?: JsonSchema.JsonSchema +} + +export class Failure extends Schema.TaggedErrorClass()("LLM.ToolFailure", { + message: Schema.String, + error: Schema.optional(Schema.Defect()), +}) {} + +export class RegistrationError extends Schema.TaggedErrorClass()("Tool.RegistrationError", { + name: Schema.String, + message: Schema.String, +}) {} + +export type Content = + | { readonly type: "text"; readonly text: string } + | { readonly type: "file"; readonly data: string; readonly mime: string; readonly name?: string } + +/** Model-facing tool content: plain text or non-empty rich content. */ +export type ModelOutput = string | readonly [Content, ...Content[]] + +type BaseTool> = { + readonly description: string + readonly input: Input +} + +export type Response> = { + readonly output: OutputValue + readonly content?: ModelOutput + readonly metadata?: Metadata +} + +export type ContentResponse = { + readonly content: ModelOutput + readonly metadata?: Metadata +} + +export type Tool< + Input extends SchemaType, + Output extends SchemaType | undefined = undefined, +> = BaseTool & + (Output extends SchemaType + ? { + readonly output: Output + readonly execute: (input: InputValue, context: Context) => Effect.Effect, Failure> + } + : { + readonly output?: undefined + readonly execute: (input: InputValue, context: Context) => Effect.Effect + }) + +export type Any = BaseTool & { + readonly output?: SchemaType + readonly execute: (input: any, context: Context) => Effect.Effect | ContentResponse, Failure> +} + +export function make, Output extends SchemaType>( + config: Tool, +): Tool +export function make>(config: Tool): Tool +export function make(config: Any): Any +export function make(config: Any): Any { + return config +} + +// Registration + +export interface RegisterOptions { + readonly namespace?: string + /** Defaults to true. False exposes the tool directly to the provider. */ + readonly codemode?: boolean + /** Permission action used for whole-tool visibility filtering. */ + readonly permission?: string +} + +export interface Registration { + readonly tool: Any + readonly name: string + readonly namespace?: string + readonly permission: string +} + +export const validateName = (name: string) => + /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(name) + ? Effect.void + : Effect.fail(new RegistrationError({ name, message: `Invalid tool name: ${name}` })) + +export const registrationEntries = ( + tools: Readonly>, + options?: RegisterOptions, +): Array => + Object.entries(tools).map(([name, tool]) => { + const normalized = name.replace(/[^a-zA-Z0-9_-]/g, "_") + const key = + options?.namespace === undefined ? normalized : `${options.namespace.replaceAll(".", "_")}_${normalized}` + return { + key, + name: normalized, + namespace: options?.namespace, + tool, + permission: options?.permission ?? key, + } + }) + +export const validateNamespace = (namespace: string) => + namespace.split(".").every((segment) => /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(segment)) + ? Effect.void + : Effect.fail( + new RegistrationError({ name: namespace, message: `Invalid tool namespace: ${JSON.stringify(namespace)}` }), + ) + +export const toLLMDefinition = (name: string, tool: Any): ToolDefinition => ({ + name, + description: tool.description, + inputSchema: inputJsonSchema(tool.input), + ...(tool.output === undefined ? {} : { outputSchema: outputJsonSchema(tool.output) }), +}) + +// Schema interpretation + +export function decodeInput(schema: SchemaType, value: unknown): Effect.Effect { + if (Schema.isSchema(schema)) + return Schema.decodeUnknownEffect(schema)(value).pipe( + Effect.mapError((error) => new Failure({ message: `Invalid tool input: ${error.message}` })), + ) + if (isStandardSchema(schema)) return validateStandard(schema, value, "Invalid tool input") + return Effect.succeed(value) +} + +export function encodeOutput(schema: SchemaType, value: unknown): Effect.Effect { + if (Schema.isSchema(schema)) + return Schema.encodeEffect(schema)(value).pipe( + Effect.mapError( + (error) => new Failure({ message: `Tool returned an invalid value for its output schema: ${error.message}` }), + ), + ) + if (isStandardSchema(schema)) + return validateStandard(schema, value, "Tool returned an invalid value for its output schema") + return Schema.decodeUnknownEffect(Schema.Json)(value).pipe( + Effect.mapError( + (error) => new Failure({ message: `Tool returned a non-JSON value for its output schema: ${error.message}` }), + ), + ) +} + +function isStandardSchema(schema: SchemaType): schema is StandardSchemaType { + return "~standard" in schema +} + +function validateStandard(schema: StandardSchemaType, value: unknown, prefix: string): Effect.Effect { + return Effect.gen(function* () { + const pending = yield* Effect.try({ + try: () => schema["~standard"].validate(value), + catch: (error) => standardFailure(prefix, error), + }) + const result = + pending instanceof Promise + ? yield* Effect.tryPromise({ try: () => pending, catch: (error) => standardFailure(prefix, error) }) + : pending + if (result.issues) + return yield* Effect.fail( + new Failure({ message: `${prefix}: ${result.issues.map((issue) => issue.message).join(", ")}` }), + ) + return result.value + }) +} + +function standardFailure(prefix: string, error: unknown) { + return new Failure({ message: `${prefix}: ${error instanceof Error ? error.message : String(error)}` }) +} + +function inputJsonSchema(schema: SchemaType): JsonSchema.JsonSchema { + if (isStandardSchema(schema)) + return schema["~standard"].jsonSchema.input({ target: "draft-2020-12" }) as JsonSchema.JsonSchema + return Schema.isSchema(schema) ? toJsonSchema(schema) : (schema as JsonSchema.JsonSchema) +} + +function outputJsonSchema(schema: SchemaType): JsonSchema.JsonSchema { + if (isStandardSchema(schema)) + return schema["~standard"].jsonSchema.output({ target: "draft-2020-12" }) as JsonSchema.JsonSchema + return Schema.isSchema(schema) ? toJsonSchema(schema) : (schema as JsonSchema.JsonSchema) +} + +function toJsonSchema(schema: Schema.Top): JsonSchema.JsonSchema { + const document = Schema.toJsonSchemaDocument(schema) + if (Object.keys(document.definitions).length === 0) return document.schema + return { ...document.schema, $defs: document.definitions } +} + +// Plugin events + +export interface ToolExecuteBeforeEvent { + readonly tool: string + readonly sessionID: Session.ID + readonly agent: Agent.ID + readonly messageID: SessionMessage.ID + readonly callID: string + input: unknown +} + +type ToolHookBase = { + readonly tool: string + readonly sessionID: Session.ID + readonly agent: Agent.ID + readonly messageID: SessionMessage.ID + readonly callID: string + readonly input: unknown +} + +export const ExecuteAfterOutcome = Schema.Union([ + Schema.Struct({ + status: Schema.Literal("completed"), + content: Schema.NonEmptyArray(LLM.ToolContent), + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Json)), + outputPaths: Schema.optional(Schema.Array(Schema.String)), + }), + Schema.Struct({ + status: Schema.Literal("error"), + error: SessionError.Error, + content: Schema.optional(Schema.NonEmptyArray(LLM.ToolContent)), + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Json)), + outputPaths: Schema.optional(Schema.Array(Schema.String)), + }), +]).pipe(Schema.toTaggedUnion("status")) + +type Mutable = { -readonly [K in keyof A]: A[K] } +type HookOutcome = Omit, "status"> & Pick + +/** The bounded terminal outcome exposed to tool hooks. */ +export type Outcome = typeof ExecuteAfterOutcome.Type extends infer A + ? A extends { readonly status: string } + ? HookOutcome + : never + : never + +/** + * The canonical execution outcome as seen by `execute.after` hooks. Hooks + * observe bounded model content, optional metadata, and managed output paths; + * they never observe the raw domain output. + */ +export type ToolExecuteAfterEvent = ToolHookBase & Outcome + +export interface ToolDraft { + add(name: string, tool: Any, options?: RegisterOptions): void +} + +export interface ToolHooks { + readonly "execute.before": ToolExecuteBeforeEvent + readonly "execute.after": ToolExecuteAfterEvent +} + +export interface ToolDomain { + readonly transform: Transform + readonly hook: Hooks +} diff --git a/packages/plugin/src/v2/effect/tool.ts b/packages/plugin/src/v2/effect/tool.ts index 879e5badef2e..cbc634f8e5b3 100644 --- a/packages/plugin/src/v2/effect/tool.ts +++ b/packages/plugin/src/v2/effect/tool.ts @@ -1,314 +1,2 @@ -export * as Tool from "./tool.js" - -import { Agent } from "@opencode-ai/schema/agent" -import type { LLM } from "@opencode-ai/schema/llm" -import { Session } from "@opencode-ai/schema/session" -import { SessionMessage } from "@opencode-ai/schema/session-message" -import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec" -import { Effect, JsonSchema, Schema } from "effect" -import type { Hooks, Transform } from "./registration.js" - -export interface Context { - readonly sessionID: Session.ID - readonly agent: Agent.ID - readonly messageID: SessionMessage.ID - readonly callID: string - readonly progress: (update: Progress) => Effect.Effect -} - -export interface Progress { - readonly structured: Readonly> - readonly content?: ReadonlyArray -} - -export type StandardSchemaType = StandardSchemaV1 & - StandardJSONSchemaV1 -export type SchemaType = Schema.Codec | StandardSchemaType -type IsAny = 0 extends 1 & A ? true : false -export type InputValue = - IsAny extends true - ? any - : S extends Schema.Codec - ? A - : S extends StandardSchemaV1 - ? A - : never -export type OutputValue = - IsAny extends true - ? any - : S extends Schema.Codec - ? A - : S extends StandardSchemaV1 - ? A - : never -export type EncodedValue = - IsAny extends true - ? any - : S extends Schema.Codec - ? A - : S extends StandardSchemaV1 - ? A - : never - -type ToolDefinition = { - readonly name: string - readonly description: string - readonly inputSchema: JsonSchema.JsonSchema - readonly outputSchema?: JsonSchema.JsonSchema -} - -type ToolCall = { - readonly input: unknown - readonly [key: string]: unknown -} - -type ToolResultValue = - | { readonly type: "json"; readonly value: unknown } - | { readonly type: "text"; readonly value: unknown } - | { readonly type: "error"; readonly value: unknown } - | { readonly type: "content"; readonly value: ReadonlyArray } - -type ToolOutput = { - readonly structured: unknown - readonly content: ReadonlyArray -} - -export class Failure extends Schema.TaggedErrorClass()("LLM.ToolFailure", { - message: Schema.String, - error: Schema.optional(Schema.Defect()), - metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), -}) {} - -export class RegistrationError extends Schema.TaggedErrorClass()("Tool.RegistrationError", { - name: Schema.String, - message: Schema.String, -}) {} - -export type Content = - | { readonly type: "text"; readonly text: string } - | { readonly type: "file"; readonly data: string; readonly mime: string; readonly name?: string } - -export type Definition< - Input extends SchemaType, - Structured extends SchemaType, - Output extends SchemaType = any, -> = { - readonly description: string - readonly input: Input - readonly output: Output - readonly structured?: Structured - readonly permission?: string - readonly toStructuredOutput?: (input: { - readonly input: InputValue - readonly output: EncodedValue - }) => OutputValue - readonly execute: (input: InputValue, context: Context) => Effect.Effect, Failure> - readonly toModelOutput?: (input: { - readonly input: InputValue - readonly output: EncodedValue - }) => ReadonlyArray -} - -export type DynamicOutput = { - readonly structured: unknown - readonly content: ReadonlyArray -} - -/** - * Config for a tool whose input shape is a raw JSON Schema not known at compile - * time (MCP servers, plugin manifests). Input is passed through as `unknown`; - * `execute` returns the already-projected structured value and model content. - */ -export type DynamicDefinition = { - readonly description: string - readonly jsonSchema: JsonSchema.JsonSchema - readonly outputSchema?: JsonSchema.JsonSchema - readonly permission?: string - readonly execute: (input: unknown, context: Context) => Effect.Effect -} - -export type AnyTool = Definition | DynamicDefinition - -export function make< - Input extends SchemaType, - Output extends SchemaType, - Structured extends SchemaType = Output, ->(config: Definition): Definition -export function make(config: DynamicDefinition): DynamicDefinition -export function make(config: AnyTool): AnyTool -export function make(config: AnyTool): AnyTool { - return config -} - -function toModelContent(part: Content) { - if (part.type === "text") return { type: "text" as const, text: part.text } - return { type: "file" as const, uri: `data:${part.mime};base64,${part.data}`, mime: part.mime, name: part.name } -} - -export const validateName = (name: string) => - /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(name) - ? Effect.void - : Effect.fail(new RegistrationError({ name, message: `Invalid tool name: ${name}` })) - -export const registrationEntries = (tools: Readonly>, namespace?: string) => - Object.entries(tools).map(([name, tool]) => { - const normalized = name.replace(/[^a-zA-Z0-9_-]/g, "_") - return { - key: namespace === undefined ? normalized : `${namespace.replaceAll(".", "_")}_${normalized}`, - name: normalized, - namespace, - tool, - } - }) - -export const validateNamespace = (namespace: string) => - namespace.split(".").every((segment) => /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(segment)) - ? Effect.void - : Effect.fail( - new RegistrationError({ name: namespace, message: `Invalid tool namespace: ${JSON.stringify(namespace)}` }), - ) - -export const withPermission = ( - tool: T, - permission: string, -): Omit & { - readonly permission: string -} => ({ ...tool, permission }) - -export const permission = (tool: AnyTool, name: string) => tool.permission ?? name - -export const definition = (name: string, tool: AnyTool): ToolDefinition => - "jsonSchema" in tool - ? { - name, - description: tool.description, - inputSchema: tool.jsonSchema, - outputSchema: tool.outputSchema, - } - : { - name, - description: tool.description, - inputSchema: inputJsonSchema(tool.input), - outputSchema: outputJsonSchema(tool.structured ?? tool.output), - } - -export const settle = (tool: AnyTool, call: ToolCall, context: Context): Effect.Effect => - Effect.gen(function* () { - if ("jsonSchema" in tool) { - const output = yield* tool.execute(call.input, context) - return { structured: output.structured, content: output.content.map(toModelContent) } - } - - const input = yield* decodeInput(tool.input, call.input) - const value = yield* tool.execute(input, context) - const output = yield* encodeOutput(tool.output, value) - const structured = - tool.structured && tool.toStructuredOutput - ? yield* encodeOutput(tool.structured, tool.toStructuredOutput({ input, output })) - : output - return { - structured, - content: - tool.toModelOutput?.({ input, output }).map(toModelContent) ?? - (typeof output === "string" ? [{ type: "text" as const, text: output }] : []), - } - }) - -function decodeInput(schema: SchemaType, value: unknown): Effect.Effect { - if (Schema.isSchema(schema)) - return Schema.decodeUnknownEffect(schema)(value).pipe( - Effect.mapError((error) => new Failure({ message: `Invalid tool input: ${error.message}` })), - ) - return validateStandard(schema, value, "Invalid tool input") -} - -function encodeOutput(schema: SchemaType, value: unknown): Effect.Effect { - if (Schema.isSchema(schema)) - return Schema.encodeEffect(schema)(value).pipe( - Effect.mapError( - (error) => new Failure({ message: `Tool returned an invalid value for its output schema: ${error.message}` }), - ), - ) - return validateStandard(schema, value, "Tool returned an invalid value for its output schema") -} - -function validateStandard(schema: StandardSchemaType, value: unknown, prefix: string): Effect.Effect { - return Effect.gen(function* () { - const pending = yield* Effect.try({ - try: () => schema["~standard"].validate(value), - catch: (error) => standardFailure(prefix, error), - }) - const result = - pending instanceof Promise - ? yield* Effect.tryPromise({ try: () => pending, catch: (error) => standardFailure(prefix, error) }) - : pending - if (result.issues) - return yield* Effect.fail( - new Failure({ message: `${prefix}: ${result.issues.map((issue) => issue.message).join(", ")}` }), - ) - return result.value - }) -} - -function standardFailure(prefix: string, error: unknown) { - return new Failure({ message: `${prefix}: ${error instanceof Error ? error.message : String(error)}` }) -} - -function inputJsonSchema(schema: SchemaType): JsonSchema.JsonSchema { - if (!Schema.isSchema(schema)) - return schema["~standard"].jsonSchema.input({ target: "draft-2020-12" }) as JsonSchema.JsonSchema - return toJsonSchema(schema) -} - -function outputJsonSchema(schema: SchemaType): JsonSchema.JsonSchema { - if (!Schema.isSchema(schema)) - return schema["~standard"].jsonSchema.output({ target: "draft-2020-12" }) as JsonSchema.JsonSchema - return toJsonSchema(schema) -} - -function toJsonSchema(schema: Schema.Top): JsonSchema.JsonSchema { - const document = Schema.toJsonSchemaDocument(schema) - if (Object.keys(document.definitions).length === 0) return document.schema - return { ...document.schema, $defs: document.definitions } -} - -export interface ToolExecuteBeforeEvent { - readonly tool: string - readonly sessionID: Session.ID - readonly agent: Agent.ID - readonly messageID: SessionMessage.ID - readonly callID: string - input: unknown -} - -export interface ToolExecuteAfterEvent { - readonly tool: string - readonly sessionID: Session.ID - readonly agent: Agent.ID - readonly messageID: SessionMessage.ID - readonly callID: string - readonly input: unknown - result: ToolResultValue - output?: ToolOutput - outputPaths?: ReadonlyArray -} - -export interface RegisterOptions { - readonly namespace?: string - /** Defaults to true. False exposes the tool directly to the provider. */ - readonly codemode?: boolean -} - -export interface ToolDraft { - add(name: string, tool: AnyTool, options?: RegisterOptions): void -} - -export interface ToolHooks { - readonly "execute.before": ToolExecuteBeforeEvent - readonly "execute.after": ToolExecuteAfterEvent -} - -export interface ToolDomain { - readonly transform: Transform - readonly hook: Hooks -} +export * as Tool from "./internal/tool.js" +export * from "./internal/tool.js" diff --git a/packages/plugin/src/v2/promise/README.md b/packages/plugin/src/v2/promise/README.md index e23a231e4fa3..c7ffbfd251c4 100644 --- a/packages/plugin/src/v2/promise/README.md +++ b/packages/plugin/src/v2/promise/README.md @@ -94,19 +94,23 @@ await ctx.session.hook("context", (event) => { }) ``` -Promise tools use plain object declarations with async executors: +Promise tools use executable tool values with async executors. Registration +supplies the tool's name and options separately: ```ts import { Schema } from "effect" +import { Tool } from "@opencode-ai/plugin/v2/tool" await ctx.tool.transform((tools) => { - tools.add({ - name: "echo", - description: "Echo text", - input: Schema.Struct({ text: Schema.String }), - output: Schema.Struct({ text: Schema.String }), - execute: async ({ text }) => ({ text }), - }) + tools.add( + "echo", + Tool.make({ + description: "Echo text", + input: Schema.Struct({ text: Schema.String }), + output: Schema.Struct({ text: Schema.String }), + execute: async ({ text }) => ({ output: { text }, content: text }), + }), + ) }) ``` diff --git a/packages/plugin/src/v2/promise/internal/tool.ts b/packages/plugin/src/v2/promise/internal/tool.ts new file mode 100644 index 000000000000..3d5def019baa --- /dev/null +++ b/packages/plugin/src/v2/promise/internal/tool.ts @@ -0,0 +1,64 @@ +import type { Hooks, Transform } from "../registration.js" + +export type Context = Omit & { + readonly progress: (update: import("../../effect/internal/tool.js").Progress) => Promise +} +export type SchemaType = import("../../effect/internal/tool.js").SchemaType +export type Content = import("../../effect/internal/tool.js").Content +export type Metadata = import("../../effect/internal/tool.js").Metadata +export type ModelOutput = import("../../effect/internal/tool.js").ModelOutput + +export type Tool, Output extends SchemaType | undefined = undefined> = Omit< + import("../../effect/internal/tool.js").Tool, + "execute" +> & { + readonly execute: ( + input: import("../../effect/internal/tool.js").InputValue, + context: Context, + ) => Promise< + Output extends SchemaType + ? import("../../effect/internal/tool.js").Response + : import("../../effect/internal/tool.js").ContentResponse + > +} + +export type Any = Omit & { + readonly execute: ( + input: any, + context: Context, + ) => Promise< + import("../../effect/internal/tool.js").Response | import("../../effect/internal/tool.js").ContentResponse + > +} + +export function make, Output extends SchemaType>( + tool: Tool, +): Tool +export function make>(tool: Tool): Tool +export function make(tool: Any): Any +export function make(tool: Any): Any { + return tool +} + +export type ToolExecuteBeforeEvent = import("../../effect/internal/tool.js").ToolExecuteBeforeEvent +export type ToolExecuteAfterEvent = import("../../effect/internal/tool.js").ToolExecuteAfterEvent +export type RegisterOptions = import("../../effect/internal/tool.js").RegisterOptions + +export interface ToolDraft { + add, Output extends SchemaType>( + name: string, + tool: Tool, + options?: RegisterOptions, + ): void + add>(name: string, tool: Tool, options?: RegisterOptions): void +} + +export interface ToolHooks { + readonly "execute.before": ToolExecuteBeforeEvent + readonly "execute.after": ToolExecuteAfterEvent +} + +export interface ToolDomain { + readonly transform: Transform + readonly hook: Hooks +} diff --git a/packages/plugin/src/v2/promise/tool.ts b/packages/plugin/src/v2/promise/tool.ts index 91b9b2c1e286..cbc634f8e5b3 100644 --- a/packages/plugin/src/v2/promise/tool.ts +++ b/packages/plugin/src/v2/promise/tool.ts @@ -1,48 +1,2 @@ -import type { Tool } from "../effect/tool.js" -import type { Hooks, Transform } from "./registration.js" - -export type Context = Omit & { - readonly progress: (update: Tool.Progress) => Promise -} -export type SchemaType = Tool.SchemaType -export type Content = Tool.Content -export type DynamicOutput = Tool.DynamicOutput - -export type Definition< - Input extends SchemaType, - Output extends SchemaType, - Structured extends SchemaType = Output, -> = Omit, "execute" | "permission"> & { - readonly name: string - readonly options?: RegisterOptions - readonly execute: (input: Tool.InputValue, context: Context) => Promise> -} - -export type DynamicDefinition = Omit & { - readonly name: string - readonly options?: RegisterOptions - readonly execute: (input: unknown, context: Context) => Promise -} - -export type AnyTool = Definition | DynamicDefinition - -export type ToolExecuteBeforeEvent = Tool.ToolExecuteBeforeEvent -export type ToolExecuteAfterEvent = Tool.ToolExecuteAfterEvent -export type RegisterOptions = Tool.RegisterOptions - -export interface ToolDraft { - add, Output extends SchemaType, Structured extends SchemaType = Output>( - tool: Definition, - ): void - add(tool: DynamicDefinition): void -} - -export interface ToolHooks { - readonly "execute.before": ToolExecuteBeforeEvent - readonly "execute.after": ToolExecuteAfterEvent -} - -export interface ToolDomain { - readonly transform: Transform - readonly hook: Hooks -} +export * as Tool from "./internal/tool.js" +export * from "./internal/tool.js" diff --git a/packages/plugin/test/tool.test.ts b/packages/plugin/test/tool.test.ts index c22ddbec9a71..19017cab20e1 100644 --- a/packages/plugin/test/tool.test.ts +++ b/packages/plugin/test/tool.test.ts @@ -1,29 +1,18 @@ import { expect, test } from "bun:test" -import { Agent } from "@opencode-ai/schema/agent" -import { Session } from "@opencode-ai/schema/session" -import { SessionMessage } from "@opencode-ai/schema/session-message" import { Effect, Schema } from "effect" import * as Tool from "../src/v2/effect/tool" -const context = { - sessionID: Session.ID.make("ses_test"), - agent: Agent.ID.make("build"), - messageID: SessionMessage.ID.make("msg_test"), - callID: "call_test", - progress: () => Effect.void, -} satisfies Tool.Context - test("tools remain valid across separate module instances", async () => { const ForeignTool = await import(`${new URL("../src/v2/effect/tool.ts", import.meta.url).href}?foreign`) const config = { description: "Foreign tool", input: Schema.Struct({ value: Schema.String }), output: Schema.Struct({ ok: Schema.Boolean }), - execute: () => Effect.succeed({ ok: true }), + execute: () => Effect.succeed({ output: { ok: true } }), } const tool = ForeignTool.make(config) - expect(Tool.definition("foreign", tool)).toEqual({ + expect(Tool.toLLMDefinition("foreign", tool)).toEqual({ name: "foreign", description: "Foreign tool", inputSchema: { @@ -39,10 +28,7 @@ test("tools remain valid across separate module instances", async () => { additionalProperties: false, }, }) - expect(await Effect.runPromise(Tool.settle(tool, { input: { value: "input" } }, context))).toEqual({ - structured: { ok: true }, - content: [], - }) + expect(await Effect.runPromise(Tool.decodeInput(tool.input, { value: "input" }))).toEqual({ value: "input" }) }) test("portable schemas validate and describe typed tools", async () => { @@ -77,19 +63,18 @@ test("portable schemas validate and describe typed tools", async () => { description: "Portable tool", input, output, - execute: ({ count }) => Effect.succeed(count + 1), + execute: ({ count }) => Effect.succeed({ output: count + 1 }), }) - expect(Tool.definition("portable", tool)).toEqual({ + expect(Tool.toLLMDefinition("portable", tool)).toEqual({ name: "portable", description: "Portable tool", inputSchema: { type: "object", properties: { count: { type: "string" } } }, outputSchema: { type: "string" }, }) - expect(await Effect.runPromise(Tool.settle(tool, { input: { count: "41" } }, context))).toEqual({ - structured: "42", - content: [{ type: "text", text: "42" }], - }) + const decoded = await Effect.runPromise(Tool.decodeInput(tool.input, { count: "41" })) + expect(decoded).toEqual({ count: 41 }) + expect(await Effect.runPromise(Tool.encodeOutput(tool.output, 42))).toBe("42") }) test("portable schema failures become tool failures", async () => { @@ -104,29 +89,39 @@ test("portable schema failures become tool failures", async () => { }, }, } - const tool = Tool.make({ - description: "Failing tool", - input, - output: input, - execute: Effect.succeed, - }) - const error = await Effect.runPromiseExit(Tool.settle(tool, { input: 1 }, context)) + const error = await Effect.runPromiseExit(Tool.decodeInput(input, 1)) expect(error.toString()).toContain("Invalid tool input: expected a string") }) -test("two-parameter Definition annotations retain their original meaning", () => { +test("canonical results carry metadata with typed output", async () => { const input = Schema.Struct({ value: Schema.String }) const output = Schema.Struct({ value: Schema.String, internal: Schema.Boolean }) - const structured = Schema.Struct({ value: Schema.String }) - const tool: Tool.Definition = Tool.make({ + const tool = Tool.make({ description: "Annotated tool", input, output, - structured, - toStructuredOutput: ({ output }) => ({ value: output.value }), - execute: ({ value }) => Effect.succeed({ value, internal: true }), + execute: ({ value }) => Effect.succeed({ output: { value, internal: true }, metadata: { value }, content: value }), }) - expect(tool.structured).toBe(structured) + expect(await Effect.runPromise(tool.execute({ value: "out" }, {} as Tool.Context))).toEqual({ + output: { value: "out", internal: true }, + metadata: { value: "out" }, + content: "out", + }) +}) + +test("raw JSON schemas are render-only and omitted output means model-only", async () => { + const tool = Tool.make({ + description: "Raw tool", + input: { type: "object", properties: { value: { type: "string" } } }, + execute: (input) => Effect.succeed({ content: JSON.stringify(input) }), + }) + + expect(Tool.toLLMDefinition("raw", tool)).toEqual({ + name: "raw", + description: "Raw tool", + inputSchema: { type: "object", properties: { value: { type: "string" } } }, + }) + expect(await Effect.runPromise(Tool.decodeInput(tool.input, { value: 1 }))).toEqual({ value: 1 }) }) diff --git a/packages/schema/src/session-event.ts b/packages/schema/src/session-event.ts index cc56a83eab94..faa4853adedc 100644 --- a/packages/schema/src/session-event.ts +++ b/packages/schema/src/session-event.ts @@ -409,40 +409,49 @@ export namespace Tool { }) export type Called = typeof Called.Type - /** Live replacement snapshot for a running tool. */ + /** Live replacement metadata for a running tool. */ export const Progress = Event.ephemeral({ type: "session.tool.progress", schema: { ...ToolBase, - structured: Schema.Record(Schema.String, Schema.Unknown), - content: Schema.Array(ToolContent), + metadata: Schema.Record(Schema.String, Schema.Json), }, }) export type Progress = typeof Progress.Type + /** Canonical terminal success: one non-empty model representation plus optional UI metadata. */ export const Success = Event.durable({ type: "session.tool.success", - ...options, + durable: { + aggregate: "sessionID", + version: 2, + }, schema: { ...ToolBase, - structured: Schema.Record(Schema.String, Schema.Unknown), - content: Schema.Array(ToolContent), - result: Schema.Unknown.pipe(optional), + content: Schema.NonEmptyArray(ToolContent), + metadata: Schema.Record(Schema.String, Schema.Json).pipe(optional), executed: Schema.Boolean, resultState: SessionMessage.ProviderState.pipe(optional), }, }) export type Success = typeof Success.Type + /** + * Canonical terminal failure: one error plus the final bounded snapshot of + * partial progress. The event is self-contained; projection never reaches + * into ephemeral progress history. + */ export const Failed = Event.durable({ type: "session.tool.failed", - ...options, + durable: { + aggregate: "sessionID", + version: 2, + }, schema: { ...ToolBase, error: SessionError.Error, content: Schema.NonEmptyArray(ToolContent).pipe(optional), - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(optional), - result: Schema.Unknown.pipe(optional), + metadata: Schema.Record(Schema.String, Schema.Json).pipe(optional), executed: Schema.Boolean, resultState: SessionMessage.ProviderState.pipe(optional), }, diff --git a/packages/schema/src/session-message.ts b/packages/schema/src/session-message.ts index 3d45e283e602..c30ffc6aa465 100644 --- a/packages/schema/src/session-message.ts +++ b/packages/schema/src/session-message.ts @@ -110,27 +110,24 @@ export interface ToolStateRunning extends Schema.Schema.Type {} export const ToolStateCompleted = Schema.Struct({ status: Schema.tag("completed"), input: Schema.Record(Schema.String, Schema.Unknown), - content: ToolContent.pipe(Schema.Array), - structured: Schema.Record(Schema.String, Schema.Unknown), - result: Schema.Unknown.pipe(optional), + content: Schema.NonEmptyArray(ToolContent), + metadata: Schema.Record(Schema.String, Schema.Json).pipe(optional), }).annotate({ identifier: "Session.Message.ToolState.Completed" }) export interface ToolStateError extends Schema.Schema.Type {} export const ToolStateError = Schema.Struct({ status: Schema.tag("error"), input: Schema.Record(Schema.String, Schema.Unknown), - content: ToolContent.pipe(Schema.Array), - structured: Schema.Record(Schema.String, Schema.Unknown), error: SessionError.Error, - result: Schema.Unknown.pipe(optional), + content: Schema.NonEmptyArray(ToolContent).pipe(optional), + metadata: Schema.Record(Schema.String, Schema.Json).pipe(optional), }).annotate({ identifier: "Session.Message.ToolState.Error" }) export const ToolState = Schema.Union([ToolStateStreaming, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe( diff --git a/packages/schema/test/event-manifest.test.ts b/packages/schema/test/event-manifest.test.ts index 89766d3d262e..bd08c653a5c7 100644 --- a/packages/schema/test/event-manifest.test.ts +++ b/packages/schema/test/event-manifest.test.ts @@ -125,8 +125,8 @@ describe("public event manifest", () => { "session.tool.input.started.1", "session.tool.input.ended.1", "session.tool.called.1", - "session.tool.success.1", - "session.tool.failed.1", + "session.tool.success.2", + "session.tool.failed.2", "session.reasoning.started.1", "session.reasoning.ended.1", "session.retry.scheduled.1", diff --git a/packages/sdk-next/src/tool.ts b/packages/sdk-next/src/tool.ts index eff4b809aa3c..978ea6d0cbf1 100644 --- a/packages/sdk-next/src/tool.ts +++ b/packages/sdk-next/src/tool.ts @@ -1,2 +1,2 @@ export { Failure, RegistrationError, make } from "@opencode-ai/plugin/v2/effect/tool" -export type { AnyTool, Content, Context, Definition } from "@opencode-ai/plugin/v2/effect/tool" +export type { Any, Content, Context, Tool } from "@opencode-ai/plugin/v2/effect/tool" diff --git a/packages/sdk-next/test/embedded.test.ts b/packages/sdk-next/test/embedded.test.ts index 09980bd8cbbd..68b8ef8e6cea 100644 --- a/packages/sdk-next/test/embedded.test.ts +++ b/packages/sdk-next/test/embedded.test.ts @@ -77,7 +77,7 @@ it.live( description: "Marks the initial Location plugin generation", input: Schema.Struct({}), output: Schema.Void, - execute: () => Effect.void, + execute: () => Effect.succeed({ output: undefined }), }), ), ) @@ -104,7 +104,7 @@ it.live( description: "Tool registered after Location boot", input: Schema.Struct({}), output: Schema.Void, - execute: () => Effect.void, + execute: () => Effect.succeed({ output: undefined }), }), ), ) @@ -225,7 +225,7 @@ it.live( description: "Embedded test tool", input: Schema.Struct({}), output: Schema.Struct({ ok: Schema.Boolean }), - execute: () => Effect.succeed({ ok: true }), + execute: () => Effect.succeed({ output: { ok: true } }), }), ), ) diff --git a/packages/simulation/src/backend/simulated-provider.ts b/packages/simulation/src/backend/simulated-provider.ts index ba64f21bca5d..3bb9014df8d6 100644 --- a/packages/simulation/src/backend/simulated-provider.ts +++ b/packages/simulation/src/backend/simulated-provider.ts @@ -10,6 +10,7 @@ import { Exit, Fiber, FiberSet, + JsonSchema, Layer, PubSub, Queue, @@ -451,7 +452,7 @@ const makeToolDriver = Effect.fn("SimulatedProvider.makeToolDriver")(function* ( name: string, input: unknown, context: Tool.Context, - ): Effect.Effect => + ): Effect.Effect, Tool.Failure> => Effect.gen(function* () { const encoded = yield* Schema.decodeUnknownEffect(Schema.Json)(input).pipe( Effect.mapError((error) => new Tool.Failure({ message: `Simulated tool input is not JSON: ${error.message}` })), @@ -518,7 +519,15 @@ const makeToolDriver = Effect.fn("SimulatedProvider.makeToolDriver")(function* ( ), ), ) - if (invocation.type === "success") return invocation.output + // The simulation wire protocol keeps its historical field names; map to the + // canonical output at this boundary. + if (invocation.type === "success") + return { + output: invocation.output.structured, + ...(invocation.output.content.length === 0 + ? {} + : { content: invocation.output.content as [Tool.Content, ...Tool.Content[]] }), + } return yield* Effect.fail(new Tool.Failure({ message: invocation.message })) }) @@ -546,11 +555,8 @@ const makeToolDriver = Effect.fn("SimulatedProvider.makeToolDriver")(function* ( registration.name, Tool.make({ description: registration.description, - jsonSchema: registration.inputSchema, - ...(registration.outputSchema === undefined - ? {} - : { outputSchema: registration.outputSchema }), - ...(registration.permission === undefined ? {} : { permission: registration.permission }), + input: registration.inputSchema, + output: registration.outputSchema ?? {}, execute: (input, context) => invoke( generation, @@ -559,7 +565,9 @@ const makeToolDriver = Effect.fn("SimulatedProvider.makeToolDriver")(function* ( context, ), }), - registration.options, + registration.permission === undefined + ? registration.options + : { ...registration.options, permission: registration.permission }, ) }) .pipe(Scope.provide(nextScope)), diff --git a/packages/simulation/src/protocol/index.ts b/packages/simulation/src/protocol/index.ts index d97b80868629..573baf1f8342 100644 --- a/packages/simulation/src/protocol/index.ts +++ b/packages/simulation/src/protocol/index.ts @@ -473,10 +473,7 @@ export namespace Backend { : `${registration.options.namespace.replaceAll(".", "_")}_${registration.name}` } - export const ToolProgress = Schema.Struct({ - structured: Schema.Record(Schema.String, Schema.Json), - content: Schema.optionalKey(Schema.Array(ToolContent)), - }) + export const ToolProgress = Schema.Record(Schema.String, Schema.Json) export interface ToolProgress extends Schema.Schema.Type {} export const ToolOutput = Schema.Struct({ diff --git a/packages/simulation/test/protocol.test.ts b/packages/simulation/test/protocol.test.ts index b293f3543987..34893914c6b1 100644 --- a/packages/simulation/test/protocol.test.ts +++ b/packages/simulation/test/protocol.test.ts @@ -127,7 +127,7 @@ test("decodes the simulated tool lifecycle", () => { params: { id: "tool_1", sequence: 0, - update: { structured: { phase: "searching" }, content: [{ type: "text", text: "Searching" }] }, + update: { phase: "searching" }, }, }), ).toMatchObject({ method: "tool.update" }) diff --git a/packages/simulation/test/simulated-provider.test.ts b/packages/simulation/test/simulated-provider.test.ts index 8aac0cd72375..82238bae0286 100644 --- a/packages/simulation/test/simulated-provider.test.ts +++ b/packages/simulation/test/simulated-provider.test.ts @@ -241,6 +241,7 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { additionalProperties: false, }, outputSchema: { type: "object" }, + permission: "simulate_lookup", options: { codemode: false }, } const locations = yield* LocationServiceMap.Service @@ -264,19 +265,22 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { ) expect(yield* Queue.take(messages)).toMatchObject({ id: 1, result: { attached: true } }) const registry = yield* ToolRegistry.Service - const materialized = yield* registry.materialize() - expect(materialized.definitions).toContainEqual( + const toolSet = yield* registry.snapshot() + expect(toolSet.definitions).toContainEqual( expect.objectContaining({ name: "lookup", description: "Look up a value" }), ) - const secondaryMaterialized = yield* ToolRegistry.Service.use((secondaryRegistry) => - secondaryRegistry.materialize(), + expect( + (yield* registry.snapshot([{ action: "simulate_lookup", resource: "*", effect: "deny" }])).definitions, + ).not.toContainEqual(expect.objectContaining({ name: "lookup" })) + const secondaryToolSet = yield* ToolRegistry.Service.use((secondaryRegistry) => + secondaryRegistry.snapshot(), ).pipe(Effect.provide(secondary)) - expect(secondaryMaterialized.definitions).toContainEqual( + expect(secondaryToolSet.definitions).toContainEqual( expect.objectContaining({ name: "lookup", description: "Look up a value" }), ) const progress: ToolRegistry.Progress[] = [] - const settle = (callID: string, query: string) => - materialized.settle({ + const executeCall = (callID: string, query: string) => + toolSet.execute({ sessionID: SessionV2.ID.make("ses_simulated_tools"), agent: AgentV2.ID.make("build"), messageID: SessionMessage.ID.make("msg_simulated_tools"), @@ -289,7 +293,7 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { }, }) - const successful = yield* settle("call_success", "answer").pipe(Effect.forkScoped) + const successful = yield* executeCall("call_success", "answer").pipe(Effect.forkScoped) const successInvocation = yield* takeToolInvocation(messages) expect(successInvocation.params).toMatchObject({ name: "lookup", @@ -309,10 +313,7 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { params: { id: successID, sequence: 0, - update: { - structured: { phase: "searching" }, - content: [{ type: "text", text: "Searching" }], - }, + update: { phase: "searching" }, }, }) socket.send(update) @@ -334,7 +335,7 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { params: { id: successID, sequence: 0, - update: { structured: { phase: "different" } }, + update: { phase: "different" }, }, }), ) @@ -350,7 +351,7 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { params: { id: successID, sequence: 2, - update: { structured: { phase: "skipped" } }, + update: { phase: "skipped" }, }, }), ) @@ -389,20 +390,13 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { ) expect(yield* Queue.take(messages)).toMatchObject({ id: 23, result: { ok: true } }) expect(yield* Fiber.join(successful)).toMatchObject({ - result: { type: "text", value: "42" }, - output: { - structured: { answer: 42 }, - content: [{ type: "text", text: "42" }], - }, + status: "completed", + output: { answer: 42 }, + content: [{ type: "text", text: "42" }], }) - expect(progress).toEqual([ - { - structured: { phase: "searching" }, - content: [{ type: "text", text: "Searching" }], - }, - ]) + expect(progress).toEqual([{ phase: "searching" }]) - const failed = yield* settle("call_failure", "missing").pipe(Effect.forkScoped) + const failed = yield* executeCall("call_failure", "missing").pipe(Effect.forkScoped) const failedInvocation = yield* takeToolInvocation(messages) const failedID = requireString(requireRecord(failedInvocation.params).id) socket.send( @@ -415,12 +409,13 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { ) expect(yield* Queue.take(messages)).toMatchObject({ id: 4, result: { ok: true } }) expect(yield* Fiber.join(failed)).toMatchObject({ - result: { type: "error", value: "lookup failed" }, + status: "error", + error: { message: "lookup failed" }, }) const concurrent = [ - yield* settle("call_first", "first").pipe(Effect.forkScoped), - yield* settle("call_second", "second").pipe(Effect.forkScoped), + yield* executeCall("call_first", "first").pipe(Effect.forkScoped), + yield* executeCall("call_second", "second").pipe(Effect.forkScoped), ] const invocations = [yield* takeToolInvocation(messages), yield* takeToolInvocation(messages)] const byCall = new Map( @@ -447,10 +442,18 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { ) expect(yield* Queue.take(messages)).toMatchObject({ id, result: { ok: true } }) } - expect((yield* Fiber.join(concurrent[0])).result).toEqual({ type: "text", value: "first result" }) - expect((yield* Fiber.join(concurrent[1])).result).toEqual({ type: "text", value: "second result" }) + expect(yield* Fiber.join(concurrent[0])).toMatchObject({ + status: "completed", + output: "first result", + content: [{ type: "text", text: "first result" }], + }) + expect(yield* Fiber.join(concurrent[1])).toMatchObject({ + status: "completed", + output: "second result", + content: [{ type: "text", text: "second result" }], + }) - const cancelled = yield* settle("call_cancelled", "slow").pipe(Effect.forkScoped) + const cancelled = yield* executeCall("call_cancelled", "slow").pipe(Effect.forkScoped) const cancelledInvocation = yield* takeToolInvocation(messages) const cancelledID = requireString(requireRecord(cancelledInvocation.params).id) yield* Fiber.interrupt(cancelled) @@ -474,13 +477,10 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { error: { message: expect.stringContaining("not found or already finished") }, }) - const replayed = yield* settle("call_replayed", "reconnect").pipe(Effect.forkScoped) + const replayed = yield* executeCall("call_replayed", "reconnect").pipe(Effect.forkScoped) const original = yield* takeToolInvocation(messages) const originalID = requireString(requireRecord(original.params).id) - const replayedProgress = { - structured: { phase: "before-reconnect" }, - content: [{ type: "text", text: "Still running" }], - } + const replayedProgress = { phase: "before-reconnect" } socket.send( JSON.stringify({ jsonrpc: "2.0", @@ -505,7 +505,7 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { error: { message: expect.stringContaining("already attached") }, }) yield* closeSocket(socket) - const disconnected = yield* registry.materialize() + const disconnected = yield* registry.snapshot() expect(disconnected.definitions).toContainEqual(expect.objectContaining({ name: "lookup" })) replacement.send( JSON.stringify({ @@ -547,7 +547,7 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { }), ) expect(yield* Queue.take(replacementMessages)).toMatchObject({ id: 26, result: { ok: true } }) - expect(progress.filter((update) => update.structured.phase === "before-reconnect")).toHaveLength(1) + expect(progress.filter((update) => update.phase === "before-reconnect")).toHaveLength(1) replacement.send( JSON.stringify({ jsonrpc: "2.0", @@ -563,12 +563,13 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { }), ) expect(yield* Queue.take(replacementMessages)).toMatchObject({ id: 10, result: { ok: true } }) - expect((yield* Fiber.join(replayed)).result).toEqual({ - type: "text", - value: "replayed result", + expect(yield* Fiber.join(replayed)).toMatchObject({ + status: "completed", + output: "replayed result", + content: [{ type: "text", text: "replayed result" }], }) - const preserved = yield* settle("call_preserved", "same generation").pipe(Effect.forkScoped) + const preserved = yield* executeCall("call_preserved", "same generation").pipe(Effect.forkScoped) const preservedInvocation = yield* takeToolInvocation(replacementMessages) const preservedID = requireString(requireRecord(preservedInvocation.params).id) replacement.send( @@ -583,7 +584,11 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { }), ) expect(yield* Queue.take(replacementMessages)).toMatchObject({ id: 27, result: { ok: true } }) - expect((yield* Fiber.join(preserved)).result).toEqual({ type: "text", value: "preserved" }) + expect(yield* Fiber.join(preserved)).toMatchObject({ + status: "completed", + output: "preserved", + content: [{ type: "text", text: "preserved" }], + }) const namespaced = [ { ...registration, name: "search", options: { namespace: "github", codemode: false } }, @@ -598,18 +603,18 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { }), ) expect(yield* Queue.take(replacementMessages)).toMatchObject({ id: 11, result: { attached: true } }) - const replaced = yield* registry.materialize() + const replaced = yield* registry.snapshot() const replacedNames = replaced.definitions.map((definition) => definition.name) expect(replacedNames).toEqual(expect.arrayContaining(["github_search", "web_search"])) expect(replacedNames).not.toContain("lookup") const secondaryReplaced = yield* ToolRegistry.Service.use((secondaryRegistry) => - secondaryRegistry.materialize(), + secondaryRegistry.snapshot(), ).pipe(Effect.provide(secondary)) const secondaryNames = secondaryReplaced.definitions.map((definition) => definition.name) expect(secondaryNames).toEqual(expect.arrayContaining(["github_search", "web_search"])) expect(secondaryNames).not.toContain("lookup") const routed = yield* replaced - .settle({ + .execute({ sessionID: SessionV2.ID.make("ses_simulated_tools"), agent: AgentV2.ID.make("build"), messageID: SessionMessage.ID.make("msg_simulated_tools"), @@ -636,9 +641,13 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { }), ) expect(yield* Queue.take(replacementMessages)).toMatchObject({ id: 12, result: { ok: true } }) - expect((yield* Fiber.join(routed)).result).toEqual({ type: "text", value: "routed" }) + expect(yield* Fiber.join(routed)).toMatchObject({ + status: "completed", + output: "routed", + content: [{ type: "text", text: "routed" }], + }) expect( - yield* materialized.settle({ + yield* toolSet.execute({ sessionID: SessionV2.ID.make("ses_simulated_tools"), agent: AgentV2.ID.make("build"), messageID: SessionMessage.ID.make("msg_simulated_tools"), @@ -650,7 +659,8 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { }, }), ).toMatchObject({ - result: { type: "error", value: expect.stringContaining("no longer active") }, + status: "error", + error: { message: expect.stringContaining("no longer active") }, }) expect(activations).toBe(2) }).pipe(Effect.provide(primary)) diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index a5a18ff077f1..8eb4a190022d 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -32,6 +32,7 @@ import type { Plugin } from "@opencode-ai/plugin/v2/tui" import { createStore, produce, reconcile } from "solid-js/store" import { createSimpleContext } from "./helper" import { useClient } from "./client" +import { nonEmptyToolContent } from "../util/tool-display" import { createEffect, createSignal, onCleanup } from "solid-js" export type DataSessionStatus = "idle" | "running" @@ -605,7 +606,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ match.time.ran = event.created match.executed = event.data.executed match.providerState = event.data.state - match.state = { status: "running", input: event.data.input, structured: {}, content: [] } + match.state = { status: "running", input: event.data.input, metadata: {} } }) break case "session.tool.progress": @@ -615,8 +616,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ event.data.callID, ) if (match?.state.status !== "running") return - match.state.structured = event.data.structured - match.state.content = [...event.data.content] + match.state.metadata = event.data.metadata }) break case "session.tool.success": @@ -629,9 +629,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ match.state = { status: "completed", input: match.state.input, - structured: event.data.structured, + metadata: event.data.metadata, content: [...event.data.content], - result: event.data.result, } match.executed = event.data.executed || match.executed === true match.providerResultState = event.data.resultState @@ -649,9 +648,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ status: "error", error: event.data.error, input: typeof match.state.input === "string" ? {} : match.state.input, - structured: event.data.metadata ?? (match.state.status === "running" ? match.state.structured : {}), - content: event.data.content ?? (match.state.status === "running" ? match.state.content : []), - result: event.data.result, + metadata: event.data.metadata, + content: event.data.content, } match.executed = event.data.executed || match.executed === true match.providerResultState = event.data.resultState diff --git a/packages/tui/src/mini/demo.ts b/packages/tui/src/mini/demo.ts index a16ae991ac28..dcf6bf353721 100644 --- a/packages/tui/src/mini/demo.ts +++ b/packages/tui/src/mini/demo.ts @@ -342,13 +342,13 @@ function make(state: State, tool: string, input: Record): Ref } } -function startTool(state: State, ref: Ref, structured: Record = {}): SessionMessageAssistantTool { +function startTool(state: State, ref: Ref, metadata: Record = {}): SessionMessageAssistantTool { state.started.add(ref.call) const part = { type: "tool" as const, id: ref.call, name: ref.tool, - state: { status: "running" as const, input: ref.input, structured, content: [] }, + state: { status: "running" as const, input: ref.input, metadata }, time: { created: ref.start, ran: ref.start }, } present(state, [toolCommit(part, ref.msg, "start")]) @@ -395,8 +395,8 @@ function doneTool( state: { status: "completed", input: ref.input, - content: output.output ? [{ type: "text", text: output.output }] : [], - structured: output.metadata ?? {}, + content: [{ type: "text", text: output.output }], + metadata: output.metadata, }, time: { created: ref.start, ran: ref.start, completed: Date.now() }, } @@ -415,8 +415,6 @@ function failTool(state: State, ref: Ref, error: string): void { status: "error", input: ref.input, error: { type: "unknown", message: error }, - structured: {}, - content: [], }, time: { created: ref.start, ran: ref.start, completed: Date.now() }, }, @@ -527,8 +525,7 @@ function emitTask(state: State): void { offset: 1, limit: 200, }, - structured: {}, - content: [], + metadata: {}, }, time: { created: Date.now(), ran: Date.now() }, } satisfies SessionMessageAssistantTool diff --git a/packages/tui/src/mini/permission.shared.ts b/packages/tui/src/mini/permission.shared.ts index e293d3ef17b7..8294a5abe503 100644 --- a/packages/tui/src/mini/permission.shared.ts +++ b/packages/tui/src/mini/permission.shared.ts @@ -53,7 +53,7 @@ export function permissionInfo(request: MiniPermissionRequest, directory?: strin resources: request.resources, metadata: request.metadata, input: state?.status === "streaming" ? undefined : state?.input, - structured: state?.status === "streaming" ? undefined : state?.structured, + toolMetadata: state?.status === "streaming" ? undefined : state?.metadata, }, (value) => toolPath(value, { home: true, directory }), ) diff --git a/packages/tui/src/mini/stream-v2.subagent.ts b/packages/tui/src/mini/stream-v2.subagent.ts index ea3b566af43f..95e9719f8699 100644 --- a/packages/tui/src/mini/stream-v2.subagent.ts +++ b/packages/tui/src/mini/stream-v2.subagent.ts @@ -1,7 +1,7 @@ // Current-native subagent (child Session) tracking for the mini transport. // // Discovers child Sessions of the active parent from four current sources: -// 1. projected subagent tool output (`structured.sessionID`) during hydration +// 1. projected subagent tool output (`metadata.sessionID`) during hydration // 2. the current session list filtered by `parentID` during hydration // 3. the process-local active-session map during hydration // 4. live events from unknown sessions whose `parentID` matches the parent @@ -33,6 +33,7 @@ import type { StreamCommit, } from "./types" import { canonicalToolName, normalizeTool, toolOutputText, toolView } from "./tool" +import { toolDisplayContent } from "../util/tool-display" const CHILD_MESSAGE_LIMIT = 80 const CHILD_FRAME_LIMIT = 80 @@ -55,7 +56,7 @@ export function toolCommit( ): StreamCommit { const part = normalizeTool(input) const status = part.state.status - const output = status === "streaming" ? "" : toolOutputText(part.name, part.state.content) + const output = status === "streaming" ? "" : toolOutputText(part.name, toolDisplayContent(part.state)) const partial = status === "error" && phase === "progress" && value !== undefined const text = status === "running" || partial @@ -178,10 +179,10 @@ function blockerCategory(event: V2Event): "permission" | "form" | undefined { if (event.type === "form.created" || event.type === "form.replied" || event.type === "form.cancelled") return "form" } -function childSessionID(structured: Record | undefined) { - const sessionID = text(structured?.sessionID) +function childSessionID(metadata: Record | undefined) { + const sessionID = text(metadata?.sessionID) if (!sessionID || !sessionID.startsWith("ses")) return undefined - const status = structured?.status + const status = metadata?.status if (status !== "running" && status !== "completed") return undefined return { sessionID, running: status === "running" } } @@ -199,7 +200,7 @@ function tab(child: ChildState): FooterSubagentTab { export function createSubagentTracker(input: SubagentTrackerInput): SubagentTracker { const children = new Map() - // Live subagent tool calls in the parent, so tool.success structured output + // Live subagent tool calls in the parent, so tool.success metadata // can be joined with the call's input metadata. const pendingCalls = new Map>() // Recently resolved non-family sessions. Retention is bounded so unrelated @@ -309,7 +310,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac return } const current = child.tools.get(key) - const output = toolOutputText(part.name, part.state.content) + const output = toolOutputText(part.name, toolDisplayContent(part.state)) if (part.state.status === "running") { if (!current || current.part.state.status === "streaming") setFrame(child, frame, toolCommit(part, messageID, "start", undefined, input.directory)) @@ -779,7 +780,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac name: current?.part.name ?? "tool", executed: event.data.executed, providerState: event.data.state, - state: { status: "running", input: event.data.input, structured: {}, content: [] }, + state: { status: "running", input: event.data.input, metadata: {} }, time: { created: current?.part.time.created ?? event.created, ran: event.created }, }, event.data.assistantMessageID, @@ -804,8 +805,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac state: { status: "running", input: part && part.state.status !== "streaming" ? part.state.input : {}, - structured: event.data.structured, - content: event.data.content, + metadata: event.data.metadata, }, time: { created: part?.time.created ?? event.created, @@ -837,18 +837,15 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac ? { status: "error", input: part && part.state.status !== "streaming" ? part.state.input : {}, - structured: - event.data.metadata ?? (part && part.state.status !== "streaming" ? part.state.structured : {}), - content: event.data.content ?? (part && part.state.status !== "streaming" ? part.state.content : []), + metadata: event.data.metadata, + content: event.data.content, error: event.data.error, - result: event.data.result, } : { status: "completed", input: part && part.state.status !== "streaming" ? part.state.input : {}, - structured: event.data.structured, + metadata: event.data.metadata, content: event.data.content, - result: event.data.result, }, time: { created: part?.time.created ?? event.created, @@ -921,7 +918,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac const mainTool = (item: SessionMessageAssistantTool, active?: Record) => { const tool = normalizeTool(item) if (tool.name !== "subagent" || tool.state.status === "streaming") return - const found = childSessionID(record(tool.state.structured)) + const found = childSessionID(record(tool.state.metadata)) if (!found) return const child = admitChild(found.sessionID) if (!child) return @@ -967,7 +964,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac const key = sourceKey(event.data.assistantMessageID, event.data.callID) const pending = pendingCalls.get(key) if (event.type !== "session.tool.progress") pendingCalls.delete(key) - const found = childSessionID(record(event.type === "session.tool.failed" ? event.data.metadata : event.data.structured)) + const found = childSessionID(record(event.data.metadata)) if (!found) return const child = admitChild(found.sessionID) if (!child) return @@ -1085,11 +1082,13 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac input.emit() }, snapshot() { - const tabs = [...children.values()].toSorted((a, b) => { - const active = Number(b.status === "running") - Number(a.status === "running") - if (active !== 0) return active - return b.lastUpdatedAt - a.lastUpdatedAt - }).map(tab) + const tabs = [...children.values()] + .toSorted((a, b) => { + const active = Number(b.status === "running") - Number(a.status === "running") + if (active !== 0) return active + return b.lastUpdatedAt - a.lastUpdatedAt + }) + .map(tab) const child = selected ? children.get(selected) : undefined const details: Record = child && !child.detailStale ? { [child.sessionID]: { commits: child.frames.map((item) => item.commit) } } : {} diff --git a/packages/tui/src/mini/stream-v2.transport.ts b/packages/tui/src/mini/stream-v2.transport.ts index 496bda83b387..e0e767ca56d0 100644 --- a/packages/tui/src/mini/stream-v2.transport.ts +++ b/packages/tui/src/mini/stream-v2.transport.ts @@ -16,6 +16,7 @@ import { writeSessionOutput } from "./stream" import { createFragmentReconciler, fragmentRef, type FragmentReconciler } from "./stream-v2.fragment" import { createSubagentTracker, toolCommit, toolFinalPhase } from "./stream-v2.subagent" import { normalizeTool, toolOutputText } from "./tool" +import { toolDisplayContent } from "../util/tool-display" import type { FooterApi, FooterView, @@ -558,7 +559,7 @@ export async function createSessionTransport(input: StreamInput): Promise) { +export function toolOutputText(name: string, content: ReadonlyArray<{ type: string; text?: string }> | undefined) { + if (!content) return "" // V2 shell content appends model-only status after the user-visible command output. if (canonicalToolName(name) === "shell") return content.find((item) => item.type === "text")?.text ?? "" - return content.flatMap((item) => (item.type === "text" && item.text ? [item.text] : [])).join("\n") + const joined = content.flatMap((item) => (item.type === "text" && item.text ? [item.text] : [])).join("\n") + if (canonicalToolName(name) === "read") return readDisplayText(joined) ?? joined + return joined +} + +/** Read's model content is a JSON page envelope; unwrap the human-facing text. */ +export function readDisplayText(text: string): string | undefined { + if (!text.startsWith("{")) return undefined + const parsed = (() => { + try { + return JSON.parse(text) as unknown + } catch { + return undefined + } + })() + const envelope = dict(parsed) + if (typeof envelope.content === "string" && (envelope.type === "text-page" || envelope.encoding === "utf8")) + return envelope.content + if (!Array.isArray(envelope.entries)) return undefined + return envelope.entries + .flatMap((entry): string[] => { + if (typeof entry === "string") return [entry] + const path = dict(entry).path + return typeof path === "string" ? [path] : [] + }) + .join("\n") } function normalizeInput(name: string, value: unknown) { @@ -202,16 +229,16 @@ function normalizeFile(value: unknown): PatchFile | undefined { } } -function normalizeStructured(name: string, value: unknown) { - const structured = dict(value) - const files = list(structured.files).flatMap((item) => { +function normalizeMetadata(name: string, value: unknown) { + const metadata = dict(value) + const files = list(metadata.files).flatMap((item) => { const file = normalizeFile(item) return file ? [file] : [] }) - const sessionID = text(structured.sessionID) || text(structured.sessionId) + const sessionID = text(metadata.sessionID) || text(metadata.sessionId) return { - ...structured, - ...(["edit", "patch"].includes(name) && Array.isArray(structured.files) ? { files } : {}), + ...metadata, + ...(["edit", "patch"].includes(name) && Array.isArray(metadata.files) ? { files } : {}), ...(name === "subagent" && sessionID ? { sessionID } : {}), } } @@ -225,7 +252,7 @@ export function normalizeTool(tool: SessionMessageAssistantTool): SessionMessage state: { ...tool.state, input: normalizeInput(name, tool.state.input), - structured: normalizeStructured(name, toolDisplayMetadata(tool.state)), + metadata: normalizeMetadata(name, toolDisplayMetadata(tool.state)), }, } as SessionMessageAssistantTool } @@ -1089,13 +1116,13 @@ function frame(part: SessionMessageAssistantTool, directory?: string): ToolFrame output: "", time: { start: tool.time.created }, } - const output = toolOutputText(tool.name, tool.state.content) + const output = toolOutputText(tool.name, toolDisplayContent(tool.state)) return { directory, raw: output, name: tool.name, input: normalizeInput(tool.name, tool.state.input), - meta: normalizeStructured(tool.name, tool.state.structured), + meta: normalizeMetadata(tool.name, tool.state.metadata), state: dict(tool.state), status: tool.state.status, error: tool.state.status === "error" ? tool.state.error.message : "", diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 9b03a404225a..d4f9f2f00a91 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -42,6 +42,7 @@ import { canonicalToolName, finiteNumber, primitiveInputSummary, + toolDisplayContent, toolDisplayMetadata, webSearchProviderLabel, } from "../../util/tool-display" @@ -2146,7 +2147,7 @@ function ToolPart(props: { part: SessionMessageAssistantTool }) { }, get output() { if (props.part.state.status === "streaming") return undefined - return props.part.state.content + return toolDisplayContent(props.part.state) .flatMap((content) => (content.type === "text" ? [content.text] : [content.name ?? content.uri])) .join("\n") }, @@ -2548,6 +2549,8 @@ function BlockToolContent(props: BlockToolProps & { borderColor: RGBA }) { ) } +const SHELL_DISPLAY_LIMIT = 1024 * 1024 + function Shell(props: ToolProps) { const { themeV2 } = useTheme() const ctx = use() @@ -2559,6 +2562,7 @@ function Shell(props: ToolProps) { }) const color = createMemo(() => (permission() ? themeV2.text.feedback.warning.default : themeV2.text.default)) const shellID = createMemo(() => stringValue(props.metadata.shellID)) + const background = createMemo(() => Boolean(shellID()) && props.part.state.status !== "running") const backgroundRunning = createMemo(() => { const id = shellID() return Boolean(id && data.shell.get(id)) @@ -2567,31 +2571,73 @@ function Shell(props: ToolProps) { const command = createMemo(() => stringValue(props.input.command)) const [expanded, setExpanded] = createSignal(false) const [backgroundOutput, setBackgroundOutput] = createSignal("") + const [outputTruncated, setOutputTruncated] = createSignal(false) let loading = false - const loadBackgroundOutput = async () => { + let drainRequested = false + let cursor = 0 + let wasRunning = false + const loadBackgroundOutput = async (drain = false) => { const id = shellID() - if (!id || loading) return + if (!id) return + if (loading) { + if (drain) drainRequested = true + return + } loading = true const location = data.session.get(ctx.sessionID)?.location - await client.api.shell - .output({ - id, - limit: 1024 * 1024, - location: location ? { directory: location.directory, workspace: location.workspaceID } : undefined, - }) - .then((response) => setBackgroundOutput(stripAnsi(response.data.output.trim()))) - .catch(() => undefined) + do { + const response = await client.api.shell + .output({ + id, + cursor, + limit: SHELL_DISPLAY_LIMIT, + location: location ? { directory: location.directory, workspace: location.workspaceID } : undefined, + }) + .catch(() => undefined) + if (!response) break + if (response.data.output) + setBackgroundOutput((output) => { + const next = stripAnsi(output + response.data.output) + if (next.length <= SHELL_DISPLAY_LIMIT) return next + setOutputTruncated(true) + return next.slice(-SHELL_DISPLAY_LIMIT) + }) + if (response.data.cursor <= cursor) break + cursor = response.data.cursor + if (!drain || cursor >= response.data.size) break + const tail = Math.max(cursor, response.data.size - SHELL_DISPLAY_LIMIT) + if (tail > cursor) { + cursor = tail + setOutputTruncated(true) + } + } while (true) loading = false + if (drainRequested) { + drainRequested = false + void loadBackgroundOutput(true) + } } createEffect(() => { - if (!expanded() || !backgroundRunning()) return + const running = backgroundRunning() + if (!running) { + if (wasRunning) void loadBackgroundOutput(true) + wasRunning = false + return + } + wasRunning = true + if (background() && !expanded()) return + void loadBackgroundOutput() const interval = setInterval(() => void loadBackgroundOutput(), 1_000) onCleanup(() => clearInterval(interval)) }) const output = createMemo(() => { if (props.part.state.status === "streaming") return "" - if (shellID()) return expanded() ? backgroundOutput() : "" - const content = props.part.state.content[0] + if (shellID()) { + if (background() && !expanded()) return "" + const text = backgroundOutput().trim() + return outputTruncated() ? `[earlier output omitted]\n${text}` : text + } + const content = toolDisplayContent(props.part.state)[0] return stripAnsi(content?.type === "text" ? content.text.trim() : "") }) const maxLines = 10 @@ -2607,7 +2653,7 @@ function Shell(props: ToolProps) { const toggle = () => { const next = !expanded() setExpanded(next) - if (next) void loadBackgroundOutput() + if (next) void loadBackgroundOutput(!backgroundRunning()) } return ( @@ -2638,7 +2684,7 @@ function Shell(props: ToolProps) { - + Background @@ -3154,7 +3200,7 @@ function formatSessionTranscript(session: SessionInfo, messages: SessionMessageI ? item.state.error.message : item.state.status === "streaming" ? "" - : item.state.content + : toolDisplayContent(item.state) .flatMap((entry) => (entry.type === "text" ? [entry.text] : [entry.name ?? entry.uri])) .join("\n") return [`**Tool: ${item.name}**\n\n**Input:**\n\`\`\`json\n${input}\n\`\`\`\n\n${output}`] diff --git a/packages/tui/src/routes/session/permission.tsx b/packages/tui/src/routes/session/permission.tsx index 403e682f6f29..a6a746d881bc 100644 --- a/packages/tui/src/routes/session/permission.tsx +++ b/packages/tui/src/routes/session/permission.tsx @@ -118,14 +118,14 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director const source = createMemo(() => { const tool = props.request.source - if (!tool) return { input: undefined, structured: undefined } + if (!tool) return { input: undefined, metadata: undefined } const message = data.session.message.get(props.request.sessionID, tool.messageID) - if (message?.type !== "assistant") return { input: undefined, structured: undefined } + if (message?.type !== "assistant") return { input: undefined, metadata: undefined } const part = message.content.find((part) => part.type === "tool" && part.id === tool.callID) if (part?.type === "tool" && part.state.status !== "streaming") { - return { input: part.state.input, structured: part.state.structured } + return { input: part.state.input, metadata: part.state.metadata } } - return { input: undefined, structured: undefined } + return { input: undefined, metadata: undefined } }) const { themeV2 } = useTheme() @@ -182,7 +182,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director resources: props.request.resources, metadata: props.request.metadata, input: source().input, - structured: source().structured, + toolMetadata: source().metadata, }, pathFormatter.format, ) diff --git a/packages/tui/src/util/permission.ts b/packages/tui/src/util/permission.ts index 966ee670a0a0..165943c41348 100644 --- a/packages/tui/src/util/permission.ts +++ b/packages/tui/src/util/permission.ts @@ -17,7 +17,7 @@ export type PermissionPresentationInput = { resources: ReadonlyArray metadata?: unknown input?: unknown - structured?: unknown + toolMetadata?: unknown } export function permissionPresentation( @@ -26,7 +26,7 @@ export function permissionPresentation( ): PermissionPresentation { const action = canonicalToolName(source.action) const input = normalizeInput(action, source.input) - const metadata = { ...dict(source.structured), ...dict(source.metadata) } + const metadata = { ...dict(source.toolMetadata), ...dict(source.metadata) } const resources = source.resources.filter((item): item is string => typeof item === "string") if (action === "edit") { diff --git a/packages/tui/src/util/tool-display.ts b/packages/tui/src/util/tool-display.ts index d181545341ed..25e333456b98 100644 --- a/packages/tui/src/util/tool-display.ts +++ b/packages/tui/src/util/tool-display.ts @@ -28,7 +28,19 @@ export function webSearchProviderLabel(provider: unknown) { export function toolDisplayMetadata(state: unknown): Record { if (!state || typeof state !== "object" || Array.isArray(state)) return {} if (!("status" in state) || state.status === "streaming") return {} - if (!("structured" in state) || !state.structured || typeof state.structured !== "object") return {} - if (Array.isArray(state.structured)) return {} - return state.structured as Record + if (!("metadata" in state) || !state.metadata || typeof state.metadata !== "object") return {} + if (Array.isArray(state.metadata)) return {} + return state.metadata as Record } + +export function toolDisplayContent(state: SessionMessageAssistantTool["state"]) { + if (state.status === "streaming" || state.status === "running") return [] + return state.content ?? [] +} + +export function nonEmptyToolContent(content: ReadonlyArray | undefined): [T, ...T[]] | undefined { + if (!content) return undefined + const [first, ...rest] = content + return first === undefined ? undefined : [first, ...rest] +} +import type { SessionMessageAssistantTool } from "@opencode-ai/client/promise" diff --git a/packages/tui/test/cli/tui/data.test.tsx b/packages/tui/test/cli/tui/data.test.tsx index e7e6146f4b5d..3b4e5569f875 100644 --- a/packages/tui/test/cli/tui/data.test.tsx +++ b/packages/tui/test/cli/tui/data.test.tsx @@ -2342,8 +2342,7 @@ test("settles pending tools when a live failure arrives", async () => { sessionID: "session-1", assistantMessageID: "msg_explicit_assistant_9", callID: "call-1", - structured: { sessionID: "session-child", status: "running" }, - content: [], + metadata: { sessionID: "session-child", status: "running" }, }, }) @@ -2353,7 +2352,7 @@ test("settles pending tools when a live failure arrives", async () => { assistant?.type === "assistant" && assistant.content[0]?.type === "tool" && assistant.content[0].state.status === "running" && - assistant.content[0].state.structured.sessionID === "session-child" + assistant.content[0].state.metadata.sessionID === "session-child" ) }) @@ -2361,7 +2360,7 @@ test("settles pending tools when a live failure arrives", async () => { id: "evt_failed_1", created: 0, type: "session.tool.failed", - durable: durable("session-1", 6), + durable: durable("session-1", 6, 2), data: { sessionID: "session-1", assistantMessageID: "msg_explicit_assistant_9", @@ -2392,8 +2391,8 @@ test("settles pending tools when a live failure arrives", async () => { if (tool.state.status !== "error") return expect(tool.state.error).toEqual({ type: "unknown", message: "aborted" }) expect(tool.state.input).toEqual({}) - expect(tool.state.structured).toEqual({ sessionID: "session-child", status: "running" }) - expect(tool.state.content).toEqual([]) + expect(tool.state.metadata).toBeUndefined() + expect(tool.state.content).toBeUndefined() expect(tool.executed).toBe(false) expect(tool.providerState).toEqual({ call: true }) expect(tool.providerResultState).toEqual({ result: true }) diff --git a/packages/tui/test/mini/entry.body.test.ts b/packages/tui/test/mini/entry.body.test.ts index a5695305c49d..14b1a035b91b 100644 --- a/packages/tui/test/mini/entry.body.test.ts +++ b/packages/tui/test/mini/entry.body.test.ts @@ -133,8 +133,8 @@ describe("run entry body", () => { path: "src/a.ts", content: "const x = 1\n", }, - structured: {}, - content: [], + metadata: {}, + content: [{ type: "text", text: "" }], }, }), snapshot: { @@ -153,10 +153,10 @@ describe("run entry body", () => { input: { path: "src/a.ts", }, - structured: { + metadata: { files: [{ file: "src/a.ts", status: "modified", patch: "@@ -1 +1 @@\n-old\n+new\n" }], }, - content: [], + content: [{ type: "text", text: "" }], }, }), snapshot: { @@ -177,8 +177,8 @@ describe("run entry body", () => { state: { status: "completed", input: {}, - content: [], - structured: { + content: [{ type: "text", text: "" }], + metadata: { files: [ { status: "modified", @@ -221,8 +221,7 @@ describe("run entry body", () => { description: "Inspect reducer", agent: "explore", }, - structured: { sessionID: "ses-child-1", status: "running" }, - content: [], + metadata: { sessionID: "ses-child-1", status: "running" }, }, }), ), @@ -243,7 +242,7 @@ describe("run entry body", () => { agent: "explore", }, content: [{ type: "text", text: "# Findings\n\n- Footer stays live" }], - structured: { + metadata: { sessionID: "ses-child-1", status: "completed", output: "# Findings\n\n- Footer stays live", @@ -266,8 +265,8 @@ describe("run entry body", () => { description: "Inspect reducer", agent: "explore", }, - content: [], - structured: { + content: [{ type: "text", text: "" }], + metadata: { sessionID: "ses-child-1", status: "completed", output: "", @@ -341,7 +340,7 @@ describe("run entry body", () => { workdir: "/tmp/demo", }, content: [{ type: "text", text: output }], - structured: { exit: 0, truncated: false }, + metadata: { exit: 0, truncated: false }, }, }), ), @@ -364,8 +363,7 @@ describe("run entry body", () => { input: { command: "ls", }, - structured: {}, - content: [], + metadata: {}, }, }), ), @@ -435,8 +433,8 @@ describe("run entry body", () => { input: { patchText: "*** Begin Patch\n*** End Patch", }, - content: [], - structured: { + content: [{ type: "text", text: "" }], + metadata: { files: [ { status: "modified", @@ -463,8 +461,8 @@ describe("run entry body", () => { input: { patchText: "*** Begin Patch\n*** End Patch", }, - content: [], - structured: { + content: [{ type: "text", text: "" }], + metadata: { files: [ { status: "modified", @@ -499,8 +497,7 @@ describe("run entry body", () => { path: "/tmp/demo/run", }, error: { type: "unknown", message: "No such file or directory: '/tmp/demo/run'" }, - structured: {}, - content: [], + metadata: {}, }, }), ), @@ -520,11 +517,11 @@ describe("run entry body", () => { state: { status: "completed", input: { target: "demo" }, - structured: { + metadata: { result: { ok: true, nested: { values: Array.from({ length: 40 }, (_, index) => ({ index })) } }, large: "x".repeat(8_000), }, - content: [], + content: [{ type: "text", text: "" }], }, }), ) diff --git a/packages/tui/test/mini/permission.shared.test.ts b/packages/tui/test/mini/permission.shared.test.ts index 5a5058523a0e..2b73f0aae7df 100644 --- a/packages/tui/test/mini/permission.shared.test.ts +++ b/packages/tui/test/mini/permission.shared.test.ts @@ -95,8 +95,7 @@ describe("run permission shared", () => { { status: "running", input: { command: "git status --short" }, - structured: {}, - content: [], + metadata: {}, }, "call-shell", ), @@ -141,8 +140,7 @@ describe("run permission shared", () => { { status: "running", input: { query: "current releases" }, - structured: { provider: "exa", retained: true }, - content: [], + metadata: { provider: "exa", retained: true }, }, "call-search", ), @@ -165,8 +163,7 @@ describe("run permission shared", () => { { status: "running", input: { patchText: patch }, - structured: {}, - content: [], + metadata: {}, }, "call-edit", ), diff --git a/packages/tui/test/mini/scrollback.surface.test.ts b/packages/tui/test/mini/scrollback.surface.test.ts index 5db71b1045f3..e2fe8226819f 100644 --- a/packages/tui/test/mini/scrollback.surface.test.ts +++ b/packages/tui/test/mini/scrollback.surface.test.ts @@ -217,7 +217,7 @@ test("renders monochrome scrollback as ASCII markdown", async () => { try { await out.scrollback.append(assistant("# H")) expect(Reflect.get(out.scrollback, "active")?.renderable).toBeInstanceOf(MarkdownRenderable) - await out.scrollback.append(assistant('éading →\n\n> “quote”\n\n---\n\n| A | B |\n| - | - |\n| α | β |')) + await out.scrollback.append(assistant("éading →\n\n> “quote”\n\n---\n\n| A | B |\n| - | - |\n| α | β |")) await out.scrollback.complete() out.renderer.writeToScrollback((ctx) => ({ root: new TextRenderable(ctx.renderContext, { @@ -386,8 +386,7 @@ test("renders question summaries without boilerplate footer copy", async () => { }, ], }, - structured: {}, - content: [], + metadata: {}, }, }), final: toolCommit({ @@ -406,10 +405,10 @@ test("renders question summaries without boilerplate footer copy", async () => { }, ], }, - structured: { + metadata: { answers: [["Bug fix"]], }, - content: [], + content: [{ type: "text", text: "" }], }, }), }, @@ -481,8 +480,7 @@ test("inserts spacers for new visible groups", async () => { input: { pattern: "**/run.ts", }, - structured: {}, - content: [], + metadata: {}, }, }), ) @@ -617,8 +615,7 @@ test("does not double-space before completed shell output when inline tool heade command: "ls", workdir: "src/cli/cmd/run", }, - structured: {}, - content: [], + metadata: {}, }, }), ) @@ -634,8 +631,7 @@ test("does not double-space before completed shell output when inline tool heade pattern: "**/*tool*", path: "src/cli/cmd/run", }, - structured: {}, - content: [], + metadata: {}, }, }), ) @@ -651,8 +647,7 @@ test("does not double-space before completed shell output when inline tool heade pattern: "tool", path: "src/cli/cmd/run", }, - structured: {}, - content: [], + metadata: {}, }, }), ) @@ -670,7 +665,7 @@ test("does not double-space before completed shell output when inline tool heade workdir: "src/cli/cmd/run", }, content: [{ type: "text", text: ["src/cli/cmd/run", "ls", "demo.ts", "entry.body.ts", "", ""].join("\n") }], - structured: { exit: 0, truncated: false }, + metadata: { exit: 0, truncated: false }, }, }), ) @@ -735,8 +730,7 @@ test("renders structured write finals once as code blocks", async () => { path: "src/a.ts", content: "const x = 1\nconst y = 2\n", }, - structured: {}, - content: [], + metadata: {}, }, }), ) @@ -755,8 +749,8 @@ test("renders structured write finals once as code blocks", async () => { path: "src/a.ts", content: "const x = 1\nconst y = 2\n", }, - structured: {}, - content: [], + metadata: {}, + content: [{ type: "text", text: "" }], }, }), ) diff --git a/packages/tui/test/mini/stream-v2.transport.test.ts b/packages/tui/test/mini/stream-v2.transport.test.ts index f2c72ea6f051..b4ce61ef8d18 100644 --- a/packages/tui/test/mini/stream-v2.transport.test.ts +++ b/packages/tui/test/mini/stream-v2.transport.test.ts @@ -234,8 +234,7 @@ describe("V2 mini transport", () => { sessionID: "ses_1", thinking: false, footer: ui.api, - contextLimit: (model) => - model.providerID === "test" && model.modelID === "model" ? 160_000 : undefined, + contextLimit: (model) => (model.providerID === "test" && model.modelID === "model" ? 160_000 : undefined), }) events.push({ @@ -324,8 +323,7 @@ describe("V2 mini transport", () => { { status: "running" as const, input: { command: "git status --short" }, - structured: {}, - content: [], + metadata: {}, }, "call_child_source", ), @@ -620,8 +618,8 @@ describe("V2 mini transport", () => { expect.objectContaining({ kind: "user", messageID: "msg_queued", text: "follow up" }), ) expect(pending()).toEqual([]) - const prompt = spyOn(client.session, "prompt").mockImplementation((request) => - ok({ ...promptAdmission(request), admittedSeq: 2 }) as never, + const prompt = spyOn(client.session, "prompt").mockImplementation( + (request) => ok({ ...promptAdmission(request), admittedSeq: 2 }) as never, ) await transport.queuePromptTurn({ agent: "review", @@ -631,10 +629,7 @@ describe("V2 mini transport", () => { files: [], includeFiles: false, }) - expect(client.session.switchAgent).toHaveBeenCalledWith( - { sessionID: "ses_1", agent: "review" }, - expect.anything(), - ) + expect(client.session.switchAgent).toHaveBeenCalledWith({ sessionID: "ses_1", agent: "review" }, expect.anything()) expect(prompt).toHaveBeenCalledWith(expect.objectContaining({ delivery: "queue" }), expect.anything()) events.push({ id: "evt_earlier_admission", @@ -1976,13 +1971,13 @@ describe("V2 mini transport", () => { id: `evt_repeated_success_${index}`, created: index * 3 + 3, type: "session.tool.success", - durable: durable("ses_1", index * 3 + 2), + durable: durable("ses_1", index * 3 + 2, 2), data: { sessionID: "ses_1", assistantMessageID: messageID, callID: "call_repeated", - structured: {}, - content: [], + metadata: {}, + content: [{ type: "text", text: "" }], executed: true, }, }) @@ -2048,15 +2043,14 @@ describe("V2 mini transport", () => { sessionID: "ses_1", assistantMessageID: "msg_progress", callID: "call_progress", - structured: { checkpoint: 1 }, - content: [{ type: "text", text: "partial" }], + metadata: { checkpoint: 1 }, }, }) events.push({ id: "evt_progress_failed", created: 4, type: "session.tool.failed", - durable: durable("ses_1", 3), + durable: durable("ses_1", 3, 2), data: { sessionID: "ses_1", assistantMessageID: "msg_progress", @@ -2077,7 +2071,7 @@ describe("V2 mini transport", () => { ]) expect(commits.at(-1)?.part?.state).toMatchObject({ status: "error", - structured: { checkpoint: 1 }, + metadata: { checkpoint: 1 }, content: [{ type: "text", text: "partial" }], }) await transport.close() @@ -2649,9 +2643,7 @@ describe("V2 mini transport", () => { ], command: { name: "deploy", arguments: "prod" }, }, - files: [ - { type: "file", url: "file:///tmp/context.txt", filename: "context.txt", mime: "text/plain" }, - ], + files: [{ type: "file", url: "file:///tmp/context.txt", filename: "context.txt", mime: "text/plain" }], includeFiles: true, }) @@ -2732,10 +2724,7 @@ describe("V2 mini transport", () => { includeFiles: true, }) - expect(client.session.switchAgent).toHaveBeenCalledWith( - { sessionID: "ses_1", agent: "review" }, - expect.anything(), - ) + expect(client.session.switchAgent).toHaveBeenCalledWith({ sessionID: "ses_1", agent: "review" }, expect.anything()) expect(request).toMatchObject({ sessionID: "ses_1", id: "msg_skill", skill: "tigerstyle" }) expect(command).not.toHaveBeenCalled() expect(prompt).not.toHaveBeenCalled() @@ -2886,7 +2875,7 @@ describe("V2 mini transport", () => { id: "evt_failed_subagent", created: 3, type: "session.tool.failed", - durable: durable("ses_1", 2), + durable: durable("ses_1", 2, 2), data: { sessionID: "ses_1", assistantMessageID: "msg_failed_subagent", @@ -2897,8 +2886,7 @@ describe("V2 mini transport", () => { }, }) - while (!states().some((state) => state.tabs.some((tab) => tab.sessionID === "ses_child_failed"))) - await Bun.sleep(0) + while (!states().some((state) => state.tabs.some((tab) => tab.sessionID === "ses_child_failed"))) await Bun.sleep(0) expect(states().at(-1)?.tabs).toMatchObject([ { sessionID: "ses_child_failed", @@ -2954,8 +2942,7 @@ describe("V2 mini transport", () => { sessionID: "ses_1", assistantMessageID: "msg_subagent", callID: "call_subagent", - structured: { sessionID: "ses_child_progress", status: "running" }, - content: [], + metadata: { sessionID: "ses_child_progress", status: "running" }, }, }) while (!states().some((state) => state.tabs.some((tab) => tab.sessionID === "ses_child_progress"))) @@ -3005,8 +2992,7 @@ describe("V2 mini transport", () => { sessionID: "ses_child_progress", assistantMessageID: "msg_child_tool", callID: "call_child_shell", - structured: { checkpoint: "child" }, - content: [{ type: "text", text: "child partial" }], + metadata: { checkpoint: "child" }, }, }) events.push({ @@ -3025,7 +3011,7 @@ describe("V2 mini transport", () => { id: "evt_child_tool_failed", created: 8, type: "session.tool.failed", - durable: durable("ses_child_progress", 3), + durable: durable("ses_child_progress", 3, 2), data: { sessionID: "ses_child_progress", assistantMessageID: "msg_child_tool", @@ -3058,7 +3044,7 @@ describe("V2 mini transport", () => { commits.find((item) => item.part?.id === "call_child_shell" && item.toolState === "error")?.part?.state, ).toMatchObject({ status: "error", - structured: { checkpoint: "child" }, + metadata: { checkpoint: "child" }, content: [{ type: "text", text: "child partial" }], }) expect( @@ -3145,7 +3131,11 @@ describe("V2 mini transport", () => { { sessionID: "ses_child", label: "Explore", title: "Find files", status: "running" }, ]) - expect(states().at(-1)?.details.ses_child?.commits.filter((item) => item.text === "child answer")).toHaveLength(1) + expect( + states() + .at(-1) + ?.details.ses_child?.commits.filter((item) => item.text === "child answer"), + ).toHaveLength(1) events.push({ id: "evt_child_text_replayed", @@ -3159,7 +3149,11 @@ describe("V2 mini transport", () => { }, }) await Bun.sleep(0) - expect(states().at(-1)?.details.ses_child?.commits.filter((item) => item.text === "child answer")).toHaveLength(1) + expect( + states() + .at(-1) + ?.details.ses_child?.commits.filter((item) => item.text === "child answer"), + ).toHaveLength(1) events.push({ id: "evt_child_text_suffix", @@ -3172,7 +3166,9 @@ describe("V2 mini transport", () => { delta: " suffix", }, }) - while (!states().some((state) => state.details.ses_child?.commits.some((item) => item.text === "child answer suffix"))) + while ( + !states().some((state) => state.details.ses_child?.commits.some((item) => item.text === "child answer suffix")) + ) await Bun.sleep(0) events.push({ @@ -3437,7 +3433,7 @@ describe("V2 mini transport", () => { status: "completed" as const, input: { command: "projected" }, content: [{ type: "text" as const, text: "projected result" }], - structured: {}, + metadata: {}, }, time: { created: 1, ran: 1, completed: 2 }, }, @@ -3488,12 +3484,12 @@ describe("V2 mini transport", () => { id: "evt_success_terminal", created: 2, type: "session.tool.success", - durable: durable("ses_child", 2), + durable: durable("ses_child", 2, 2), data: { sessionID: "ses_child", assistantMessageID: "msg_tool_projected", callID: "call_terminal", - structured: {}, + metadata: {}, content: [{ type: "text", text: "found" }], executed: true, }, @@ -3651,13 +3647,13 @@ describe("V2 mini transport", () => { id: "evt_parent_success", created: 0, type: "session.tool.success", - durable: durable("ses_1", 1), + durable: durable("ses_1", 1, 2), data: { sessionID: "ses_1", assistantMessageID: "msg_parent_a", callID: "call_sub", - structured: { sessionID: "ses_child", status: "running", output: "" }, - content: [], + metadata: { sessionID: "ses_child", status: "running", output: "" }, + content: [{ type: "text", text: "" }], executed: true, }, }) @@ -3738,7 +3734,7 @@ describe("V2 mini transport", () => { status: "completed" as const, input: { agent: "explore", description: "Find things", prompt: "go" }, content: [{ type: "text" as const, text: "done" }], - structured: { sessionID: "ses_child", status: "completed", output: "done" }, + metadata: { sessionID: "ses_child", status: "completed", output: "done" }, }, time: { created: 1, ran: 1, completed: 2 }, }, diff --git a/packages/tui/test/mini/tool.test.ts b/packages/tui/test/mini/tool.test.ts index fb005cdf13ca..62a28c08ce9b 100644 --- a/packages/tui/test/mini/tool.test.ts +++ b/packages/tui/test/mini/tool.test.ts @@ -28,7 +28,7 @@ describe("Mini tool presentation", () => { state: { status: "completed", input: { patchText: "*** Begin Patch\n*** End Patch" }, - structured: { + metadata: { files: [ { type: "update", @@ -45,7 +45,7 @@ describe("Mini tool presentation", () => { ).toMatchObject({ name: "patch", state: { - structured: { + metadata: { files: [ { status: "modified", @@ -66,27 +66,25 @@ describe("Mini tool presentation", () => { state: { status: "running", input: { subagent_type: "explore", description: "Inspect" }, - structured: {}, - content: [], + metadata: {}, }, time: { created: 1, ran: 1 }, }), ).toMatchObject({ name: "subagent", state: { input: { agent: "explore" } } }) }) - test("renders the skill name from structured metadata with the input id as fallback", () => { - const skill = (structured: { name?: string }) => ({ - type: "tool" as const, - id: "call-skill", - name: "skill", - state: { - status: "completed" as const, - input: { id: "tigerstyle" }, - structured, - content: [], - }, - time: { created: 1, ran: 1, completed: 2 }, - }) + test("renders the skill name from tool metadata with the input id as fallback", () => { + const skill = (metadata: { name?: string }) => + canonicalToolPart( + "skill", + { + status: "completed", + input: { id: "tigerstyle" }, + metadata, + content: [{ type: "text", text: "" }], + }, + "call-skill", + ) expect(toolInlineInfo(skill({ name: "effect" })).title).toBe('Skill "effect"') expect(toolInlineInfo(skill({})).title).toBe('Skill "tigerstyle"') @@ -112,8 +110,8 @@ describe("Mini tool presentation", () => { canonicalToolPart("glob", { status: "completed", input: { pattern: "*.ts" }, - structured: { count: 3 }, - content: [], + metadata: { count: 3 }, + content: [{ type: "text", text: "" }], }), ).description, ).toBe("3 matches") @@ -122,8 +120,8 @@ describe("Mini tool presentation", () => { canonicalToolPart("grep", { status: "completed", input: { pattern: "needle" }, - structured: { matches: 1 }, - content: [], + metadata: { matches: 1 }, + content: [{ type: "text", text: "" }], }), ).description, ).toBe("1 match") diff --git a/packages/tui/test/util/tool-display.test.ts b/packages/tui/test/util/tool-display.test.ts index 1201dba25796..cedde400c3c5 100644 --- a/packages/tui/test/util/tool-display.test.ts +++ b/packages/tui/test/util/tool-display.test.ts @@ -40,19 +40,19 @@ describe("webSearchProviderLabel", () => { }) describe("toolDisplayMetadata", () => { - test("returns structured metadata for non-pending states", () => { - const structured = { provider: "parallel", numResults: 3 } + test("returns tool metadata for non-pending states", () => { + const metadata = { provider: "parallel", numResults: 3 } - expect(toolDisplayMetadata({ status: "running", structured })).toBe(structured) - expect(toolDisplayMetadata({ status: "completed", structured })).toBe(structured) - expect(toolDisplayMetadata({ status: "error", structured })).toBe(structured) + expect(toolDisplayMetadata({ status: "running", metadata })).toBe(metadata) + expect(toolDisplayMetadata({ status: "completed", metadata })).toBe(metadata) + expect(toolDisplayMetadata({ status: "error", metadata })).toBe(metadata) }) test("does not expose pending or malformed metadata", () => { - expect(toolDisplayMetadata({ status: "streaming", structured: { provider: "exa" } })).toEqual({}) + expect(toolDisplayMetadata({ status: "streaming", metadata: { provider: "exa" } })).toEqual({}) expect(toolDisplayMetadata({ status: "completed" })).toEqual({}) - expect(toolDisplayMetadata({ status: "completed", structured: null })).toEqual({}) - expect(toolDisplayMetadata({ status: "completed", structured: [] })).toEqual({}) + expect(toolDisplayMetadata({ status: "completed", metadata: null })).toEqual({}) + expect(toolDisplayMetadata({ status: "completed", metadata: [] })).toEqual({}) expect(toolDisplayMetadata(undefined)).toEqual({}) }) }) diff --git a/packages/www/content/docs/build/plugins.mdx b/packages/www/content/docs/build/plugins.mdx index e5adbc92d768..74c7a6e5e9b1 100644 --- a/packages/www/content/docs/build/plugins.mdx +++ b/packages/www/content/docs/build/plugins.mdx @@ -248,7 +248,7 @@ mutable fields: | `ctx.aisdk.hook("language", callback)` | `language`, after inspecting `model`, `sdk`, and `options` | | `ctx.session.hook("request", callback)` | `system`, `messages`, and the `tools` record immediately before model dispatch | | `ctx.tool.hook("execute.before", callback)` | `input`, before the selected tool executes | -| `ctx.tool.hook("execute.after", callback)` | `result`, `output`, and `outputPaths`, after execution settles | +| `ctx.tool.hook("execute.after", callback)` | Terminal `content`, `metadata`, and `outputPaths`; `error` on failure | For example, remove a tool from selected model requests and normalize another tool's input: @@ -278,51 +278,66 @@ handle expected errors inside the callback. ### Add a tool -Pass a tool declaration to `tools.add`. Define its input with JSON Schema and -use an async executor: +Create an executable tool with `Tool.make`, then register it with a name +and registration options. Define its input with JSON Schema and use an async +executor: ```js title=".opencode/plugins/greeting.js" import { Plugin } from "@opencode-ai/plugin/v2" +import { Tool } from "@opencode-ai/plugin/v2/tool" export default Plugin.define({ id: "acme.greeting", setup: async (ctx) => { await ctx.tool.transform((tools) => { - tools.add({ - name: "greeting", - description: "Create a greeting", - jsonSchema: { - type: "object", - properties: { - name: { type: "string" }, + tools.add( + "greeting", + Tool.make({ + description: "Create a greeting", + input: { + type: "object", + properties: { + name: { type: "string" }, + }, + required: ["name"], + additionalProperties: false, }, - required: ["name"], - additionalProperties: false, - }, - execute: async ({ name }) => { - const text = `Hello, ${name}!` - return { - structured: { greeting: text }, - content: [{ type: "text", text }], - } - }, - }) + output: { + type: "object", + properties: { greeting: { type: "string" } }, + required: ["greeting"], + additionalProperties: false, + }, + execute: async ({ name }) => { + const text = `Hello, ${name}!` + return { + output: { greeting: text }, + content: text, + } + }, + }), + ) }) }, }) ``` -Unsupported characters in tool and group names are normalized to underscores. -The resulting exposed key must begin with a letter and contain at most 64 -letters, digits, underscores, or hyphens. Set `options` on the declaration to -configure registration with `{ group, deferred }`: +Unsupported characters in tool names are normalized to underscores. Namespace +segments must begin with a letter, contain at most 64 letters, digits, +underscores, or hyphens, and are joined with dots. Pass the optional third +argument to `tools.add` to configure the registration with +`{ namespace, codemode }`: -- `group` prefixes and groups the exposed tool name. -- `deferred: true` makes the tool available through the deferred `execute` - tool instead of exposing it directly. +- `namespace` prefixes and groups the exposed tool name. +- `codemode` defaults to `true` and makes the tool available through the + `execute` CodeMode tool. Set `codemode: false` to expose it directly to the + provider. The executor receives a second context argument containing `sessionID`, -`agent`, `assistantMessageID`, and `toolCallID`. +`agent`, `messageID`, `callID`, and `progress`. A tool with `output` +must return `output`; Effect and Standard Schema codecs validate it, while raw +JSON Schema definitions enforce JSON compatibility only. A tool +without `output` returns model-visible `content` instead. ### Add a command diff --git a/specs/v2/README.md b/specs/v2/README.md index 49fe529fa6d1..33a91df32187 100644 --- a/specs/v2/README.md +++ b/specs/v2/README.md @@ -23,7 +23,7 @@ Generated clients follow the assembled public `HttpApi`. GitHub issues own activ | Document | Job | | ----------------------- | --------------------------------------------------------------------------------------- | | [Session](./session.md) | Explain prompt admission, execution, instructions, compaction, and recovery boundaries. | -| [Tools](./tools.md) | Explain tool construction, registration, execution, and settlement laws. | +| [Tools](./tools.md) | Explain tool construction, registration, execution, and outcome laws. | ## Decisions And Proposals diff --git a/specs/v2/schema-changelog.md b/specs/v2/schema-changelog.md index 3e760ad00df9..1ad9151a011a 100644 --- a/specs/v2/schema-changelog.md +++ b/specs/v2/schema-changelog.md @@ -2,6 +2,19 @@ Status: **Historical pre-release compatibility ledger.** Older entries retain the names and behavior that were accurate when written; current contracts live in Protocol, Schema, Core, and the indexed specifications. +## 2026-07-22: Canonical Tool Results + +- Bump `session.tool.success` and `session.tool.failed` to version 2. Success stores exactly non-empty model `content` plus optional JSON `metadata`; failure stores one `error` plus the final bounded partial snapshot (`content?`, `metadata?`). The generic `structured` and `result` fields are removed. +- Rename the ephemeral `session.tool.progress` field `structured` to `metadata`; progress is one metadata replacement snapshot. Model content belongs exclusively to terminal outcomes. +- Change projected `SessionMessage.ToolState`: completed is `{ input, content, metadata? }` with non-empty content, error is `{ input, error, content?, metadata? }`, running renames `structured` to `metadata`. Provider replay derives wire values from canonical content. +- Move provider-hosted result payloads into provider-owned result state (`providerResultState.result`); Anthropic server-tool round-trips read it during lowering. OpenAI continues replaying from item references. +- Public Plugin API: remove `structured`, projection callbacks, the `Structured` generic, `Tool.Failure.metadata`, and the exported `Tool.settle`; tool responses carry schema-validated `output`, model-visible `content`, and optional JSON `metadata`. Code Mode receives the validated encoded output. + +Compatibility: + +- `20260722170000_canonical_tool_results` rewrites projected assistant tool rows in place: terminal content is preserved (or synthesized once from the old `structured`/`result`), compact values move to `metadata` only where tools now declare projections, and hosted payloads are copied into `providerResultState.result`. Old-version tool events fall out of the durable manifest and are skipped on read; no event rows are deleted. +- Promise and Effect client surfaces are regenerated. The legacy JavaScript SDK regenerates on the branch where the V1 package exists. + ## 2026-07-10: Replace Instruction Checkpoints With Value Deltas - Replace rendered `session.instructions.updated.1` prose with `session.instructions.updated.2 { delta }`, where values are SHA-256 hashes and the literal `"removed"` means removal. @@ -74,7 +87,7 @@ Compatibility: - No stored event row, database, or runtime publish behavior change; runtime already attaches the envelope only after durable commit/replay. - Generated clients now model the existing invariant: durable events carry `durable`, live-only events do not. -## 2026-07-03: Declare Event Durability At Definition Level +## 2026-07-03: Declare Event Durability At Tool Level - Add explicit `Event.durable(...)` and `Event.ephemeral(...)` definition constructors. - Preserve the existing durable and live-only event classifications while deriving durable inventories from definition metadata instead of hand-maintained lists. diff --git a/specs/v2/session.md b/specs/v2/session.md index 763d5807197e..63dcadcac1ea 100644 --- a/specs/v2/session.md +++ b/specs/v2/session.md @@ -43,13 +43,13 @@ The managed server provides graceful restart continuity through private Session Before each Step, the runner reloads Session History, resolves the selected agent and model, prepares instructions, and materializes tools. Most Steps make one Physical Attempt; overflow-triggered compaction recovery may rebuild the same Step for one additional provider request. -Each complete local tool call is durable before side effects begin. Local calls start eagerly and may run concurrently, but settlement publication remains serialized. Every local and hosted call reaches durable success or failure before the Step publishes its single terminal ended or failed event. +Each complete local tool call is durable before side effects begin. Local calls start eagerly and may run concurrently, but terminal outcome publication remains serialized. Every local and hosted call reaches durable success or failure before the Step publishes its single terminal ended or failed event. Tool calls belong to their assistant message. `callID` is unique only within that Step, so durable tool events also carry `assistantMessageID`. Before `runStep` assembles its provider request, orphan reconciliation fails tool calls still projected as streaming or running from an earlier process. It preserves the original assistant attribution and never replays ambiguous side effects. -After local settlement, continuation reloads projected history and begins a new Step. The runner never delegates orchestration to an in-memory tool loop. +After a local outcome, continuation reloads projected history and begins a new Step. The runner never delegates orchestration to an in-memory tool loop. ## Retry Is Narrow And Observable diff --git a/specs/v2/tools.md b/specs/v2/tools.md index 2b30c0199187..592223a20c2b 100644 --- a/specs/v2/tools.md +++ b/specs/v2/tools.md @@ -1,28 +1,28 @@ # V2 Tools -Status: **Current semantic overview.** The Plugin package owns the public tool type; Core owns registration, settlement, and generic output bounding. +Status: **Current semantic overview.** The Plugin package owns the public tool type; Core owns registration, execution, and generic output bounding. -## Tool Declarations +## Tools -V2 has one structural declaration for locally executable tools. Typed tools declare schemas, execution, and optional model-facing projection together: +V2 has one structural tool value for locally executable tools. Typed tools declare schemas and execution together: ```ts const read = Tool.make({ description: "Read a file", input: Schema.Struct({ path: Schema.String }), output: Schema.Struct({ content: Schema.String }), - execute: ({ path }, context) => readFile(path, context), - toModelOutput: ({ output }) => [{ type: "text", text: output.content }], + execute: ({ path }, context) => + readFile(path, context).pipe(Effect.map((output) => ({ output, content: output.content }))), }) ``` -`structured` and `toStructuredOutput` may expose a smaller validated result than the complete execution output. Dynamic MCP and manifest tools use the same declaration with runtime JSON Schema. +One tool response may carry three values: the declared, schema-validated `output` is the ephemeral machine value Code Mode receives; `content` is the model-facing value stored durably; and optional `metadata` is compact JSON for tool-specific UI. A tool without `output` intentionally returns only model-visible `content` and optional `metadata`. Dynamic MCP and manifest tools use the same tool shape with runtime JSON Schema. Built-ins and statically authored plugin tools use this same constructor and execution contract. -`Tool.Definition` is a transparent structural value with exactly one executor. Effect schemas and schemas implementing both Standard Schema V1 and Standard JSON Schema V1 are accepted. The Tool module derives model definitions and interprets invocations for the registry; callers normally rely on `Tool.make` inference rather than naming the declaration type. +`Tool.Tool` is a transparent structural value with exactly one `execute` function. Effect schemas and schemas implementing both Standard Schema V1 and Standard JSON Schema V1 are accepted. The Tool module derives inert model-facing `LLM.ToolDefinition` values and executes tools for the registry; callers normally rely on `Tool.make` inference rather than naming the nested type. -Standard input schemas validate model input into the handler value. Standard output schemas validate the handler result into the model-facing value. Effect codecs retain their native decode-input and encode-output directions. +Standard input schemas validate model input into the tool input. Standard output schemas validate the tool response's `output` into the Code Mode machine value. Effect codecs retain their native decode-input and encode-output directions. Input and output codecs are self-contained. Schema conversion cannot require services. Tool dependencies are acquired during construction and captured by `execute`. @@ -34,14 +34,13 @@ Every local tool receives the same concrete invocation context: interface Tool.Context { readonly sessionID: Session.ID readonly agent: Agent.ID - readonly assistantMessageID: SessionMessage.ID - readonly toolCallID: string + readonly messageID: SessionMessage.ID + readonly callID: string + readonly progress: (update: Progress) => Effect.Effect } ``` -`assistantMessageID` is the durable ID of the assistant message containing the call. The Session runner owns this association and supplies the complete context to the registry; the registry does not infer it. - -Durable events call the invocation identifier `callID`; `Tool.Context.toolCallID` is the same value at the executor boundary. +`messageID` is the durable ID of the assistant message containing the call. The Session runner owns this association and supplies the complete context to the registry; the registry does not infer it. `callID` carries the same invocation identifier durable events use. Decoded tool input is passed separately to `execute`. Raw provider input and domain services do not belong in the invocation context. @@ -65,7 +64,8 @@ The record key is the authored name. Registration normalizes it before deriving ```ts interface Tools { readonly register: ( - tools: Readonly>, + tools: Readonly>, + options?: Tool.RegisterOptions, ) => Effect.Effect } ``` @@ -105,8 +105,8 @@ yield * agent: context.agent, source: { type: "tool", - messageID: context.assistantMessageID, - callID: context.toolCallID, + messageID: context.messageID, + callID: context.callID, }, action: "grep", resources: [input.pattern], @@ -126,30 +126,32 @@ Sharing a tool type does not imply equal authority. Built-ins and trusted Locati ## Requests Capture Tool Values -The Location-scoped registry owns effective lookup and settlement. For each local call it: +The Location-scoped registry owns effective lookup and execution through one request-scoped snapshot pairing advertised LLM definitions with captured tools. For each local call it: 1. Resolves one effective named registration. 2. Decodes provider input with the input codec. -3. Invokes the tool with the runner-supplied context. -4. Encodes the returned output with the output codec. -5. Projects encoded output into model-facing content. -6. Bounds the complete model-facing output. -7. Runs `execute.after` hooks with the bounded settlement. -8. Returns the settlement to the runner for durable publication. +3. Executes the tool with the runner-supplied context. +4. Encodes the returned output with the output codec; the encoded value is the ephemeral machine output for Code Mode. +5. Normalizes the tool response into canonical non-empty model content and optional JSON metadata. +6. Bounds the model content; validates metadata, dropping invalid or oversized values with a warning rather than failing the call. +7. Runs `execute.after` hooks with the canonical outcome and managed output paths. +8. Returns one `ToolOutcome` — completed with output, content, and optional metadata, or an error with an optional final partial snapshot — to the runner for durable publication. + +Invalid input never executes the tool. Invalid output never produces a successful execution. -Invalid input never invokes the tool. Invalid output never produces a successful settlement. +When an output-bearing tool omits `content`, an encoded string becomes one text item and any other encoded JSON is serialized once. A tool without `output` must provide non-empty model content. -`toModelOutput` is pure and total. When omitted, the encoded output remains structured output; an encoded string is also projected as text. Projection does not receive invocation identity because presentation depends only on validated input and output. +Each model request captures the effective registration for every advertised name. Execution uses those captured tools; later registration changes affect later requests. Unknown, hook-removed, and final-Step calls fail individually through the same execution seam; the final Step retains tool definitions with `toolChoice: "none"` where the provider supports it so the cached prompt prefix survives. -Each model request captures the effective registered `Tool` value for every advertised name. Settlement executes those captured values; later registration changes affect later requests. +Durable terminal events are self-contained: success stores exactly the non-empty model content plus optional metadata; failure stores one error plus the final bounded snapshot of partial progress. Provider replay derives its wire value from canonical content; provider-hosted payloads that a protocol requires verbatim live in provider-owned result state, never in a generic result field. ## Producers And The Registry Own Different Limits Producers may cap capture or spool data before a complete tool result exists. For example, a process tool may retain output it cannot keep in memory. Producer limits must report their own loss accurately; they are separate from registry bounding and cannot claim to reconstruct bytes already discarded. -After projection, the registry bounds the channel sent to the provider. When content exists, only its textual parts are measured; structured metadata is retained unchanged without being double-counted, and native media remains unchanged under producer-owned limits. When content is empty, the structured output is measured. Oversized provider-facing text or structured output is retained in managed storage and replaced with a bounded text preview while structured metadata and media are preserved; if complete retention fails, settlement fails operationally rather than publishing lossy success. Managed paths never appear in `Tool.make`, tool output schemas, or projection callbacks solely for retention bookkeeping. +After tool execution, the registry bounds the model content sent to the provider: only textual parts are measured, native media remains unchanged under producer-owned limits, and the default cut keeps a head-plus-tail split with the omission marker in the middle. Oversized text is retained in managed storage and replaced with a bounded preview; if complete retention fails, execution fails operationally rather than publishing lossy success. Metadata is validated and measured independently and never becomes an unbounded side channel. Managed paths never appear in `Tool.make` or tool output schemas solely for retention bookkeeping. -`execute.after` hooks receive the bounded settlement and its internal managed paths. Hooks may deliberately transform that settlement; the registry does not apply a second bounding pass afterward. +`execute.after` hooks receive the canonical bounded outcome and its internal managed paths. Hooks may deliberately transform that outcome; changed content is normalized and bounded again before publication. ## Failures Preserve Interruptions @@ -158,15 +160,18 @@ Outcomes remain distinct: - `ToolFailure` is an expected model-visible failure. - Interruption cancels the invocation and is not a tool result. - Unexpected typed errors and defects follow the runner's operational failure policy. -- Unknown and invalid calls become explicit model-visible settlement errors without invoking a handler. +- Unknown and invalid calls become explicit model-visible execution errors without executing a tool. -Leaf tools translate only errors they deliberately classify as recoverable. Broad cause-catching around an executor is invalid because it consumes interruption and defects. +Tools translate only errors they deliberately classify as recoverable. Broad cause-catching around `execute` is invalid because it consumes interruption and defects. ## Laws -- **Single executor:** `Tool.make(config)` can invoke only `config.execute`. -- **Codec boundary:** execution observes decoded input; projection observes encoded output. +- **Single execution:** `Tool.make(config)` can execute only `config.execute`. +- **Codec boundary:** a tool observes decoded input; Code Mode observes the validated encoded output; model content and metadata come from the tool response. +- **Canonical representation:** a completed call has exactly one stored model representation; a failed call has exactly one stored error plus at most one final partial snapshot. Every other view is derived at a named boundary. +- **Metadata opt-in:** absent response metadata produces absent metadata, never a copied output. - **Durable identity:** invocation-owned records use the exact Session, agent, assistant message, and call IDs supplied by the runner. - **Scoped registration:** closing a Scope removes exactly its registration and reveals any prior active overlay. -- **Captured execution:** a call executes the registered `Tool` value advertised in its model request. +- **Captured execution:** a call executes the registered tool advertised in its model request. +- **Per-call rejection:** rejecting one unavailable call cannot fail another call. - **Storage encapsulation:** domain output does not change according to model-output bounding or retention policy. From 02f27251540efeb414dd526f227a27c78000841a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 16:20:37 -0500 Subject: [PATCH 063/150] chore: merge dev into v2 (#38563) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Aiden Cline Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Dax Raad Co-authored-by: Dax Co-authored-by: opencode-agent[bot] Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Nabs Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Brendan Allan Co-authored-by: Victor Navarro Co-authored-by: Vladimir Glafirov Co-authored-by: AidenGeunGeun Co-authored-by: Mark Co-authored-by: Aiden Cline Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: opencode Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com> Co-authored-by: Jay Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: BB84 <110078428+BB-84C@users.noreply.github.com> Co-authored-by: Dustin Deus Co-authored-by: Frank Co-authored-by: Jack Co-authored-by: Sebastian Co-authored-by: Jérôme Benoit Co-authored-by: Test User Co-authored-by: Simon Klee Co-authored-by: Rahul A Mistry <149420892+ProdigyRahul@users.noreply.github.com> Co-authored-by: Qiping Li Co-authored-by: liqiping Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Matthias Reso <13337103+mreso@users.noreply.github.com> Co-authored-by: tobwen <1864057+tobwen@users.noreply.github.com> Co-authored-by: Daniel Polito Co-authored-by: opencode --- bun.lock | 21 +- nix/hashes.json | 8 +- package.json | 1 + .../performance/timeline-stability/fixture.ts | 2 + .../session-timeline-transport.spec.ts | 6 +- packages/app/e2e/utils/mock-server.ts | 278 ++++++- packages/app/e2e/utils/sse-transport.ts | 38 +- packages/app/package.json | 1 + packages/app/src/context/server-sdk.test.ts | 38 +- packages/app/src/context/server-sdk.tsx | 194 +++-- packages/app/src/utils/server-compat.test.ts | 113 +++ packages/app/src/utils/server-compat.ts | 495 ++++++++++++ packages/app/src/utils/server-health.test.ts | 38 +- packages/app/src/utils/server-health.ts | 28 +- .../app/src/utils/server-protocol.test.ts | 40 + packages/app/src/utils/server-protocol.ts | 35 + packages/app/src/utils/server.ts | 21 + .../app/vendor/opencode-ai-client-1.17.13.tgz | Bin 0 -> 75585 bytes packages/core/package.json | 2 +- packages/core/test/provider-mistral.test.ts | 282 +++++++ packages/desktop/src/main/server.ts | 23 +- packages/session-ui/package.json | 1 + patches/@ai-sdk%2Fmistral@3.0.51.patch | 709 ++++++++++++++++++ 23 files changed, 2280 insertions(+), 94 deletions(-) create mode 100644 packages/app/src/utils/server-compat.test.ts create mode 100644 packages/app/src/utils/server-compat.ts create mode 100644 packages/app/src/utils/server-protocol.test.ts create mode 100644 packages/app/src/utils/server-protocol.ts create mode 100644 packages/app/vendor/opencode-ai-client-1.17.13.tgz create mode 100644 packages/core/test/provider-mistral.test.ts create mode 100644 patches/@ai-sdk%2Fmistral@3.0.51.patch diff --git a/bun.lock b/bun.lock index 24e3c97c4ed2..fb36652e940b 100644 --- a/bun.lock +++ b/bun.lock @@ -61,6 +61,7 @@ "@dnd-kit/helpers": "0.5.0", "@dnd-kit/solid": "0.5.0", "@kobalte/core": "catalog:", + "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "workspace:*", @@ -350,7 +351,7 @@ "@ai-sdk/google": "3.0.73", "@ai-sdk/google-vertex": "4.0.128", "@ai-sdk/groq": "3.0.31", - "@ai-sdk/mistral": "3.0.27", + "@ai-sdk/mistral": "3.0.51", "@ai-sdk/openai": "3.0.84", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/perplexity": "3.0.26", @@ -470,7 +471,9 @@ "packages/docs": { "name": "@opencode-ai/docs", "devDependencies": { + "effect": "catalog:", "mint": "4.2.666", + "prettier": "3.6.2", }, }, "packages/effect-drizzle-sqlite": { @@ -706,6 +709,7 @@ "version": "1.18.4", "dependencies": { "@kobalte/core": "catalog:", + "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -1079,6 +1083,7 @@ "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", + "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", @@ -1205,7 +1210,7 @@ "@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="], - "@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZXe7nZQgliDdjz5ufH5RKpHWxbN72AzmzzKGbF/z+0K9GN5tUCnftrQRvTRFHA5jAzTapcm2BEevmGLVbMkW+A=="], + "@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.51", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.40" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-83eXY6p0lUFhSuMvNDmTKDuMciK5XDAWDlNh5c0L80tKjmtCFRItA1MZHp4IKe1r7eK8Rb5nN7qtxqMLUFRIRw=="], "@ai-sdk/openai": ["@ai-sdk/openai@3.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ALmj/53EXpcRqMbGpPJPP4UOSWw0q4VGpnDo7YctvsynjkrKDmoneDG/1a7VQnSPYHnJp6tTRMf5ZdxZ5whulg=="], @@ -6151,7 +6156,9 @@ "@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - "@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + "@ai-sdk/mistral/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="], + + "@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.40", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OL5IrpUm9Y8Dwy+w/vvFwPotS6m52O9W0op2oXgXdCROMJIBalBI0oro6OIBYkPxvm5Xg02GSkoQN25RlR0bnw=="], "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], @@ -6581,6 +6588,8 @@ "@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], + "@opencode-ai/app/@opencode-ai/client": ["@opencode-ai/client@vendor/opencode-ai-client-1.17.13.tgz", {}, "sha512-1cYJikTCrXNhnS2qQ3P3rtdbGqhvJKksswG0amJNqgaeUfz3xDlKEDx+YoIosT7Cqk/AtO18jtip0lYL3TVdHQ=="], + "@opencode-ai/cli/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "@opencode-ai/console-app/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="], @@ -6599,6 +6608,8 @@ "@opencode-ai/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], + "@opencode-ai/session-ui/@opencode-ai/client": ["@opencode-ai/client@../app/vendor/opencode-ai-client-1.17.13.tgz", {}, "sha512-1cYJikTCrXNhnS2qQ3P3rtdbGqhvJKksswG0amJNqgaeUfz3xDlKEDx+YoIosT7Cqk/AtO18jtip0lYL3TVdHQ=="], + "@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/storybook/@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="], @@ -6927,6 +6938,8 @@ "aggregate-error/indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], + "ai-gateway-provider/@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZXe7nZQgliDdjz5ufH5RKpHWxbN72AzmzzKGbF/z+0K9GN5tUCnftrQRvTRFHA5jAzTapcm2BEevmGLVbMkW+A=="], + "ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="], "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -8047,6 +8060,8 @@ "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], + "ai-gateway-provider/@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + "ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="], "ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="], diff --git a/nix/hashes.json b/nix/hashes.json index 963d46ecf49a..407d7812fb22 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-qt11SKmOjq0KU542QFbs+u7YyJicn4drCcwCdg325yk=", - "aarch64-linux": "sha256-z68doReXTrWS7HeiAjc0btIjAsvzeZZ7hXAlHr0c77Q=", - "aarch64-darwin": "sha256-PILYH1Pi8XBvSkuZ+1sNnUTao5kba+m5Z8iJKx6YXPo=", - "x86_64-darwin": "sha256-KpcJzP4m0SUavu/WaSffgzOxrHq8ljdy0GOzs9p16lo=" + "x86_64-linux": "sha256-0kcwV34P2C3yKg2eG9W2nW+OedrSBb+1TdpuUeYtauY=", + "aarch64-linux": "sha256-yHVygApQchAB34wrtFR4GU0CkmZOlLsl3wsp15u0xzs=", + "aarch64-darwin": "sha256-DyalcwyK2Wn5R6249keFcNVECbgtjYNjscOFqTi88FI=", + "x86_64-darwin": "sha256-BkGw0GWN9W9q+/g4FYR0MqxUuFP80BPoERO+ypz/arQ=" } } diff --git a/package.json b/package.json index 2110c4d9af0c..6332861c9156 100644 --- a/package.json +++ b/package.json @@ -160,6 +160,7 @@ "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", + "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", "pacote@21.5.0": "patches/pacote@21.5.0.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", diff --git a/packages/app/e2e/performance/timeline-stability/fixture.ts b/packages/app/e2e/performance/timeline-stability/fixture.ts index 5095d95db029..df67da5a6621 100644 --- a/packages/app/e2e/performance/timeline-stability/fixture.ts +++ b/packages/app/e2e/performance/timeline-stability/fixture.ts @@ -97,6 +97,7 @@ export async function setupTimeline( locale?: string deviceScaleFactor?: number seedHistory?: boolean + protocol?: "v1" | "v2" } = {}, ) { const sessions = input.sessions ?? [session()] @@ -114,6 +115,7 @@ export async function setupTimeline( retry: input.eventRetry ?? 20, }) await mockOpenCodeServer(page, { + protocol: input.protocol, directory, project: project(), provider: provider(), diff --git a/packages/app/e2e/regression/session-timeline-transport.spec.ts b/packages/app/e2e/regression/session-timeline-transport.spec.ts index 850e966d0b0f..778ff3a3af94 100644 --- a/packages/app/e2e/regression/session-timeline-transport.spec.ts +++ b/packages/app/e2e/regression/session-timeline-transport.spec.ts @@ -89,8 +89,8 @@ test("reconnects after a stream error", async ({ page }) => { expect((await timeline.transport.connections())[0]?.endedBy).toBe("error") }) -test("records event IDs and reconnect Last-Event-ID headers", async ({ page }) => { - const timeline = await setupTimeline(page, { eventRetry: 10 }) +test("does not request replay when reconnecting the volatile V2 event stream", async ({ page }) => { + const timeline = await setupTimeline(page, { eventRetry: 10, protocol: "v2" }) const first = await timeline.transport.send(partUpdated(textPart("prt_transport_id", "event with id")), { id: "timeline-event-7", }) @@ -100,7 +100,7 @@ test("records event IDs and reconnect Last-Event-ID headers", async ({ page }) = const connection = await timeline.transport.waitForConnection({ after: first.connectionID }) expect(first.eventID).toBe("timeline-event-7") - expect(connection.headers["last-event-id"]).toBe("timeline-event-7") + expect(connection.headers["last-event-id"]).toBeUndefined() }) test("passes through non-event fetches", async ({ page }) => { diff --git a/packages/app/e2e/utils/mock-server.ts b/packages/app/e2e/utils/mock-server.ts index 34c60ba7f4d8..5cca83895f50 100644 --- a/packages/app/e2e/utils/mock-server.ts +++ b/packages/app/e2e/utils/mock-server.ts @@ -4,6 +4,7 @@ const emptyList = new Set(["/skill", "/command", "/lsp", "/formatter", "/vcs/sta const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp", "/experimental/resource"]) export interface MockServerConfig { + protocol?: "v1" | "v2" provider: unknown directory: string project: unknown @@ -53,8 +54,20 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { if (url.port !== targetPort && url.port !== appPort) return route.fallback() const path = url.pathname - if (path === "/global/event" || path === "/event") return sse(route, config.events?.(), config.eventRetry) - if (path === "/global/health") return json(route, { healthy: true }) + if (path === "/global/event" || path === "/event" || path === "/api/event") { + const events = config.events?.() + return sse( + route, + path === "/api/event" + ? [{ id: "evt_mock_connected", type: "server.connected", data: {} }, ...(events?.map(currentEvent) ?? [])] + : events, + config.eventRetry, + ) + } + if (path === "/global/health") + return config.protocol === "v2" ? json(route, {}, undefined, 404) : json(route, { healthy: true }) + if (path === "/api/health" && config.protocol === "v2") + return json(route, { healthy: true, version: "2.0.0", pid: 1 }) if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: true }) if (path === "/permission") return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? [])) @@ -83,10 +96,129 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { }, data: [], }) + if (path === "/api/agent") + return json(route, { + location: location(config), + data: [ + { + id: "build", + name: "Build", + mode: "primary", + hidden: false, + request: { settings: {}, headers: {}, body: {} }, + permissions: [], + }, + ], + }) + if (path === "/api/command") return json(route, { location: location(config), data: [] }) + if (path === "/api/mcp") return json(route, { location: location(config), data: [] }) + if (path === "/api/mcp/resource") + return json(route, { location: location(config), data: { resources: [], templates: [] } }) + const integration = path.match(/^\/api\/integration\/([^/]+)$/)?.[1] + if (integration && route.request().method() === "GET") + return json(route, { + location: location(config), + data: { id: integration, name: integration, methods: [{ type: "key", label: "API key" }], connections: [] }, + }) + if (/^\/api\/integration\/[^/]+\/connect\/key$/.test(path) && route.request().method() === "POST") + return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) + if (path === "/api/project") return json(route, [config.project]) + if (path === "/api/project/current") + return json(route, { id: (config.project as { id?: string }).id, directory: config.directory }) + if (path.startsWith("/api/project/") && route.request().method() === "PATCH") return json(route, config.project) + if (path === "/api/path") + return json(route, { + state: config.directory, + config: config.directory, + worktree: config.directory, + directory: config.directory, + home: "C:/OpenCode", + }) + if (path === "/api/permission/request") + return json(route, { + location: location(config), + data: (typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? [])).map( + currentPermission, + ), + }) + if (path === "/api/question/request") + return json(route, { + location: location(config), + data: typeof config.questions === "function" ? config.questions() : (config.questions ?? []), + }) + if (path === "/api/vcs") + return json(route, { location: location(config), data: { branch: "main", defaultBranch: "main" } }) + if (path === "/api/vcs/status") return json(route, { location: location(config), data: [] }) + if (path === "/api/vcs/diff") return json(route, { location: location(config), data: config.vcsDiff ?? [] }) + if (path === "/api/pty/shells") return json(route, { location: location(config), data: [] }) + if (/^\/api\/pty\/[^/]+\/connect-token$/.test(path)) + return json(route, { location: location(config), data: { ticket: "e2e-ticket", expires_in: 60 } }) if (emptyObject.has(path)) return json(route, {}) if (emptyList.has(path)) return json(route, []) + if (path === "/api/session") { + const directory = url.searchParams.get("directory") + const parentID = url.searchParams.get("parentID") + const limit = Number(url.searchParams.get("limit") ?? 50) + const offset = Number(url.searchParams.get("cursor") ?? 0) + const sessions = config.sessions + .filter((session) => !directory || session.directory === directory) + .filter((session) => parentID !== "null" || session.parentID === undefined) + .filter((session) => { + const search = url.searchParams.get("search")?.toLowerCase() + return ( + !search || + String(session.title ?? "") + .toLowerCase() + .includes(search) + ) + }) + const ordered = url.searchParams.get("order") === "asc" ? sessions.toReversed() : sessions + const data = ordered.slice(offset, offset + limit) + const next = offset + limit < ordered.length ? String(offset + limit) : undefined + return json(route, { + data: data.map((session) => currentSession(session, config.directory)), + cursor: { next }, + }) + } + if (path === "/api/session/active") { + const statuses = (config.sessionStatus ?? {}) as Record + return json(route, { + data: Object.fromEntries( + Object.entries(statuses).flatMap(([id, status]) => + status.type === "idle" ? [] : [[id, { type: "running" }]], + ), + ), + }) + } + if (/^\/api\/session\/[^/]+\/shell$/.test(path) && route.request().method() === "POST") { + return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) + } + if (/^\/api\/session\/[^/]+\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST") { + return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) + } + if (/^\/api\/session\/[^/]+\/permission\/[^/]+\/reply$/.test(path) && route.request().method() === "POST") { + return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) + } + if ( + /^\/api\/session\/[^/]+\/(archive|rename|interrupt|revert\/clear|revert\/commit)$/.test(path) && + route.request().method() === "POST" + ) { + return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) + } + if (/^\/api\/session\/[^/]+$/.test(path) && route.request().method() === "DELETE") { + return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) + } if (path in staticRoutes) return json(route, staticRoutes[path]) + const currentSessionMatch = path.match(/^\/api\/session\/([^/]+)$/) + if (currentSessionMatch) { + const session = config.sessions.find((item) => item.id === currentSessionMatch[1]) + if (!session) return json(route, { error: "Session not found" }, undefined, 404) + return json(route, { + data: currentSession(session, config.directory), + }) + } + const sessionMatch = path.match(/^\/session\/([^/]+)$/) if (sessionMatch) { const session = config.sessions.find((s) => s.id === sessionMatch[1]) @@ -107,6 +239,24 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { if (/^\/session\/[^/]+\/(children|diff)$/.test(path)) return json(route, []) + const currentMessagesMatch = path.match(/^\/api\/session\/([^/]+)\/message$/) + if (currentMessagesMatch) { + const token = url.searchParams.get("cursor") ?? undefined + const before = token ? cursors.get(token) : undefined + if (token && !before) return json(route, { error: "Invalid cursor" }, undefined, 400) + config.onMessages?.({ sessionID: currentMessagesMatch[1], before, phase: "start" }) + await config.beforeMessagesResponse?.({ sessionID: currentMessagesMatch[1]!, before }) + if (config.messageDelay !== undefined) await new Promise((resolve) => setTimeout(resolve, config.messageDelay)) + const pageData = config.pageMessages(currentMessagesMatch[1], Number(url.searchParams.get("limit") ?? 50), before) + config.onMessages?.({ sessionID: currentMessagesMatch[1], before, phase: "end" }) + const cursor = pageData.cursor ? `cursor_${++nextCursor}` : undefined + if (cursor) cursors.set(cursor, pageData.cursor!) + return json(route, { + data: pageData.items.map(currentMessage).reverse(), + cursor: { next: cursor }, + }) + } + const messagesMatch = path.match(/^\/session\/([^/]+)\/message$/) if (messagesMatch) { const token = url.searchParams.get("before") ?? undefined @@ -129,6 +279,115 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { }) } +function location(config: MockServerConfig) { + return { + directory: config.directory, + project: { id: (config.project as { id?: string }).id, directory: config.directory }, + } +} + +function currentPermission(value: unknown) { + const permission = value as Record + if (permission.action) return permission + const tool = permission.tool as { messageID?: string; callID?: string } | undefined + return { + id: permission.id, + sessionID: permission.sessionID, + action: permission.permission, + resources: permission.patterns ?? [], + save: permission.always, + metadata: permission.metadata, + source: + tool?.messageID && tool.callID ? { type: "tool", messageID: tool.messageID, callID: tool.callID } : undefined, + } +} + +export function currentSession(session: { id: string } & Record, fallbackDirectory?: string) { + const time = session.time && typeof session.time === "object" ? session.time : {} + return { + id: session.id, + parentID: session.parentID, + projectID: session.projectID ?? "project", + agent: session.agent ?? "build", + model: session.model ?? { id: "mock-model", providerID: "mock-provider" }, + cost: session.cost ?? 0, + tokens: session.tokens ?? { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { + created: "created" in time && typeof time.created === "number" ? time.created : 0, + updated: "updated" in time && typeof time.updated === "number" ? time.updated : 0, + ...(session.time && typeof session.time === "object" && "archived" in session.time + ? { archived: session.time.archived } + : {}), + }, + title: session.title ?? session.id, + location: { + directory: typeof session.directory === "string" ? session.directory : fallbackDirectory, + ...(typeof session.workspaceID === "string" ? { workspaceID: session.workspaceID } : {}), + }, + subpath: session.path, + revert: session.revert, + } +} + +function currentMessage(value: unknown) { + const item = value as { + info: Record & { id: string; role: "user" | "assistant"; time: { created: number } } + parts: Array & { type: string }> + } + if (item.info.role === "user") { + return { + id: item.info.id, + type: "user", + time: item.info.time, + text: item.parts + .flatMap((part) => (part.type === "text" && typeof part.text === "string" ? [part.text] : [])) + .join("\n"), + } + } + return { + id: item.info.id, + type: "assistant", + time: item.info.time, + agent: item.info.agent ?? "build", + model: { id: item.info.modelID ?? "model", providerID: item.info.providerID ?? "provider" }, + cost: item.info.cost, + tokens: item.info.tokens, + error: item.info.error, + content: item.parts.flatMap((part) => { + if (part.type === "text" || part.type === "reasoning") return [{ type: part.type, text: part.text ?? "" }] + if (part.type !== "tool") return [] + const state = part.state as Record + return [ + { + type: "tool", + id: part.id, + name: part.tool, + time: state.time ?? { created: item.info.time.created }, + state: + state.status === "pending" + ? { status: "streaming", input: state.raw ?? JSON.stringify(state.input ?? {}) } + : state.status === "completed" + ? { + status: "completed", + input: state.input ?? {}, + structured: state.metadata ?? {}, + content: [{ type: "text", text: state.output ?? "" }], + } + : state.status === "error" + ? { + status: "error", + input: state.input ?? {}, + structured: state.metadata ?? {}, + content: [], + error: { type: "ToolError", message: state.error ?? "Tool failed" }, + } + : { status: "running", input: state.input ?? {}, structured: state.metadata ?? {}, content: [] }, + }, + ] + }), + } +} + function json(route: Route, body: unknown, headers?: Record, status = 200) { return route.fulfill({ status, @@ -149,3 +408,18 @@ function sse(route: Route, events?: unknown[], retry?: number) { body: `${retry === undefined ? "" : `retry: ${retry}\n\n`}${events?.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") || ": ok\n\n"}`, }) } + +function currentEvent(input: unknown) { + if (!input || typeof input !== "object" || !("payload" in input)) return input + const envelope = input as { directory?: string; payload?: unknown } + if (!envelope.payload || typeof envelope.payload !== "object") return input + const payload = envelope.payload as { id?: string; type?: string; properties?: unknown } + if (!payload.type) return input + return { + id: payload.id ?? `evt_mock_${Date.now()}`, + created: Date.now(), + type: payload.type, + data: payload.properties ?? {}, + location: envelope.directory && envelope.directory !== "global" ? { directory: envelope.directory } : undefined, + } +} diff --git a/packages/app/e2e/utils/sse-transport.ts b/packages/app/e2e/utils/sse-transport.ts index 55420485f399..15c3577279f6 100644 --- a/packages/app/e2e/utils/sse-transport.ts +++ b/packages/app/e2e/utils/sse-transport.ts @@ -3,7 +3,7 @@ import type { Page } from "@playwright/test" export type SseConnectionRecord = { id: number url: string - path: "/global/event" | "/event" + path: "/global/event" | "/event" | "/api/event" headers: Record openedAt: number endedAt?: number @@ -93,6 +93,21 @@ export async function installSseTransport( eventOptions.retry === undefined ? "" : `retry: ${eventOptions.retry}\n`, `data: ${JSON.stringify(payload)}\n\n`, ].join("") + const currentEvent = (input: unknown) => { + if (!input || typeof input !== "object" || !("payload" in input)) return input + const envelope = input as { directory?: string; payload?: unknown } + if (!envelope.payload || typeof envelope.payload !== "object") return input + const payload = envelope.payload as { id?: string; type?: string; properties?: unknown } + if (!payload.type) return input + return { + id: payload.id ?? `evt_mock_${Date.now()}`, + created: Date.now(), + type: payload.type, + data: payload.properties ?? {}, + location: + envelope.directory && envelope.directory !== "global" ? { directory: envelope.directory } : undefined, + } + } const acknowledge = ( connection: Connection, bytes: number, @@ -140,15 +155,13 @@ export async function installSseTransport( output.forEach((chunk) => connection.controller.enqueue(chunk)) return acknowledge(connection, input.bytes.length, output.length) } - const encoded = input.deliveries.map((delivery) => ({ - delivery, - bytes: encoder.encode(frame(delivery.payload, delivery.options)), - })) + const encoded = input.deliveries.map((delivery) => { + const payload = connection.path === "/api/event" ? currentEvent(delivery.payload) : delivery.payload + return { delivery, payload, bytes: encoder.encode(frame(payload, delivery.options)) } + }) encoded.forEach((item) => marker(item.delivery.options?.marker)) if (input.burst) { - const bytes = encoder.encode( - encoded.map((item) => frame(item.delivery.payload, item.delivery.options)).join(""), - ) + const bytes = encoder.encode(encoded.map((item) => frame(item.payload, item.delivery.options)).join("")) connection.controller.enqueue(bytes) return encoded.map((item) => acknowledge(connection, item.bytes.byteLength, 1, item.delivery.options?.id)) } @@ -161,7 +174,10 @@ export async function installSseTransport( const fetch = (input: RequestInfo | URL, init?: RequestInit) => { const request = new Request(input, init) const url = new URL(request.url) - if (url.origin !== server || (url.pathname !== "/global/event" && url.pathname !== "/event")) + if ( + url.origin !== server || + (url.pathname !== "/global/event" && url.pathname !== "/event" && url.pathname !== "/api/event") + ) return originalFetch(request) const id = ++nextConnectionID @@ -177,6 +193,10 @@ export async function installSseTransport( record.controller = controller connections.push(record) if (retry !== undefined) controller.enqueue(encoder.encode(`retry: ${retry}\n\n`)) + if (url.pathname === "/api/event") + controller.enqueue( + encoder.encode(frame({ id: `evt_mock_connected_${id}`, type: "server.connected", data: {} })), + ) request.signal.addEventListener( "abort", () => { diff --git a/packages/app/package.json b/packages/app/package.json index faf019446128..f6a1bf9d2ea7 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -53,6 +53,7 @@ "@dnd-kit/helpers": "0.5.0", "@dnd-kit/solid": "0.5.0", "@kobalte/core": "catalog:", + "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "workspace:*", diff --git a/packages/app/src/context/server-sdk.test.ts b/packages/app/src/context/server-sdk.test.ts index 052faf496a5e..767d16ffa05a 100644 --- a/packages/app/src/context/server-sdk.test.ts +++ b/packages/app/src/context/server-sdk.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test" -import { coalesceServerEvents, enqueueServerEvent, resumeStreamAfterPageShow } from "./server-sdk" +import { adaptServerEvent, coalesceServerEvents, enqueueServerEvent, resumeStreamAfterPageShow } from "./server-sdk" +import type { OpenCodeEvent } from "@opencode-ai/client/promise" import type { Event } from "@opencode-ai/sdk/v2/client" describe("resumeStreamAfterPageShow", () => { @@ -14,6 +15,23 @@ describe("resumeStreamAfterPageShow", () => { }) }) +describe("adaptServerEvent", () => { + test("preserves V2 events while adapting permission requests for existing consumers", () => { + const current = { + id: "evt_1", + created: 1, + type: "permission.v2.asked", + data: { id: "perm_1", sessionID: "ses_1", action: "read", resources: ["src/**"] }, + } as OpenCodeEvent + + expect(adaptServerEvent(current)).toMatchObject({ + type: "permission.asked", + properties: { id: "perm_1", sessionID: "ses_1", permission: "read", patterns: ["src/**"] }, + current, + }) + }) +}) + describe("coalesceServerEvents", () => { const delta = (value: string, field = "text", partID = "part") => ({ directory: "/repo", @@ -34,6 +52,24 @@ describe("coalesceServerEvents", () => { expect(result[0]?.payload).toMatchObject({ id: "second", properties: { delta: "hello world" } }) }) + test("merges adjacent current text deltas", () => { + const current = (id: string, value: string) => + adaptServerEvent({ + id, + created: 1, + type: "session.text.delta", + location: { directory: "/repo" }, + data: { sessionID: "ses", assistantMessageID: "msg", ordinal: 0, delta: value }, + } as OpenCodeEvent) + const result = coalesceServerEvents([ + { directory: "/repo", payload: current("evt_1", "hello ") }, + { directory: "/repo", payload: current("evt_2", "world") }, + ]) + + expect(result).toHaveLength(1) + expect(result[0]?.payload.current).toMatchObject({ id: "evt_2", data: { delta: "hello world" } }) + }) + test("preserves event boundaries and distinct fields", () => { const status = { directory: "/repo", diff --git a/packages/app/src/context/server-sdk.tsx b/packages/app/src/context/server-sdk.tsx index 06597e56e7ba..62c585779487 100644 --- a/packages/app/src/context/server-sdk.tsx +++ b/packages/app/src/context/server-sdk.tsx @@ -1,21 +1,60 @@ +import type { OpenCodeEvent } from "@opencode-ai/client/promise" import type { Event } from "@opencode-ai/sdk/v2/client" import { createSimpleContext } from "@opencode-ai/ui/context" import { createGlobalEmitter } from "@solid-primitives/event-bus" import { makeEventListener } from "@solid-primitives/event-listener" import { type Accessor, batch, createMemo, onCleanup, onMount } from "solid-js" -import { createSdkForServer } from "@/utils/server" +import { createApiForServer, createSdkForServer, type ServerApi } from "@/utils/server" import { useLanguage } from "./language" import { usePlatform } from "./platform" import { ServerConnection, useServer } from "./server" import { createRefCountMap } from "@/utils/refcount" import { useGlobal } from "./global" import { ServerScope } from "@/utils/server-scope" +import { detectServerProtocol, type ServerProtocol } from "@/utils/server-protocol" +import { createCompatibleApi, type CompatibleApi } from "@/utils/server-compat" const isAbortError = (error: unknown) => error !== null && typeof error === "object" && "name" in error && error.name === "AbortError" const isStreamClosed = (error: unknown, signal?: AbortSignal) => isAbortError(error) || signal?.aborted === true -type QueuedServerEvent = { directory: string; payload: Event } +export type ServerEvent = Event & { current?: OpenCodeEvent } +type QueuedServerEvent = { directory: string; payload: ServerEvent } +type CurrentDelta = Extract< + OpenCodeEvent, + { type: "session.text.delta" | "session.reasoning.delta" | "session.tool.input.delta" | "session.compaction.delta" } +> + +export function adaptServerEvent(event: OpenCodeEvent): ServerEvent { + if (event.type === "permission.v2.asked") { + return { + id: event.id, + type: "permission.asked", + properties: { + id: event.data.id, + sessionID: event.data.sessionID, + permission: event.data.action, + patterns: event.data.resources, + always: event.data.save ?? [], + metadata: event.data.metadata ?? {}, + tool: + event.data.source?.type === "tool" + ? { messageID: event.data.source.messageID, callID: event.data.source.callID } + : undefined, + }, + current: event, + } as ServerEvent + } + if (event.type === "permission.v2.replied") + return { id: event.id, type: "permission.replied", properties: event.data, current: event } as ServerEvent + if (event.type === "question.v2.asked") + return { id: event.id, type: "question.asked", properties: event.data, current: event } as ServerEvent + if (event.type === "question.v2.replied") + return { id: event.id, type: "question.replied", properties: event.data, current: event } as ServerEvent + if (event.type === "question.v2.rejected") + return { id: event.id, type: "question.rejected", properties: event.data, current: event } as ServerEvent + return { id: event.id, type: event.type, properties: event.data, current: event } as ServerEvent +} const coalescedKey = (event: QueuedServerEvent) => { if (event.payload.type === "lsp.updated") return `lsp.updated:${event.directory}` @@ -40,6 +79,34 @@ export function enqueueServerEvent(queue: QueuedServerEvent[], event: QueuedServ export function coalesceServerEvents(events: QueuedServerEvent[]) { const output: QueuedServerEvent[] = [] events.forEach((event) => { + const current = currentDelta(event.payload.current) + if (current) { + const previous = output[output.length - 1] + const prior = currentDelta(previous?.payload.current) + if ( + previous && + prior && + previous.directory === event.directory && + currentDeltaKey(prior) === currentDeltaKey(current) + ) { + const fragment = currentDeltaFragment(prior) + currentDeltaFragment(current) + const data = + current.type === "session.compaction.delta" + ? { ...current.data, text: fragment } + : { ...current.data, delta: fragment } + output[output.length - 1] = { + directory: event.directory, + payload: { + ...event.payload, + properties: data, + current: { ...current, data } as CurrentDelta, + } as ServerEvent, + } + return + } + output.push(event) + return + } if (event.payload.type !== "message.part.delta") { output.push(event) return @@ -71,12 +138,52 @@ export function coalesceServerEvents(events: QueuedServerEvent[]) { return output } +function currentDelta(event: OpenCodeEvent | undefined): CurrentDelta | undefined { + if ( + event?.type === "session.text.delta" || + event?.type === "session.reasoning.delta" || + event?.type === "session.tool.input.delta" || + event?.type === "session.compaction.delta" + ) + return event +} + +function currentDeltaKey(event: CurrentDelta) { + if (event.type === "session.tool.input.delta") + return `${event.type}:${event.data.sessionID}:${event.data.assistantMessageID}:${event.data.callID}` + if (event.type === "session.compaction.delta") return `${event.type}:${event.data.sessionID}` + return `${event.type}:${event.data.sessionID}:${event.data.assistantMessageID}:${event.data.ordinal}` +} + +function currentDeltaFragment(event: CurrentDelta) { + return event.type === "session.compaction.delta" ? event.data.text : event.data.delta +} + export function resumeStreamAfterPageShow(event: PageTransitionEvent, start: () => unknown) { if (!event.persisted) return start() } -function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerScope) { +type ServerEventEmitter = ReturnType> +type ServerSDKBase = { + server: ServerConnection.Any + scope: ServerScope + protocol: Promise + url: string + client: ReturnType + api: CompatibleApi + currentApi: ServerApi + event: { + on: ServerEventEmitter["on"] + listen: ServerEventEmitter["listen"] + start: () => Promise | undefined + } + createClient: ( + opts: Omit[0], "server" | "fetch">, + ) => ReturnType +} + +function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerScope): ServerSDKBase { const platform = usePlatform() const abort = new AbortController() @@ -91,13 +198,15 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS } })() + const eventApi = createApiForServer({ server: server.http, fetch: eventFetch }) const eventSdk = createSdkForServer({ signal: abort.signal, fetch: eventFetch, server: server.http, }) + const protocol = detectServerProtocol(server.http, platform.fetch ?? globalThis.fetch) const emitter = createGlobalEmitter<{ - [key: string]: Event + [key: string]: ServerEvent }>() type Queued = QueuedServerEvent @@ -142,21 +251,6 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS let run: Promise | undefined let started = false let generation = 0 - const HEARTBEAT_TIMEOUT_MS = 15_000 - let lastEventAt = Date.now() - let heartbeat: ReturnType | undefined - const resetHeartbeat = () => { - lastEventAt = Date.now() - if (heartbeat) clearTimeout(heartbeat) - heartbeat = setTimeout(() => { - attempt?.abort() - }, HEARTBEAT_TIMEOUT_MS) - } - const clearHeartbeat = () => { - if (!heartbeat) return - clearTimeout(heartbeat) - heartbeat = undefined - } const start = () => { if (started) return run @@ -168,35 +262,24 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS // oxlint-disable-next-line no-unmodified-loop-condition -- `started` is set to false by stop() which also aborts; both flags are checked to allow graceful exit while (!abort.signal.aborted && started && generation === active) { attempt = new AbortController() - lastEventAt = Date.now() const onAbort = () => { attempt?.abort() } abort.signal.addEventListener("abort", onAbort) try { - const events = await eventSdk.global.event({ - signal: attempt.signal, - onSseError: (error) => { - if (isStreamClosed(error, attempt?.signal)) return - if (streamErrorLogged) return - streamErrorLogged = true - console.error("[global-sdk] event stream error", { - url: server.http.url, - fetch: eventFetch ? "platform" : "webview", - error, - }) - }, - }) + const kind = await protocol + const events = + kind === "v1" + ? (await eventSdk.global.event({ signal: attempt.signal })).stream + : eventApi.event.subscribe({ signal: attempt.signal }) let yielded = Date.now() - resetHeartbeat() - for await (const event of events.stream) { - resetHeartbeat() + for await (const event of events) { streamErrorLogged = false - if (event.payload.type !== "sync") { - const directory = event.directory ?? "global" - const payload = event.payload as Event - if (enqueueServerEvent(queue, { directory, payload })) schedule() - } + const legacy = "payload" in event + if (legacy && event.payload.type === "sync") continue + const directory = legacy ? (event.directory ?? "global") : (event.location?.directory ?? "global") + const payload = legacy ? (event.payload as Event) : adaptServerEvent(event) + if (enqueueServerEvent(queue, { directory, payload })) schedule() if (Date.now() - yielded < STREAM_YIELD_MS) continue yielded = Date.now() @@ -214,7 +297,6 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS } finally { abort.signal.removeEventListener("abort", onAbort) attempt = undefined - clearHeartbeat() } if (abort.signal.aborted || !started || generation !== active) return @@ -233,18 +315,11 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS started = false generation++ attempt?.abort() - clearHeartbeat() } onMount(() => { makeEventListener(window, "pagehide", stop) makeEventListener(window, "pageshow", (event) => resumeStreamAfterPageShow(event, start)) - makeEventListener(document, "visibilitychange", () => { - if (document.visibilityState !== "visible") return - if (!started) return - if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return - attempt?.abort() - }) }) onCleanup(() => { @@ -258,12 +333,24 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS fetch: platform.fetch, throwOnError: true, }) + const currentApi: ServerApi = createApiForServer({ server: server.http, fetch: platform.fetch }) + const legacy = (directory?: string) => + createSdkForServer({ + server: server.http, + fetch: platform.fetch, + throwOnError: true, + directory, + }) + const api = createCompatibleApi({ protocol, current: currentApi, legacy }) return { server, scope, + protocol, url: server.http.url, client: sdk, + api, + currentApi, event: { on: emitter.on.bind(emitter), listen: emitter.listen.bind(emitter), @@ -279,7 +366,6 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS } } -type ServerSDKBase = ReturnType export type ServerSDK = ServerSDKBase & { ensureDirSdkContext: (directory: string) => ReturnType } @@ -309,7 +395,7 @@ export const { use: useServerSDK, provider: ServerSDKProvider } = createSimpleCo }) type SDKEventMap = { - [key in Event["type"]]: Extract + [key in Event["type"]]: Extract } function createDirSdkContext(directory: string, serverSDK: ServerSDKBase) { @@ -329,6 +415,12 @@ function createDirSdkContext(directory: string, serverSDK: ServerSDKBase) { scope: serverSDK.scope, directory, client, + api: createCompatibleApi({ + protocol: serverSDK.protocol, + current: serverSDK.currentApi, + legacy: (next) => serverSDK.createClient({ directory: next ?? directory, throwOnError: true }), + directory, + }), event: emitter, get url() { return serverSDK.url diff --git a/packages/app/src/utils/server-compat.test.ts b/packages/app/src/utils/server-compat.test.ts new file mode 100644 index 000000000000..4907eb41eb15 --- /dev/null +++ b/packages/app/src/utils/server-compat.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, test } from "bun:test" +import { createApiForServer, createSdkForServer } from "./server" +import { createCompatibleApi } from "./server-compat" + +function setup(protocol: "v1" | "v2" | Promise<"v1" | "v2">) { + const requests: Request[] = [] + const fetcher = Object.assign( + async (input: string | URL | Request, init?: RequestInit) => { + const request = new Request(input, init) + requests.push(request) + if (request.method === "PATCH") { + return Response.json({ + id: "ses_1", + slug: "ses_1", + projectID: "project", + directory: "/repo", + title: "Session", + version: "1", + time: { created: 1, updated: 1 }, + }) + } + if (request.method === "POST" && request.url.endsWith("/prompt_async")) + return new Response(undefined, { status: 204 }) + if (request.method === "POST" && request.url.endsWith("/prompt")) { + return Response.json({ + admittedSeq: 1, + id: "msg_1", + sessionID: "ses_1", + timeCreated: 1, + type: "user", + data: { text: "hello" }, + delivery: "steer", + }) + } + if (request.method === "GET") return Response.json([]) + return new Response(undefined, { status: 204 }) + }, + { preconnect: globalThis.fetch.preconnect }, + ) + const server = { url: "http://localhost:4096" } + const api = createCompatibleApi({ + protocol: typeof protocol === "string" ? Promise.resolve(protocol) : protocol, + current: createApiForServer({ server, fetch: fetcher }), + legacy: (directory) => createSdkForServer({ server, fetch: fetcher, directory, throwOnError: true }), + directory: "/repo", + }) + return { api, requests } +} + +describe("createCompatibleApi", () => { + test("routes V1 archive through the legacy session update", async () => { + const { api, requests } = setup("v1") + await api.session.archive({ sessionID: "ses_1", directory: "/repo" }) + + const url = new URL(requests[0]!.url) + expect(url.pathname).toBe("/session/ses_1") + expect(requests[0]!.headers.get("x-opencode-directory")).toBe("%2Frepo") + expect(requests[0]!.method).toBe("PATCH") + expect(await requests[0]!.json()).toMatchObject({ time: { archived: expect.any(Number) } }) + }) + + test("converts current prompts to the V1 prompt contract", async () => { + const { api, requests } = setup("v1") + await api.session.prompt({ + sessionID: "ses_1", + id: "msg_1", + text: "hello", + agent: "build", + model: { providerID: "provider", modelID: "model" }, + }) + + expect(new URL(requests[0]!.url).pathname).toBe("/session/ses_1/prompt_async") + expect(await requests[0]!.json()).toMatchObject({ + messageID: "msg_1", + agent: "build", + model: { providerID: "provider", modelID: "model" }, + parts: [{ type: "text", text: "hello" }], + }) + }) + + test("keeps V2 session actions on the current API", async () => { + const { api, requests } = setup("v2") + await api.session.archive({ sessionID: "ses_1" }) + + expect(new URL(requests[0]!.url).pathname).toBe("/api/session/ses_1/archive") + expect(requests[0]!.method).toBe("POST") + }) + + test("resolves protocol detection once across implementation methods", async () => { + let detections = 0 + const resolved = Promise.resolve<"v1" | "v2">("v2") + const protocol = new Proxy(resolved, { + get(target, property) { + if (property !== "then") return Reflect.get(target, property, target) + detections++ + return target.then.bind(target) + }, + }) + const { api } = setup(protocol) + + await api.session.archive({ sessionID: "ses_1" }) + await api.session.list() + + expect(detections).toBe(1) + }) + + test("uses the global V1 session search endpoint", async () => { + const { api, requests } = setup("v1") + await api.session.list({ parentID: null, search: "session", limit: 50 }) + + expect(new URL(requests[0]!.url).pathname).toBe("/experimental/session") + }) +}) diff --git a/packages/app/src/utils/server-compat.ts b/packages/app/src/utils/server-compat.ts new file mode 100644 index 000000000000..177251690075 --- /dev/null +++ b/packages/app/src/utils/server-compat.ts @@ -0,0 +1,495 @@ +import type { ServerApi } from "./server" +import type { ServerProtocol } from "./server-protocol" +import type { OpencodeClient, Session } from "@opencode-ai/sdk/v2/client" +import type { + Project, + ProjectCurrent, + SessionApi, + SessionCommandInput, + SessionCommandOutput, + SessionCompactInput, + SessionCompactOutput, + SessionInfo, + SessionPromptInput, + SessionPromptOutput, + SessionShellInput, + SessionShellOutput, +} from "@opencode-ai/client/promise" + +type LegacyClient = OpencodeClient +type LegacyFor = (directory?: string) => LegacyClient +type CompatibleSessionApi = Omit< + SessionApi, + "prompt" | "command" | "shell" | "compact" | "rename" | "archive" | "remove" +> & { + prompt: (input: SessionPromptInput & LegacyPrompt) => Promise + command: (input: SessionCommandInput) => Promise + shell: (input: SessionShellInput & LegacyPrompt) => Promise + compact: (input: SessionCompactInput & { model?: LegacyPrompt["model"] }) => Promise + rename: (input: Parameters[0] & LegacyLocation) => ReturnType + archive: (input: Parameters[0] & LegacyLocation) => ReturnType + remove: (input: Parameters[0] & LegacyLocation) => ReturnType +} +export type CompatibleApi = Omit & { readonly session: CompatibleSessionApi } +type LegacyPrompt = { + agent?: string + model?: { providerID: string; modelID: string } + variant?: string +} +type LegacyLocation = { directory?: string } +type CompatibleInput = { + protocol: Promise + current: ServerApi + legacy: LegacyFor + directory?: string +} + +function mime(uri: string) { + const match = /^data:([^;,]+)/.exec(uri) + return match?.[1] ?? "application/octet-stream" +} + +function sessionInfo(session: Session): SessionInfo { + return { + id: session.id, + parentID: session.parentID, + projectID: session.projectID, + agent: session.agent, + model: session.model && { + id: session.model.id, + providerID: session.model.providerID, + variant: session.model.variant, + }, + cost: session.cost ?? 0, + tokens: session.tokens ?? { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: session.time, + title: session.title, + location: { directory: session.directory, workspaceID: session.workspaceID }, + subpath: session.path, + revert: session.revert && { + messageID: session.revert.messageID, + partID: session.revert.partID, + snapshot: session.revert.snapshot, + }, + } +} + +export function createCompatibleApi(input: CompatibleInput): CompatibleApi { + const v1 = createV1Api(input) + return lazyApi( + input.protocol.then((protocol) => (protocol === "v1" ? v1 : input.current)), + input.current, + ) +} + +function lazyApi(implementation: Promise, shape: T): T { + const cache = new Map() + return new Proxy(shape, { + get(target, property, receiver) { + const sample = Reflect.get(target, property, receiver) + if (typeof sample === "function") { + return (...args: unknown[]) => + implementation.then((value) => { + const method = Reflect.get(value, property) + if (typeof method !== "function") throw new Error(`API method unavailable: ${String(property)}`) + return Reflect.apply(method, value, args) + }) + } + if (sample === null || typeof sample !== "object") return sample + if (cache.has(property)) return cache.get(property) + const nested = lazyApi( + implementation.then((value) => { + const result = Reflect.get(value, property) + if (result === null || typeof result !== "object") { + throw new Error(`API namespace unavailable: ${String(property)}`) + } + return result + }), + sample, + ) + cache.set(property, nested) + return nested + }, + }) +} + +function createV1Api(input: CompatibleInput): CompatibleApi { + const directory = (location?: { directory?: string }) => location?.directory ?? input.directory + const legacy = (location?: { directory?: string }) => input.legacy(directory(location)) + const located = (data: T, value?: { directory?: string }) => ({ + location: { + directory: directory(value) ?? "", + project: { id: "", directory: directory(value) ?? "" }, + }, + data, + }) + + return { + ...input.current, + session: { + ...input.current.session, + async list( + value?: Parameters[0], + options?: Parameters[1], + ) { + if (!value?.directory && value?.search !== undefined) { + const result = await legacy().experimental.session.list( + { + roots: value.parentID === null ? true : undefined, + search: value.search, + limit: value.limit, + }, + options, + ) + return { data: (result.data ?? []).map(sessionInfo), cursor: {} } + } + const result = await legacy({ directory: value?.directory }).session.list({ + directory: value?.directory, + roots: value?.parentID === null ? true : undefined, + search: value?.search, + limit: value?.limit, + }) + return { data: (result.data ?? []).map(sessionInfo), cursor: {} } + }, + async create(value?: Parameters[0]) { + const result = await legacy(value?.location ?? undefined).session.create({ + directory: directory(value?.location ?? undefined), + }) + if (!result.data) throw new Error("Failed to create session") + return sessionInfo(result.data) + }, + async get(value: Parameters[0]) { + const result = await legacy().session.get(value) + if (!result.data) throw new Error(`Session not found: ${value.sessionID}`) + return sessionInfo(result.data) + }, + async active() { + const result = await legacy().session.status() + return Object.fromEntries( + Object.entries(result.data ?? {}).flatMap(([sessionID, status]) => + status.type === "idle" ? [] : [[sessionID, { type: "running" as const }]], + ), + ) + }, + async rename(value: Parameters[0] & LegacyLocation) { + await legacy(value).session.update({ sessionID: value.sessionID, title: value.title }) + }, + async archive(value: Parameters[0] & LegacyLocation) { + await legacy(value).session.update({ sessionID: value.sessionID, time: { archived: Date.now() } }) + }, + async remove(value: Parameters[0] & LegacyLocation) { + await legacy(value).session.delete(value) + }, + async fork(value: Parameters[0]) { + const result = await legacy().session.fork(value) + if (!result.data) throw new Error("Failed to fork session") + return sessionInfo(result.data) + }, + async interrupt(value: Parameters[0]) { + await legacy().session.abort(value) + }, + async prompt(value: SessionPromptInput & LegacyPrompt) { + await legacy().session.promptAsync({ + sessionID: value.sessionID, + messageID: value.id ?? undefined, + agent: value.agent, + model: value.model, + variant: value.variant, + parts: [ + { type: "text", text: value.text }, + ...(value.files ?? []).map((file) => ({ + type: "file" as const, + mime: mime(file.uri), + url: file.uri, + filename: file.name, + })), + ...(value.agents ?? []).map((agent) => ({ + type: "agent" as const, + name: agent.name, + source: agent.mention + ? { value: agent.mention.text, start: agent.mention.start, end: agent.mention.end } + : undefined, + })), + ], + }) + return { + admittedSeq: 0, + id: value.id ?? "", + sessionID: value.sessionID, + timeCreated: Date.now(), + type: "user", + data: { text: value.text }, + delivery: value.delivery ?? "steer", + } + }, + async command(value: SessionCommandInput) { + await legacy().session.command({ + sessionID: value.sessionID, + messageID: value.id ?? undefined, + command: value.command, + arguments: value.arguments ?? "", + agent: value.agent ?? undefined, + model: value.model ? `${value.model.providerID}/${value.model.id}` : undefined, + variant: value.model?.variant, + parts: value.files?.map((file) => ({ + type: "file" as const, + mime: mime(file.uri), + url: file.uri, + filename: file.name, + })), + }) + return { + admittedSeq: 0, + id: value.id ?? "", + sessionID: value.sessionID, + timeCreated: Date.now(), + type: "user", + data: { text: `/${value.command} ${value.arguments ?? ""}`.trim() }, + delivery: value.delivery ?? "steer", + } + }, + async shell(value: SessionShellInput & LegacyPrompt) { + await legacy().session.shell({ + sessionID: value.sessionID, + command: value.command, + agent: value.agent, + model: value.model, + }) + }, + compact: async (value: SessionCompactInput & { model?: LegacyPrompt["model"] }) => { + if (!value.model) throw new Error("A model is required to compact a V1 session") + await legacy().session.summarize({ + sessionID: value.sessionID, + providerID: value.model.providerID, + modelID: value.model.modelID, + }) + return { + admittedSeq: 0, + id: value.id ?? "", + sessionID: value.sessionID, + timeCreated: Date.now(), + type: "compaction", + } + }, + revert: { + stage: async (value: Parameters[0]) => { + await legacy().session.revert(value) + return { messageID: value.messageID } + }, + clear: async (value: Parameters[0]) => { + await legacy().session.unrevert(value) + }, + commit: input.current.session.revert.commit, + }, + }, + project: { + ...input.current.project, + async list() { + return ((await legacy().project.list()).data ?? []) as Project[] + }, + async current(value?: Parameters[0]) { + const result = await legacy(value?.location).project.current() + if (!result.data) throw new Error("Project not found") + return { id: result.data.id, directory: result.data.worktree } satisfies ProjectCurrent + }, + async update(value: Parameters[0]) { + const project = (await legacy().project.list()).data?.find((item) => item.id === value.projectID) + const result = await legacy({ directory: project?.worktree }).project.update({ + ...value, + directory: project?.worktree, + }) + if (!result.data) throw new Error(`Project not found: ${value.projectID}`) + return result.data as Project + }, + async directories(value: Parameters[0]) { + const result = await legacy(value.location).worktree.list() + return (result.data ?? []).map((item) => ({ directory: item })) + }, + }, + path: { + ...input.current.path, + async get(value?: Parameters[0]) { + const result = await legacy(value?.location).path.get() + if (!result.data) throw new Error("Path unavailable") + return result.data + }, + }, + vcs: { + ...input.current.vcs, + async get(value?: Parameters[0]) { + const result = await legacy(value?.location).vcs.get() + return located({ branch: result.data?.branch, defaultBranch: undefined }, value?.location) + }, + async status(value?: Parameters[0]) { + const result = await legacy(value?.location).vcs.status() + return located(result.data ?? [], value?.location) + }, + async diff(value: Parameters[0]) { + const result = await legacy(value.location).vcs.diff({ + mode: value.mode === "working" ? "git" : value.mode, + context: value.context, + }) + return located( + (result.data ?? []).map((file) => ({ + file: file.file, + patch: file.patch ?? "", + additions: file.additions, + deletions: file.deletions, + status: file.status ?? "modified", + })), + value.location, + ) + }, + }, + file: { + ...input.current.file, + async list(value?: Parameters[0]) { + const result = await legacy(value?.location).file.list({ path: value?.path ?? "" }) + return located(result.data ?? [], value?.location) + }, + async find(value: Parameters[0]) { + const result = await legacy(value.location).find.files({ + query: value.query, + type: value.type, + limit: value.limit, + }) + return located( + (result.data ?? []).map((path) => ({ path, type: value.type ?? "file" })), + value.location, + ) + }, + }, + integration: { + ...input.current.integration, + async get(value: Parameters[0]) { + const methods = ((await legacy(value.location).provider.auth()).data?.[value.integrationID] ?? []).map( + (method, index) => + method.type === "api" + ? { type: "key" as const, label: method.label } + : { type: "oauth" as const, id: String(index), label: method.label, prompts: method.prompts }, + ) + return located( + { + id: value.integrationID, + name: value.integrationID, + methods, + connections: [], + }, + value.location, + ) + }, + connect: { + ...input.current.integration.connect, + key: async (value: Parameters[0]) => { + await legacy(value.location).auth.set({ + providerID: value.integrationID, + auth: { type: "api", key: value.key }, + }) + }, + }, + oauth: { + ...input.current.integration.oauth, + connect: async (value: Parameters[0]) => { + const method = Number(value.methodID) + const result = await legacy(value.location).provider.oauth.authorize( + { providerID: value.integrationID, method, inputs: value.inputs }, + { throwOnError: true }, + ) + if (!result.data) throw new Error("Failed to start OAuth authorization") + return located( + { + attemptID: `${value.integrationID}:${method}`, + url: result.data.url, + instructions: result.data.instructions, + mode: result.data.method, + time: { created: Date.now(), expires: Date.now() + 10 * 60 * 1000 }, + }, + value.location, + ) + }, + complete: async (value: Parameters[0]) => { + const method = Number(value.attemptID.split(":").at(-1)) + await legacy(value.location).provider.oauth.callback( + { providerID: value.integrationID, method, code: value.code }, + { throwOnError: true }, + ) + }, + status: async (value: Parameters[0]) => { + const method = Number(value.attemptID.split(":").at(-1)) + await legacy(value.location).provider.oauth.callback( + { providerID: value.integrationID, method }, + { throwOnError: true }, + ) + return located( + { status: "complete" as const, time: { created: Date.now(), expires: Date.now() } }, + value.location, + ) + }, + }, + }, + pty: { + ...input.current.pty, + async shells(value?: Parameters[0]) { + return located((await legacy(value?.location).pty.shells()).data ?? [], value?.location) + }, + async list(value?: Parameters[0]) { + return located((await legacy(value?.location).pty.list()).data ?? [], value?.location) + }, + async create(value?: Parameters[0]) { + const result = await legacy(value?.location).pty.create({ + command: value?.command, + args: value?.args ? [...value.args] : undefined, + cwd: value?.cwd, + title: value?.title, + env: value?.env, + }) + if (!result.data) throw new Error("Failed to create terminal") + return located(result.data, value?.location) + }, + async get(value: Parameters[0]) { + const result = await legacy(value.location).pty.get({ ptyID: value.ptyID }) + if (!result.data) throw new Error(`Terminal not found: ${value.ptyID}`) + return located(result.data, value.location) + }, + async update(value: Parameters[0]) { + const result = await legacy(value.location).pty.update({ + ptyID: value.ptyID, + title: value.title, + size: value.size, + }) + if (!result.data) throw new Error(`Terminal not found: ${value.ptyID}`) + return located(result.data, value.location) + }, + async remove(value: Parameters[0]) { + await legacy(value.location).pty.remove({ ptyID: value.ptyID }) + }, + async connectToken(value: Parameters[0]) { + const result = await legacy(value.location).pty.connectToken({ ptyID: value.ptyID }) + if (!result.data) throw new Error(`Failed to connect terminal: ${value.ptyID}`) + return located(result.data, value.location) + }, + }, + permission: { + ...input.current.permission, + async reply(value: Parameters[0]) { + await legacy().permission.respond({ + sessionID: value.sessionID, + permissionID: value.requestID, + response: value.reply, + }) + }, + }, + question: { + ...input.current.question, + async reply(value: Parameters[0]) { + await legacy().question.reply({ + requestID: value.requestID, + answers: value.answers.map((answer) => [...answer]), + }) + }, + async reject(value: Parameters[0]) { + await legacy().question.reject({ requestID: value.requestID }) + }, + }, + } +} diff --git a/packages/app/src/utils/server-health.test.ts b/packages/app/src/utils/server-health.test.ts index b1c8f2c7e2e0..69a8c7b3be2b 100644 --- a/packages/app/src/utils/server-health.test.ts +++ b/packages/app/src/utils/server-health.test.ts @@ -14,15 +14,45 @@ function abortFromInput(input: RequestInfo | URL, init?: RequestInit) { describe("checkServerHealth", () => { test("returns healthy response with version", async () => { - const fetch = (async () => - new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), { + let request: URL | undefined + const fetch = (async (input: RequestInfo | URL) => { + request = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input) + return new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), { status: 200, headers: { "content-type": "application/json" }, - })) as unknown as typeof globalThis.fetch + }) + }) as unknown as typeof globalThis.fetch const result = await checkServerHealth(server, fetch) expect(result).toEqual({ healthy: true, version: "1.2.3" }) + expect(request?.pathname).toBe("/api/health") + }) + + test("falls back to the V1 health endpoint", async () => { + const paths: string[] = [] + const fetch = (async (input: RequestInfo | URL) => { + const url = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input) + paths.push(url.pathname) + if (url.pathname === "/api/health") return new Response(undefined, { status: 404 }) + return Response.json({ healthy: true, version: "1.18.4" }) + }) as unknown as typeof globalThis.fetch + + expect(await checkServerHealth(server, fetch)).toEqual({ healthy: true, version: "1.18.4" }) + expect(paths).toEqual(["/api/health", "/global/health"]) + }) + + test("falls back when the current health response is malformed", async () => { + const paths: string[] = [] + const fetch = (async (input: RequestInfo | URL) => { + const url = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input) + paths.push(url.pathname) + if (url.pathname === "/api/health") return Response.json({}) + return Response.json({ healthy: true, version: "1.18.4" }) + }) as unknown as typeof globalThis.fetch + + expect(await checkServerHealth(server, fetch)).toEqual({ healthy: true, version: "1.18.4" }) + expect(paths).toEqual(["/api/health", "/global/health"]) }) test("allows slow servers thirty seconds by default", async () => { @@ -142,7 +172,7 @@ describe("checkServerHealth", () => { retryDelayMs: 1, }) - expect(count).toBe(3) + expect(count).toBe(6) expect(result).toEqual({ healthy: false }) }) }) diff --git a/packages/app/src/utils/server-health.ts b/packages/app/src/utils/server-health.ts index 1b684d9af774..1d7d9e4b2ea6 100644 --- a/packages/app/src/utils/server-health.ts +++ b/packages/app/src/utils/server-health.ts @@ -1,6 +1,7 @@ import { usePlatform } from "@/context/platform" import { ServerConnection } from "@/context/server" -import { createSdkForServer } from "./server" +import { authTokenFromCredentials, createSdkForServer } from "./server" +import { ClientError, OpenCode } from "@opencode-ai/client" import { Accessor, createEffect, onCleanup } from "solid-js" import { createStore, reconcile } from "solid-js/store" @@ -61,6 +62,7 @@ function wait(ms: number, signal?: AbortSignal) { function retryable(error: unknown, signal?: AbortSignal) { if (signal?.aborted) return false + if (error instanceof ClientError) return error.reason === "Transport" if (!(error instanceof Error)) return false if (error.name === "AbortError" || error.name === "TimeoutError") return false if (error instanceof TypeError) return true @@ -82,15 +84,31 @@ export async function checkServerHealth( .then(() => attempt(count + 1)) .catch(() => ({ healthy: false })) } - const attempt = (count: number): Promise => - createSdkForServer({ - server, + const attempt = async (count: number): Promise => { + const current = await OpenCode.make({ + baseUrl: server.url, fetch, - signal, + headers: server.password + ? { + Authorization: `Basic ${authTokenFromCredentials({ username: server.username, password: server.password })}`, + } + : undefined, }) + .health.get({ signal }) + .then((x) => + typeof x.healthy === "boolean" + ? { data: { healthy: x.healthy, version: x.version } } + : { error: new Error("Invalid health response") }, + ) + .catch((error) => ({ error })) + if ("data" in current && current.data) return current.data + if (signal?.aborted) return { healthy: false } + + return createSdkForServer({ server, fetch, signal }) .global.health() .then((x) => (x.error ? next(count, x.error) : { healthy: x.data?.healthy === true, version: x.data?.version })) .catch((error) => next(count, error)) + } return attempt(0).finally(() => timeout?.clear?.()) } diff --git a/packages/app/src/utils/server-protocol.test.ts b/packages/app/src/utils/server-protocol.test.ts new file mode 100644 index 000000000000..2130a968c4bc --- /dev/null +++ b/packages/app/src/utils/server-protocol.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, test } from "bun:test" +import { detectServerProtocol } from "./server-protocol" + +const server = { url: "http://localhost:4096" } +const json = (value: unknown, status = 200) => + new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } }) +const mockFetch = (run: (input: string | URL | Request) => Promise) => + Object.assign(run, { preconnect: globalThis.fetch.preconnect }) + +describe("detectServerProtocol", () => { + test("prefers the legacy health endpoint when both API generations exist", async () => { + const fetcher = mockFetch((input) => { + const path = new URL(input instanceof Request ? input.url : input).pathname + if (path === "/global/health") return Promise.resolve(json({ healthy: true, version: "1.18.4" })) + return Promise.resolve(json({ healthy: true, version: "2.0.0", pid: 123 })) + }) + + expect(await detectServerProtocol(server, fetcher)).toBe("v1") + }) + + test("recognizes V2 health by its process identifier", async () => { + const fetcher = mockFetch((input) => { + const path = new URL(input instanceof Request ? input.url : input).pathname + if (path === "/global/health") return Promise.resolve(json({}, 404)) + return Promise.resolve(json({ healthy: true, version: "2.0.0", pid: 123 })) + }) + + expect(await detectServerProtocol(server, fetcher)).toBe("v2") + }) + + test("recognizes the transitional V1 API health response", async () => { + const fetcher = mockFetch((input) => { + const path = new URL(input instanceof Request ? input.url : input).pathname + if (path === "/global/health") return Promise.resolve(json({}, 404)) + return Promise.resolve(json({ healthy: true })) + }) + + expect(await detectServerProtocol(server, fetcher)).toBe("v1") + }) +}) diff --git a/packages/app/src/utils/server-protocol.ts b/packages/app/src/utils/server-protocol.ts new file mode 100644 index 000000000000..27b8dc208eac --- /dev/null +++ b/packages/app/src/utils/server-protocol.ts @@ -0,0 +1,35 @@ +import type { ServerConnection } from "@/context/server" +import { authTokenFromCredentials } from "./server" + +export type ServerProtocol = "v1" | "v2" + +function headers(server: ServerConnection.HttpBase) { + if (!server.password) return + return { + Authorization: `Basic ${authTokenFromCredentials({ username: server.username, password: server.password })}`, + } +} + +async function probe(server: ServerConnection.HttpBase, fetch: typeof globalThis.fetch, path: string) { + const response = await fetch(new URL(path, server.url), { + headers: headers(server), + signal: AbortSignal.timeout(5_000), + }) + if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) return + const value: unknown = await response.json() + if (!value || typeof value !== "object") return + return value +} + +export async function detectServerProtocol( + server: ServerConnection.HttpBase, + fetch: typeof globalThis.fetch, +): Promise { + const legacy = await probe(server, fetch, "/global/health").catch(() => undefined) + if (legacy && "healthy" in legacy && legacy.healthy === true) return "v1" + + const current = await probe(server, fetch, "/api/health").catch(() => undefined) + if (current && "pid" in current && typeof current.pid === "number") return "v2" + if (current && "healthy" in current && current.healthy === true) return "v1" + return "v2" +} diff --git a/packages/app/src/utils/server.ts b/packages/app/src/utils/server.ts index 603784e4d42f..1c8292ca9d95 100644 --- a/packages/app/src/utils/server.ts +++ b/packages/app/src/utils/server.ts @@ -1,4 +1,5 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" +import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise" import type { ServerConnection } from "@/context/server" import { decode64 } from "@/utils/base64" @@ -39,3 +40,23 @@ export function createSdkForServer({ baseUrl: server.url, }) } + +export function createApiForServer(input: { + server: ServerConnection.HttpBase + fetch?: typeof globalThis.fetch +}): OpenCodeClient { + return OpenCode.make({ + baseUrl: input.server.url, + fetch: input.fetch, + headers: input.server.password + ? { + Authorization: `Basic ${authTokenFromCredentials({ + username: input.server.username, + password: input.server.password, + })}`, + } + : undefined, + }) +} + +export type ServerApi = OpenCodeClient diff --git a/packages/app/vendor/opencode-ai-client-1.17.13.tgz b/packages/app/vendor/opencode-ai-client-1.17.13.tgz new file mode 100644 index 0000000000000000000000000000000000000000..5939f2cb39c27da205f1f37e9971009a669b83c2 GIT binary patch literal 75585 zcmV*8KykkxiwFSar(tRU1MIz9ciYI7DC}gdS+mCTJoAj^$;y!un$}%uIkrEw>`r@P zw;f4#X2z$N4S_`wZ4h7spk&6@|CsmrC-ZyeHSb^Tx&U`$oiU--X&R*L*8&;J)M8g$&@FhHGQ99^N% z4Lx-4SHHg3yZ)cQZ~gDPzs~{4CfU*(_wx_P?V|YM;^aW-{NLVE=KsO=!PY(J^8&`A zr8_?Vcek8D>JQN4?ZZFp;s0&zA9VH(xBhUrzx(aC_xBH-moL6MK0W#2#k=R7&u*H= z9n*AueEgRe$Nw4jPQE>Vmb|-qdw=g+=L}bR`5#4&a{cdc zduO-N|LRC){qGMu`@4VG-EH*0wNHHh!>nJF5;(d3hkxug`d=L>*Z)qRA3uBbyff&| zy$_6!gT1|C{cme`|6pr-``}<}59ogg`hTPU)sPzf?@lLO|C_%YD4YLS+$;0{aKF+2 z>Pd};cefLt|BGAwOXvUY9%e#i{og-m>_4@n|Kt3B(GZ1R)I|?m|DhN7C`|9)e-ryx zZi<{s6r$Ko{U~%Wnl4f5^rG0Ir4k1(7ycFMI$b|TUK)&@UK|aa*SNhC+}{699CwuV zqu5Vz3s=Z_-s|CF&c`3pba*`UKX&fl|3Cl7c^;K4<$tDbrgS+TXI7Og;#zYg#(=V z0K}Q!#ZZ!QpU?r}p`Z6N^x%)lDDJr)a{SN>MqQLR=nBPS2Xjr>b>pt{!}<9e=Q55) zLyDv`6ypLIMP3woF~X=me)FQ^K%*BUKj`B8!O#y-?8JCkP866DQv0Eck@(0-rGYTW z;!z0OddD9OqZpIQWiUPnBF_yR4AjrK7|<2EOX;96V$;Zrg0^$*r+r)}C;tQV%LubX zfX9HG6eTHc0)vP}1U_bC1-2p#8i6gnR-@c&^bGtfUeZZF07^7?LgNk)aM zcqr(I`Y1NGp#_0p}8E61Z8sV`9n5kgu;<{ZY z3h}Ik$=%PU6XWDg#tlw%9b$eCyA))M(j<*Xlw#1!!0&bgbPb&i16&hk%_GL(xHYU3 z_$cAjpu8?r`Gm#^pt0**j7cHF{tpQA!jzzx=Tkn8qLi@-MlTu0D8`6~ar7~!!^k;_ zaI08IDHDMLjPW(@I3Kw*V7W<#2U@A^oDpnN+GKcuAAaIo#L+dLfDbS?VIZ8u?O|kn zM}9oGh(<8q$7G5C(y+M16Xqhq(}_++ECk$PKf!RZ0Rlcp1E5KUSp!G;A!f$p=>GkW zA3tJ2$=dqj9A6|+FiO!IH|@6_&ROkD2)7-|SOQPRp zjdcd@C$#a!xo{KoHV%%QRv(MR2=L$i(FKT}hj=$NaD%ZI<@*$&ZnndK78r;L2Ld(}WFW)Q zMc^m>lPK)@m-$#+&%=8OQWFYzd4--IAo%c}_mYs-ej*>~mORPZ>12Q6A9QyIjdtk4 z{E%*cANqI)`x&F34V~d4v6k$1a37@T4Fk5rOVr&T7^^DviOW^$5y}bu0p9Q5Gz-au zN%H^bd;r5Do`skIbL?aC^Z&!W{YL)RlbZeColesH&o_Z{l>-x>|L<($9bmKnt0OgA zyt|#G`M;3WzkL3~d{Nf_ot?dA{jVcg^gmFn=hnv*`rqN!K|}vqQltOf=~SZs%|#AO zq5tjeZ8!Q~9jVd(?shWje{)^^%l$tO@DJ7gb9mUS|8=B?zx$nYl6&BFQR@0ZLcBa( zhx!U~?;nS=hx5CK_o)Hf0pqOmoV$iSX69XJ^gA~}&fC+MPShiAU1DV@18$AP>uLm! zL`fR^;pL;O_VEbU2~(f?1L4BIjQlwF5o3+DooieQ{HA*Ta#|N|;(M(}Qd8eSp4!$3j}J~p%&qaSGSr0)4&5ZVj^eJlLH<>4 z@aEBd{6A>&^$;3iUUuN>=qFwT&UwV6uo{h@u{yjhk<{ET3y4s4t@Unvp<1nEUuBAz37yK`cHMvIOVLsM(VH=CcOdnL1=^F;}p3`1Wvxm zDCUl$yfwyY;4mAaYw!+(^Bn`rl8@Q?0Z<(n;x<>^x z0AIs+6o$B6OaD1BRPy{*AkP_V4qRJ_S19oey&Qp`W+D^kBR?M_V?fS>2W%`Kb@a04 zFnUi})^QxW<3CblKglU}0xqZEY6^~!s0Z$u$_?;IokH_X=kXKgD)PJ5$&jScaFU5|hRfgDBw#RLft?Zud?s7z&~p}6 z2B5+4BJ4%d;NOP+FC&5RLhxIMKe`}vE{<7srk=;JH0lOE0 zX?5Z?WfIr9{www$>IppOKDH)~|8j7!yWQx2wWP-WbElKV{xerOP&)tB_%AznH+b0C zf9go)_%Hk0yW87`2aN;h+9zrL(^)_FFfe8Fe@l!1a<|p|LubRf202`J~bNN-A>Z{U&!iTVg4Uz@t^j#8~^WG z5;F^bacE$Zz)$(fUeczqOMY|*D4}=e{RAl`j1dNgEO-LHPgAdTvq@!1 z(f=7`=iA5R_TR&;{oQ8%*OD6j|4t{1{y$eaFopdW>jI7bUq>?Ae-FRi`oq@VPGkRF z`y|bOezkVaeN18hJ>1@I?7wv+oBem5E}#?Je|K<&M*piPHTvJ3PG$DrdB}k&?7s)w zP5h@?QlsJB?PRw9&i4Yy6!zbPz2^RBEvd2p-tA=E|8+iOv9agT$HeyEo$amd#{ajT z)ad_rIvMAGz7Lp-9GKkxyR*OB`2W_DEcV}>?Y)DYKQ#8=wNJ+RpUwLDSb`_E|L)-Z zLgW8iOS0&HJhIj;=No$k5 zm-Xb46kX!@k{Xh6d$i&AEf=y{Kgs1zXaEtpB#SDdmVJJuwMkwdRLgzu2S_dk0Wj23 zFTxaE(f}E9VL$(%)_H}J1cM=$AJ9y#tT$B3c|)yC)J1_(3bNF4Z-UXKuMBfY^0bmD zCShn)poUq#R>MvEN?Dwvm7;G&Dg|k#p)5R>8Eb_}u3GvpBb4Yw_=V-FrB6{0#qj>5 zTs-F4YI*kPqFkE2{HB%tA4B?iejE^4Ozu`T6nIMLUW&c-e{BM2#x4jMeUnBo(NX`E5PN$On z-(2Lt6#Kse@c(P}e~VAe{_k!lUndBDe;%}iN`y|T06Ub}vp6@@3P173b}^J#~_n-2Q9rFppyYo0#E-GleUaks1& z8rl(Wce=cI2lls{n;ixr-6QxtXM@yy+My99zj^!^p3-x>vzQtV(TWzOBLX5&6S+rW%ZHvJ3s*goJmJyNm?6x{Me3NiL&Kz{G~0S|+ViAtljLPK91(vmh7 z^*0^FD2!2WL;1v}_!nBHO}js0l&534$!px?tN<~SGf2J^Q)waqZbZhs?!#iY0VLQ| z=87N;o;=Q^L)WA6qLmk4@USJgqyl2BaW;GwL$W4@Yq~N`eokFqS zCJeMG^SamoUln8lt|HYUZFsT?DtX+zr^k+I0Hzv(aDVK4!`JlyUAo>F));LSl4h^- z^=lUYS1h2eLvqhd81Q~^FT4%9PChoRlL|xg8u`LLaSuJhwT3P060D;eSsZ?@ymZLy-Jp{^=QzC)!>Y*BjX@|N%$5oy z0I1Lk#_JZKTVg*S0$qP&m-^~R@|kbqSin4JD36`@czw!Mf}c7Aces)L{+<@w#B((4 zZfp=yCkQ=xI6CooFD1jKtilQ#N9%`v(kCmqI$SI~urv`y%!-;u!#;Hqtj2F__$e9? zSm2KimHz0npOTp0ga(@?juSM8$#~yoSn9_OB6G8O_F|P1IFU_ zxeL6QjX6g_r;G5ais&b$xrYy(KO;1RtpQ#`!mi(ic{>b73CY5W=qIqxUmz!mu(FD^ zp9drl&&D8t8ex<=7bGi2M86+*3?d>MO5*HMwY?>yP zx&aY1D2V_n6GJQE2vZW1qp;AH)jHa77_sY4J!_D-Obv>;F2 zQBfmH6t*@wMo+L}ge9Tr@ti(6E#S`vaZQD5NFt^)kXV%sUWh|R5XUP*H@00{pMQdYF^Iv?TY~;V<;yLaiexDu3p$Hf(m%=!`tuumNua zp@Th)$?WYN)!GAUvPaXZD9zsJNFWR62B>9tKO6+n^-HSnvz>=B&(wFNAd$B{ zpn(qlt1Xe4<#Y1LIFJ^trHHlLse{xCg@+PBy4n~ZUA^rp9;sOBy zK~BFteYw#BcdLig=7n44pkadNK(onmq|cfh15_3sImZ`xo}G~&9k|(nx1<|8WD#a# zE9M|N=8LF17R`IKnL^0J2IK-TX;i5lYJ zz=W;Xu(-41Cj_}cs&MJaca?@!2Qc&&Yb(K-J|<9EHi!v7#hbxlO6(EYk3T+kwzu-% zzyG}|phPH*0z=;J5>q^H8=98uh#RN8x)_r0 z@B@oJk}q1$=trKzOW>7A_zJ)Hm!Z_?kUX)bBHnBylBOj*xQCLI*eN(GlN_KGuPe+_ zp*%I#fg7S^OcT#E3^hh!G8%#|3YD}Y2yYa})ECyO+Vgg^qUu)W8$uHHxg*Vf^2^95 zdE0DSiZwc9dbG}7{OSDp>8sYJDxK+AH8xk9*fb-FLLOiVoD>31EpD`w8D{#m7-X^M zLQwzw;^oVh(DgUf!OVM8Evo9C_%~nF+Ba~zh&N++$H~qZKW-WqAe~EvEsWHv-?baB z=}M=qG!mOmtWC1UAA)ph$&}D^%K^e0B&^7`-n>3L7qo*+=llYvaa{3Kt>3UOp}X?* z0cg%G+z_|{v7O;VvW4VoF(gnNj_BrsK6&T&#v5Oi~9pLmfjl0FhB}MUNd5l%6wkuSoH_6b5=|DH}>!zIHIpM zf<7Zy5hyD8!={hnP?h;owArzLo9G|y%ra5)vx+N|O8md)atmk*|L@&{y~h8mmelxv z-|1A*|9d7mFuDIXygu0Yf7g*3|L?n<3jM$5bO*TX{_nxofp-6^dH-OI6A}?@bWcS~GdZa?QdBpc2f8?M#U-O;U zlSlLMOx8vw`zGUYN$53*`-V|F`t@fA^rd|65B^_h>?xC&C3y!2b141@T{NoBsy=e`l|W|6fmP^#41Z zD#U-CSq@CD|8E~2?lt;<9jVd(?{>1rf1TUvUqS!h(&E2vZ8zsXwWLD(Z$0OKCjXzU z-G=`4q(=X{)2WjEcSbodh5dK`pn3nbmelBfcRLl>e`|aH=Wy##)&CB5n)SbyRKfl$ z#y0EtG~^o@wb(L`)whZewzi$Z)btb+j1cLHjh%@&O_n1g_!hh zF+_S>3@P81BF49cc<}8k-g}!xbZ@h$?fEOV9kvV75J12?8(kz`>|dbQqjWfe020pY zAqpW91^F&ClOq%wP}}3Rrh0-Qe}7?W`j@=_*7=Y<_Aotzt&?T`rH@XN^@nIUpZGJ1Cd$pT_Op}Nf4!#XDEov@J`Vy}`` z^pHwKQc%#0&I zVF(A9my;v3$-HfRB_pY#&uj>>O$kr^?)}+#a1jL^^7BOss(Tdww4vf<9vn40SLw8< z#8|xJZ~TPDtWLp1D%5I0kkW_bQP>A~MM3VMWei1b=8g$DM5c3wU?ZoHPHmzlixDIM zrN-{Qv1315V9mOgrQ26-fK{pi>YlQ8)E+sfxb(3Wsk_c4N>9lbbC7(z3`DdV^m~qp z@{H!F#UbY21)1TvY^(LC!W_1|*`cMvyMBPx#1p(S`0gd5$0YD_NIrZB=t@V4H&)4C{j|T) z`hT&lT@otU*s2|rj9*zl3D(}x##Y;Tux;r@t%0?^!1d5^5Wv{}UkJMZ{QsNE2cA9m zp<}Lq=I!ko&`F;UGdx#fYCFWkHn&bDJUnHBRe~HL;L!8PC;cR2;J0$24E1@qL4ZX-UOx_gx8_2EqOXXC88aF)yH@&VevOgUT34N8;S zPI@&Osku?@%@sEYOr$N>w#X_wA1y0evIFEM=w$?9^KGgW57aKdbCC~~M*2`KD_HkA zayAS~Im_Dkl84j$)AMr@C>~n*D8zXG*mflK_{iDW+G;!M_8HI6y{+xGv$wVNQ`;dM z9TuenG8y zlL4UPM=Mi^FRiqQKh>&aN2V||`mo%5`GK~%Sbh*dfZ!-Jo{9;GnYe|(R>&;8a|5Fg z6FG27K0q>%ljT|ap-z)xH#Elz$LN9*CkKS)*eZCVIEh3w(F{}Zc}|8c zm<-!3Bl zL|+G@*=?{>XP_qet3!60a$VO;{VPq|x|&%GJ$wH0`T6tO5xX}}#FDqtClcX3 ztN_#}z$@bIA&jbEw>+RXBZSRzmNRD!@;2Bxx%N}9e@s`UWuuI3|HMfnE~eR1T8SjL zJP|0?U(w}#*|1KujG_r4XeF6WvIa@i10$5nvw)K3GHVVwM04UEGc%b$I< zT!3Vu16zSLa~|5pB4mUSd7NR@&9J-`&W!`YqAKLA25*CF{%{EwO**b=cdtczSMcY_ zr4asMD+upgCRs`Ht8hP@$ZNQ@FvgloPE&JDjAmGkGAO{aTx3I*VXF|MWHiV{E&aLZ z?en)8gYMO<{CV__@bd968`vV4ID?s$WWTNa8DC-!kXV%GR59^PVBje+?<{Igt>6^d zuYHpQchi?|mXKi;o%1U5fy2S_$S=sYP;?a*;ba_QDyF`-gw)|fyVa$byYA+OVVL1r z-d$X2Z^g{gM@uO|U|5q#5bNr>3c`Xs5#%kQ%FYH>oe=9WDZt-s`G^6dnzLPCe`|M9 z7A-7djkgVx6svtxBDV^HXGAg`_(L*E1F|7ivcI>@da-GK5Uf;~BarM;>Dx^Gv{Dn2 zeZoMRWzsa5hp_G-XQUsb3N(Z?106*OAF^;47NSC`N?1r!5|`k$kid{;UAqa|MsuUjo3}O6 zr>O2HG&cI6@Wjb>Z?M>u!B0@n9E7bb<<23;zvDTHsy?N*ejK>bwxSBRxD9q>M>cg&PPY3*-k1j^xlp zGk`NlR_@S5f6mS_lbnOk^$6UsF0nz?q^kfV8Z2@iTu^w@y~dMz5?WT7)N|7A zn6v*Htm*a}g^(7$syB-_PxUU|jOhhu6b) zIK8o|#Aj1MVDXtXgejh@u`pLi!=8UBsHE_7&e!|Os4~qxsM>ARY5(rDP7hv)Y3`N;gZ?X^$uH5d08=Y$Spf zPBRv9A7BAnBySYQbBHHf$%%;2hIP2Mdrlra(MQu--ljlmHHLpEjBO2lSA?O#J7cQ2 zWc6&59XO(YrK&t!ojTv!k4Pa#qrQ9xzcjolq^+Pen4E$Wg zT3j4uctkTf_cCGW=Di0le}%t*7FGRM3c%d2%`O1lqIm$t1K;@S+|sDL`4Ah$7Z20DLPk%W zTO$e?Q5i|HouvK*p6wj~0`1ZeWwPKuGlzwl+~Y!}NTJ(B*XlH}{EiQhmUWks6JZg- zpj}V0tk9nYahJ*QQNEwaa;%R|x>xy@34Y8iYBWDu<870lGse|l&WI@!1h-j66GkBe zuM~Qhalb3b=z0$kB@dyaj7D$pH-0jK|6JKiw2&_R(2b|sDA%)YVBAATQy;bQ^Wt3FCy!U( zHtI`XF7;=R%2f%u(9x9a+vcU%7xIhPyvIYq@~+WZ;TK(z-HeQ=WOM;f^ITXSHz#o|i~Mg$ql@*v5sieYwr?d5 z%GBYckdEo&HIq0UtPY<*swt#VYEoa<1B10$gXWFIB8!mx0u!`V{(@m5l!l+yw-K+U zcCYQvj|r#fP?Vpen+<&7l~5G_TC~+>RU-z0P%HAfO2^9*A!t#agL880EWKYn=nyt?d zq9Gl~gVgsvp|q7XqJNt2#M#YXWnSD)i@ptcD?2UA`;U$OH<_7vLNv}#|0~u0a+R(` zp&OuRBs?igf6wKhZ^^l&o&zD^kSUJd*G99;7;N!3b}HfeAsf&_bUe#kS_wkokq&34 z)0}3+LpqwN4dRyg`*m2q*FfdIb)j{O8n-Y!Qev4sA^89sKgcEZp5X*)+%^{CwvqAw zoSZhOk9_VR$9^xrcLt@=&-FwJSvZk?HU}12CWH(85ThQ7F=V&&(oxp^TJGlczy&AK zP}?Hcvfr1&$4_`y+QWlhz=k79;fi^%xKmx~%|1 zozgMD9RS`g0k0KZfWF(Z4CKAn_k%8N{3jUEYf`*}DFSi7I25HL{P>$Mc#t>mfAfVj zc=OZ83IjHv`V4m)Uh-~%6WnARdQQdmni?f9V=jJg;Tt>4qSA|lgxtO#-U z+VxW>YZ|t9PdhYD#D;*2o9r(Hr~kLu!K#Xv1?yIerUf&xA;vh=a&9&? zwj`YcJGM5mNIqkrG5YnFSQRV-rY%Y_f42>`6UL zCq)ugV>l)$AK}ioH{Wml^t6Mg#9(8ylSVJ2YZRZj7}QOIOx_PnlY32C`ZRRCPwpjx z=-MgRDh7UnvWKL*4?7=#2a?Xu$qapLZEYRw?K$x8;lV!ncWa0K-lBi+I$OK@2V2|Q z2M1ew&eqP({^8C&XKQXy7Nim0$i{dm7cmz2Q5az@u5T)bi}iX1?NCsh?B7*M|M0K> zc<62@BgmWzKw{|tL4-=059 z-d(-DzxSHI(3-_q9q-Gkj`{jVcoeI0L<9A*MY z-iGj0Wfc2=LtU~3!P_&zSI4@Qhutg9{f5~uAgA>qbuU|v&nzxkq4)H|PqYcG&(@>+ z%CKI9xSo$ULBu9h*T4+DT8DN*oIPtEnb4^|urulo^JA2LAC1D=sA=j}AKWV%f~M}l znYz^nhj-Au!1rq7GpRq*aLApDx^ZRd);hT7&;T)K)jT|+JgnmY0;`SBP?`qyshgIb%|m30)-mzPu5*NX#|FO3 z0tnX5dgh)DLrllDk4@>)NZ1R%+eNjlA##U?br!BZ)vlvxo$3So3v;8ddu+yjt%4W4 zv*$1};*V?^8FN$?g>Kd*9@K4b;b%$}&wsj|bb1@m;K{t)2M;Y&c7v5 z_>OAJR4e}KTtrcT+z|g2js_Pf#(yNl9TNXRd`kbAReFN6sBhW(PiTC^%KdbdH~Z-R zO{T(=4w)emuP{N{$DPvZee#p}22&TeD-_e$p=hHta|q9s&|%qHK1-t^_ngbxcRddc zQ+U6KL8sMz6?IW?ih5ZU=t=6DS+3wEAu<2TjeR#vpXRwr%iQDzmIwSj$$EJIxe~^X z;T@8bC?IW^ac|+UMQ$)8;a+$R$l@Vj#Jm`}7zManIoor+zI5Z6z5pN= zfpi`Fa4}cRzR|}2lKH-iWEO5bid4Oh;!g<{l<37XJ7vH31N6-A^(1iVs|6D0fd8kQ z<#xM1adMZ3j9CyFviTja7OsZ6jHb9G`+E>|{hp717^Vbai(@bKuky)2qqVoTYVf_Pb%R{mi}s6-H* zVa5YzU#py)1}A)+VinIc<{N3~kTYf=(EpPFCM%O{sydO6B){6DQ0$k1bKPkTAuy4RKUtxGZ*?iVYeoSZNbn>xp!kY>(O%p3#I@`crw5-#DN*hrZ|XBjHA2%xe%Dbqp>&47Mk zX$G_`lO92)i?7Z^n45Mrj$!1C9HNin{8&=#QJ#PPhwok}iQhH=&g zGwuD>`2u%KwSG?Ap?l#6e(Iy#aFyb6&RO`~PNDGhF>_qT z#409Py5O6>qv`qyxQ>FDk%-Su6jqj4FLEsWwU2V!i(u4dygbD782v&fQ;7eixx{Cr zU^UMgXAaD-NZ1c6O>(t|#NDTvfqQ|JWhh&}if5M4KUO4^zFA)wP_ZAot}Hb6JiIbD z1`jah9F&xe?}R?^0qDWHH<E*m94Zw$@!T9G6Kd#^Jd! zn*KJ~m{$m#Y7>?*m}-Ds5>T6I+)QJe>gf$BhqEP5*og?t!anH-2w8Pz0VtX$wyMvV zqNvy9Dr7U!t+T<;!Q9X@Ua-tXB4IJW3;ZLZc<_oD zy(T@TfVwm9#Y7*bGHIH7J{iYCgA!(=QMP=LW!RyancmR)^Uo=YLpR{o91F;Nr@~5S z2OxpEe5QfQ-<5~Ly}@)0=8j>?hR)`UWcCdK>yfdxg4%_Khsg zw&H)|rJ8S9%p_l-yAM52Csz5aPa2=oKqqoYMZ9&gAnLl|5lqsBAQ;RrR#}<^Zqiq+ zGjK5BhgZ=jq<+ccgUX_iSPTpC0B%n&(WUE+HRmT~NW}X|$(zRg>FE^yPdTR?bEleW zesK`a@679?fkkeajKAFC2HJ*vf=DUY(h9Iypqlq=o`%AVDYO2GH<)+SaVXcDO+zqt zLnle^sUTgm5T{mMls5F5*nnT6@X~@CTRC}r*7lDtNJPq_ro|$YKy5ff$qtI?aE-kh z3r&vg6vCgD9qOFWVBfE?X4AvWZ5)rO%;dwNhUxF9wY54IutR(-IMVz|tv)=d#B4M= ze)GcE56_qVxbp-UHnt+oK-(%#k<}D z;v29u$-%c=Ta`GYixe+X^T%&O8*XLF(qL#m)>qbW+0T;n1!s6qD=%eGTK!|AvIxXKS$Z45a*y*V4s&V@#84Ulohw)KJ&nqd=D=3=d)+YK*~ycrfj04H#R zw?f~Iu>y`_ZC%T{o4yHy8yPvpuVBm~oI;PWOUcK!4DPj6OU`;$y}m4-R+IHCZ}H|S zdMdJF0jlMNH#eOM>+WR0Dz>var&_5$O^U!$7%yP$!yC=v12Qs(v?O$gNk2-fNkSOe z_kQRn{RIuJM;d2i<6dx5)5YT@^EgTryr;xVLh^QqQa32my7W6nkWBZC^3nS0T_Z9g z^}Z1+Z}7T;WVm-EFs^}L7)};I6V|fAEOsjj?91mr46@y4W9YkyVi*DYK&e9jxo0x} zkr9Q3jZC+xP}`3rP7KQUcm*EOB8z44(0~e?l@ttXe2I+Qac@6F+o;5ivx_Lm&TVmhmq2nXJ>)qC{u`i zg{JUT+5~C{;9+du?I;%GV)L+dn&$1n`kJ*io#ixdj0Ty)%@vq(Mf3f3bvqch+~-wh zHQn3n@|oklbezv7f!_^`7ZAaw!kwL%JA>+`Yp?;(=_H8<(~?J+hV6OuF9sI2c4%Q4 zlI@3|6oV>TZ$h#q$v9I5{bH0DEYO@O=F6{hoT9-3Rg@usz6J} z=f3v|rR*@BcyZ>uMn1`{vV;5&erUg{&Yjw~?Sg-8yxObXy_kb<`ze2+8t!=q=EEmA z@!+RVI*Zs3R{A8C+fzHAWS#%XPt9G!xiwdohf(6|C#n~*8)|1IGmJ?HTn7IY9q1}G z-+762u45Tl&m9Hncj`C2Epl=uOKF|kYPu5-FT_c(^&?FeOf~lfFA979rJM&b0@4_1 zKGJ2#mG2r5$MaA*$d6Y%TU=t`tSAIl}=JF4^o(tkg!no|{P8M3-0n zNSO6e9wW1GswbbBx}UjO6Rz=w28LnlIg1G|IK)u@_9HFA2b-SM#y_bW`}AJylI}eU z#ONngPP-u&k?p>n@Wfy_Fo!W5sJywuwM_rF}x*C{+G<3ml*ZxI|`vj>MM}w*&^#z=b zKtQdiem*AWG7ue(r$De(jTKC?q1-O0&$%1;4aBq zdh4Thp`4GP!!Dkz&U%SeBe|>wes+M=ijla{62@YsPI*MY#n40y=(i#>Lic~8KF5D$wkCSig=%D2Bq7QLU=di+aToAGv5qRD*!rW}%0+Q4~CY zQ*Y`6!!BAq=#g6w^n{lXsGIv_&C#d?o6gfPa+iN>sO z zDA;QjfWs<=T)5;%A9+0KLs+1?#C$L7x8u={r+{6*jtB`|5rYuiF{FDy(mp$$g=^R? z8#1!%p!F_!G4FOBiYR>?`nX%4*2&h}%%E0TLh~9nqx6Pm&6`o$={(4GLhcUNJ$&eM9-}Azbu}yQ#H(%|6DA!$Cm|E+ znjq=8BL$F|MS4Nv@n*^#LolHAUQ_}|~Gy+Udls>-p-@F^wTmzUDy?r4tt`azRj^B8~g(c)Zg& z&4Ede17CE7zy&qkhv;T+NiXD}^L*sLA(3^DhY)PECg{?U-=P70J1*&W3J|Mo`!IA& zL8F(9>Y^UN$CWgCw`dw7X3>%|ZV3E@ie8=(qHF1Q(syGd@M0({`I48}^t=RnLzMi&G2AP+qO z@x~rKF;$+HwQ|pSg!?$_N7pqnP6~ZuN=Dby%nHNvDs)7aIBr+;SzB_d*NfPFG0(%r z$=}y*oT~vskX+LDDCXm`#XmJ7m3#M-*c~+@Hk%}*<3&nUxJTy(!L1xN=RMD`|IN;A zCWZ6(%Jl;{H@ZD|AGmxBiv#?fm(b8+^-}lBOFA^%*$lKb#!EAtNoFtg)oL*vc;vDa z?so=@sp&m9BZT}#xmy5|2f122oWe^(NqsRdhFPy2H~F+^ogGr%qviU*LBx|n+OL* z&j(DzU&NLc1T*pV>iNJyD?#{)a=BtCImbw$EF&PBIl)4#)mp*4+@QXSQIQ;;uIe^0%YG$ngxpz@a zSOc0C3-xFs{3C_aK5N!!XaRvWnNwD2l=>OtCwBbIUC|qiHU((SFB*^ez?*qg7x#5>zEpl#Gq7 ziO1;khwI}kF@GfdV!2`b6?0mNdVxukqw)*2s}o><#>lrav&pv@-g!j-6fa&Y7nhvc z=$>=K8m9XvMwC;8S2<1(=g?zdj#^ z`TH2;J9}@MF|_PbFRMp&0~=W*lHt|r>ESio z2UlScUwnh3p<(GXe5#8>w@Sq;He-3&R*WPeC&W7Cgj6rGCel!hR?%b1Q@W^QNa|vN zX-5%Jukn>wMuXk>^p#kP*Rnp4cQ!<#*3&Utl1KWFKCJEJN%FO4Uf2$~#+L}gn(E(s zN4bk)^+QDM3(YtBzXOD{cyK=8t-OyvAct!|h=`)$xXm=f>DPvfo3N}-t(u2Zi0WId zqTv1{120H8v8l^dG?ykIc+6}0a6gSh1J3h((Vbx|hv`>*?nX0aX}jn&SZK~34GGc@ z(-m#ZQ5c`i#Ns#$Xs?+yw_`5f*OOG(<;X!9<3#${v^^k~%R1~Cc2O}P>`d?EW(KJ% zTFP>u4mV484+HRIoajR?cF(MG#t2oh8ggD?#CgiGHx}VI`A8OJV#O@XN{NE z@7=BvMr{64T>8SS8q1va9fzc-X%maznlI=#Mds!*rk{0;oX?OeX2^U6<9mQA3^-9; z8E6DQE-^BMb0_+IoBK>qbLNv%OXyDJ&=3hy<3K^32vW&KG&`Dm!w>P&m>nOt;$P2s z3s#*LUzq>}u?YqFp$4ao?FWZ&Bgk(z`IXut>zN}b3rcAg|FCd_VSg+P2ww4_p~)pd(EfQ9Tbd;K^kf#*OQxr&Ti%NC z^~>*s&eW-lC*F^lbVj~D&VT-4ad~_U6FJ>qx6vO{6k@n>as>(i+O7=0j0>xh=nHj5 zHfb)60|3!&i#zjXdub#$^lKjH3HH|SJwiw8Clise}8ZT<*G%{8{;~ zg~tf2BLXfg6l`M;TVGhQ^P3MeOA+@NcC&7xj?@OB8-+pbe&)EHu3bMWUm!ph z!rzeD<#hu-Z${`3C@vtEXHxgc#tFW0(eduVZq&CNdMwWT=HX;xd(jYTa8x~8a! zDE8tDM3jF?I*WPY`f!3iARX#r&SnNzxch$#VkXp9iji}K6Lj`XbL;%g<#o25D-}#yR&xe)m#v>LrzqGQRVn)lp6W!q3`hmi@GC z8_gm-hw&_L6ZB(cM zC#|Kf7|)kcUnILxI#x~L`lr*F?v}q|M?@=^HC1TEr50nfHG2tFVTvzhnSy!g%&g_8 zicEj;zx(1Jt*idL%v}XKUX;7pe;cYbmC``tk6KDiYkKS}I+WhKp-)_TtWfx3Qh!h{ zVE?P6Pd0~x+z1gI7@+m=6?sy14kuF}l3Wa$vWx8MA0I9)#c)W|&{Qr(Sq|d#CTE(OHTf*+jfWKhB@C)I1j%^^c`AC97#UHk2r)p>wv`K z&|S^=rZp3EG5X?@3A0uUuU!`Zs^)-!i-5fv7^-|o4n3SCnlp5ne_|lVbGnc zh(Y&*Be1#Q<=pU0Z;e;kH?|FAQreDWGQ3qtlcDHYmi1-ksB%z0m-7JS9n34^M)H%`LveQ2q&b{Vz(k0HH~Nc>)^>|Uxj$xd1h_f1fJL+4_g-tXH#C_4sH zZgJ}DHP0|Xc};0~EW><>5^ivd#wzkt`gCpK9S0Kcx10G=c{lxIu><7BbAprfWW97C z!`pMxJ!$f;?2H3}#P!y8Wg3F!a5cInR2U08oBFG6V-k}PIdc!=U5~89UVG8wF z3ii_OV-jCgm(@0!0gYgEZYrwXW4)FuoIPi5C_QJs4|-EQvn=ngW*neSV6Nw~e5B4H z>ialHVa_v@?ezbw1){?;V)^|DV0woCY8sAufhnFfk0cU(ENmE3JXW*nk4_FX+*hn= zTGpim+UT?K$(aT)j3tmzOSnc;a}HQyzHNdq+NtsiGI5(TI%p~voj-fNY9L3MzMxW< zZKn^jkPa?U<8{%Dt&_#hwb+`3$PkX}K)06%@|3E|BQE9zK8m3>vWJusG(@3~=4@(R zR9BY1>9+_{{5!m2i7THWhHtF<`nj9;&_G`CJX$*TqpjYif{lOoTug&}of|b7X-dzj)h|5=}R%*q5_$V~}rX zkk-BcfY5tXivr!}ki zQgv#k9{Lq53$_zCc=)l|M`t9subxwNy)@-Gna0d83$?xPgGMcF zmAizIhsoU0_VRql3)-AiJGz&^__F0F8ve>W8k%PaN_Q72+-rcUG>I!;B1vhwp-s7Y zZn14?kpebhdxjud*4k+(6dM9PLSW(QFA6op9D-BGA2lxwahcxH5Vf}0xnM+x%_Crt zx+@Mn2L~|D1Z#c7wg7-lD*66ku-(tfp~|%-mv9rX2TrxRM$-67#m$ zB-U-B%1mIvlUgD1&npu%__*_Y$Eu%2ejgb(QS&YH#*ns{GqfJ`4;~kqRAah}cW+s@ z!j#MU%uB?|_znJPE=j}QR;t4U!4KUyywAuKk~5UU{l`?2PTb0|&raK4(_;8dr<%cm zJk*8oVZe&}2l6v!uGAbGk>Fu8Mr2Rpw_{4sc8;|Kb>SGyUHKYZ;?5+^)3o^5zTN}B zP=YDjX>4+tM#LVeKb5{5)3QEFe?x&IKjVD{e|>|=29t$U9Ybw6MCj+?A%uVnDKxEB z@{Q-Okp?Bb*P3nEHiEkN=7It32B(_W5VuhE*ZEydlGN_$ZXIURBdCrQxW-MiR@GD! zuAPbo>yn=t3S55d3K%gZF9#6kLyO#`1)kVBCD4~%g;e?ao0ISA+ipQzUq#AjP_`{n z{uyd>HXBSGHa@2K=5CWTg)Y6Vh(6!{d)E|lR1f^YJAvkoRA~+k_GCA8ZG$dSIY>yJ z!-|iTlh5VayQ^puTvxL-ERMX{M!r{pykkL9@AvJzK8Fbsr_}|s!)u_2usX!(7VCeX=@5aI}#r$+%OxXp-6P zo-e8&+8(H{s6vH_8570Cw66345;=B{KNja1Zm&r|$P!W=Qslv$JdV)7PO(JWSG*6) zBoY%C(a?H|qOv!<=yY){)S(9@?iqwbNq!bA+{Hq3M|}C>(stHtFMw5l9@+C`>YhFn zm!_a6d4wKIJcEj8{9Rkamk!stXSCPCXp)aIXptZRRg`G+QOQy6o6#M5VV)Nofej{` zfHkbM1fr7akuog?v8T7dNO_x;)7h`Y@y5&v7&I%!^884ZxEeFD&Hp<*`_9x6?Dg2L zpFDJ=7WOd|a?|vzY{^85q9*x*=G1pW;*unHp~hCbdOex=!8*byyl&{E_wxCh(c~81 zl*fo~r>%U14}~)4@o!^OkUimY|)wFl$!S~ z`-1I=XLyYLQI2lC^T!)D(m+R&4qVJu!BW7@5D-%cW6r-y)FII%i4g@U_f>&PWjd>K zoe!H0(h&m5-I4cht@?#nJ2a0IUP&3UUhy~~v&ii8dP6zZ+CDoIr+L_iB~dHaZ3dT;vp z!?aXS)UEvc08>IYVvn!Aq(OJEzYbl1hOlA_-2#+tgP2uv6b$iZ zZU4s;I{~(eJ81=8NdM}qzZvqfO;e(D{n)rg9ORLuAeIM6j-CBe>Njc5+ymAzwz5y%dC$V1)kO`oraaPtPzga_ zX^#>l2TD+SkWwJLJo4#IjsB)Pk&-UukG11H$0*v2R{0DnM{xs{zwu{uE zY(-OSt#=h=7@bwX%kT zO2{PGEtYwlN4|uJqn)8+WOqtMs)vG#-MBQyj8NAE*>tQs(#L!Zy_9=}IOa)xDIwCCQ4siZHZ= zzA_g@&^*=c8v>mlU^uz?EC}OO;Hh^}fs3;$5bg-g(mj&K?2>~5$zau3dgzm$av1!} z)NA{i%31CI?x-eH`$m*xbe-Z%?pL&>yZ}Wci;@B|s>KKG{uRzjkcEoc2S;#@zdMq{ zLJXPHeGWs%HIx8VfN6b~nl`EVNIJv2)9&QcDNP6`h7e1BTq%0{0i1X0fK~O8GNLSD zp$*?~-AnUw2o?FIq|5!!VC4d=VFIotg5R^%?d%wf5!U`o$`Ms<(W!*-ctxLl`N@!H z#M`*WZW4Uyu(`!^@jvfR0S|jGpn}po%!cA0G&%t9opHWq-+9@$d*3AmL4xPUxyv5_ zOprn_++l&!&hyPokd>AnRBgQ2yDYvb8R0t)bPniK*!u-X7b_mpZ-8DC)3P~av`2ON zZODlV{(N5U#!VGnOyfEh_f_Tybn_)F=cQch!lCqu5%9adD zysRmt3)-vY21L?$ai z??W{#p)w@uwO(;OtAx`xShZl2z&1ykg%h}D;&h05<|b2UyD%-1b`eDgQ9VR5I7)@y zRU`bgxSOTG;s#|g^*sby%qk$p@|R#?zP1gD4;js)vs}BEvq;AVhj_W zq9@9UJjCy3z!v$5N=znF)tX%2YgU92!tjSYMp?9+Grm-H8hfpgqz5_%JmP)GU$S9H z=Tj#qPjYY$3x3IH~?_JQaI)+gwsd=G_$du9-9J{ZZVb&1If2BIs&nC-7RCYu%c zT7@)|zhb7TH#rO1)Zd>I1C~?5S}75t>Dyfbb|vdn3|h=fswNc*k~mc%kjQ`5$gwmr zeXUG4Dkj-@RktKl+*T&p@#1LUMp*A{RQ}=Fsbtz|44byBaTsg7RA<;!GHsl=y(@tK zHU@CvVQ%x#a+8<}JgXjz4q)fXQ|ICo@QghONNeR#>y{(@g-v4u@D0WpcgL)R8 z;{Sc86jVqPUB$_I3t3ym!?9^Hr$j1vyiz_J%}|}+k`euGLCs$8M!#Yv>yT-@{Y^e3 zI4T6ilfEf@3TIwYf4vu8@Be^%_rJ9X%4WLk{M6Hv#;t{#*k$9$MlJ<4ZnYP)@wxSk z!b(`iDsOe&F!oW9xb0IgQ7>=mnB|Whpp9+zf zv=JG}CagFqWpoMQnv)G!4+J$;r{8DMtm4t7O6KY}im>}-Zn^BCVhScTW+M$<|4cPN z6MWS9WzY~C<1KLM&Ld28pz_qAPaMaPp>ngj3rdX2MIRQ4Q3Jjdg^Xk>Y;1qWBYkjt z*=P1s6r@PdL(!W{eJA=KY=!baG)27(+Qj3cho4>p9-4#~?WRKX7f}V*`=+B|FlC`A znsk^=n)h+4-QLM%;e8b#O~7D5^bNwB^xgqRQug$isarWoEtXEeBqxM`Piye;{jn5m z>BGcHLl47R_u>;K8BqDuBuf_^e9k4}Nl0^XiB7jUm2p-O;+yT4Ms@2?K#8lKj*rAZ zK+CpTW0O-~Ghw#u_MbQ|Ry3+x-;Tg^+bu+XUfV?@ezB?Smia=`^t@T{!zVPc;L<N1oHa2I+~6!-{5~XG9bgQ9tF@h8Dhdn!YrZi ze{}o{6c9Z*yNes(WMkC;JgK3B=MJ23@m#G}xl+qomai#TKOK|5j)V7k3|Eot*YuqM z7C9mYkPr4}%r~XiaVRagt>+OFjj^}j8t?&igc;L7_hyj?u}E}g&1tr!)r)3s$@eP7 zL33uN^&iEbWmYa&`-IgryI>%RmbOWlIDC{+w+g4cR_p4iKT+3@Nbemu_sST?h9x>X zaK*RFGE1OypjhPvwQR{A>^X$p8RCY8^htYqMPQd`c+G=kz%M>InKj6=chkcS)}jaITE)HOA? zKX5lcl~5JKsi52X8TeJNGeZy5QV)tv1$Z*|^z|YU?FE|DG^i*kWC~C_^TVvuSa6rM zG?u9k&;_3=xSbvGir08(NB&u)PEA{9276p?inwY~=qrGCQpu~S{?Bz2(ft;C;B zxaja+@>M9$1c2)$eW2Z(JM!I~i44fR*#)gH);b|EvxKs&QM^+QT zM|3;<)>T6Uj)}N4J>M|80)35R+`J)G^5K>cAZxiB0?pPu{OKML_j)H};S8xY4DP`w zNl~5UK369#d-A(Oz<$p&m)D;oJDJ%~IAlFc;X>^MyKciN2%MGK_LiAW3%rbku z2Ca>M3}MC8g7u*JfsN){zjA14Z#7tLImJlwoDo-S5VT`)lUZ0J%|bc{c$$Stad0!C zFZS>5*r3A4MA$^@>JxgvkCT^e|ve^jPld#7{$) zX&4us-iy?P*}4pvl~W`2ABO99SY1f&b>Ym; zpK9*w@)t$=?ImeHz5@ud3iLz)64#TQQK0u{w(w=Cpu6eUz5Qu_f-Z0A;6TqK4p?_) zLs8WODw$tat*`N3EcD&8$liO>^Jae<nw1n+scd30+B6SawAlK~6OAOF>NNog_q# zxL+BFV$uAC2&sLC#!W#gbDpgTqDYAj6_bVg6y#YD`mmF5z#Nv}4T~hWe$k^YW0#9G z=SR{pj|<&9+Vnq;hU<`lL(t|U`WZmVBjxe$p>qGQwaN0T(Cu%5*LM+*2_(zl02a&SO#7-Ffc(agFA$0KPHg{ zchxk>w}3*$R=7Vh0(4zr4EzbIuz#W4?qRgLJ@!F_sUIU^=D|V zT6C_)ktNRp&1zBrMT#~?EVjPpI}Gg2bWb>Sx^4%rB_-wtav5iNTTGdDi~5#I-Y&C@ zXvJK=1Mhw8{7;|InAm^rlo-t&<6{WZb|gF2Eydp^rz3xjVwx1BY9gB~Xdq&W`__ex zDC-K{K7|F0D9u+rVpiMh(OEvaWIs05VR2*4|Etl*p@ooZ$emOe_}FK3@*Z_a@Ui$H z$BpO!M{TYTWm!hHLW^M?irs!hgNR`*GB{>6JpPM2e{;`u>tuvr!BOyEO}t4{D2p!| z+W$<%V&VJVQUT2oe--t`uHPa8|0>Sb-Q(6feM5Nh$bR_`!q2(?YWkTpVWoZ1ihLHL z=sWC$o|+wU&B4!6p_l&x=CMVpWLQK|xII_o&{;$iLbF%Gv+)YW2WQTQW?~eI_QL$}fKzh$g8QY`A6J1S5&cGmkYB{e?52R{0KL?Y;!+4$x59A`DhZ zxASA!2;_qYK8$ipFyg@@&L6hpiF}g>F_P<=|%Try3j16t+- z0?CXCTnGsGfm*pRR;9l#?y%8Zj4k^9(K3?4b3*(4x9`7XXib+f?!sIx6ZuyYBJL$fQ4 zGfep`gbaTpqV|_HQ9SA3Ds^IvhI(v~R#*Z`gs45VN*XoT+yVh-6D^zoXviNb>&Dk9 z7(xP_6)fjvH7s&CDuBRnxJr~+*qZ7ZvEx7$!C^pzXnk1IWjOhf6A?qtIL4*+fnNAC z4SsXCOi50uJA#$Q(BhB6&2C|G5W1B6O(F&iCIq-vtP<0Xbi(O_Z<=ctB6!K-Y&gwq zUj9ATET>S5sAcFv=;S%SY2PNvz6+bOE;*IfH3m+e#YtC(b4k!LD}!n|@f{a(hBxiz z6-Ii86Tb|%_u@HLNgvHA*as+NF+ z5`;6tTFq;7^?cMy^N5fwB;+c}a|@c(KlmLMd}a(wlhZfx`DaYwbep?f*A&^UzUf~9 z&?4Q_xH#*q#u!d|#5pVIrT_!3LWAhKvtz^`BWoNIbGshte~msTL14YF!_G-)i(GF6 zjRsPSHUgu3kPiI07Vqa;BsCE~0_Z(pWEnD6%W$1#gJtL2L#38Yf+iyDVPdSx4j*x~ zW!a|k!%aXy+B=@$MkQFV+YIIm9xJMehp%8jyDDY1AYu8c)Ao4Y5253t2k9Ec0`N6L{t z3bL8vHMZyKmm>4wbQx-4!fhqaQqGVq?u-g3lCtQqrhv-AL`T#}p;bz-3 z^P$-Qj~ay@?aotGcZ9+WTTxX&uUuf-2>|}KZ01|xZC$6OklxJ87aj_DD4+vxyZe@O zn?O!z*X?$x(c7t3fmc*NDDzS9NA8}q$kkzQ;T*_AUY;eUXn2Ak*FD(cML*wM)~rQD zvIRp_2%kdQ`4=cuoWp8Fv~&K$T=g55CZq<-oh#`Vt$W+8-zM*pZ7}4IRHM9bG;Yr*c?CzS$jSU$hzu6j zAvx5G$NcsW7r!J+DlI*X`b4YvX|%U*sgfI8Y5d3Y*|fOj*B%kZP}07LWltQHtCl_9 z614wojj?RczY!)Dl0+1uv9uAdQo!VJ+p&z+y^)QO~!YdZkb{*3QBz4<~M5uw91nHn6t_X_%7D!&oY2VbhuX2hrV zOBl}Rrr3|~;+843`PZtXPY{)B!H{XH9Bjk|lU(8`dk2aTBU5U<_~Wg(Bvm~4GSBz1 zK60Bi>-7YgO<+r;8U0lAUFai!7uhaGw#9vam)%G^)7!^?GY&O$W0QS`7FX0Oqs+sQ zz}0Y4nt>x z!%vT{q;uromd`ibgy^&Y)|`OCFMHPp`7I2q&^*HHMAPXtsFHF+4^sexm#QcrSabDW zRlPPm^Hrd<6nen;u-Y4=+YPvR_A{M;GQ>YP!M^GPIM z0lGc8J!ih^N&T~<^T9Q{;#)T#0R5D)B8~mC4+PN%J3J;{b0CGcx zO?}jovAzMbWf#_f6iHx7=Rhc}x-a%O;J#8JaOaXG;?u9=zx0f2JF9oUtd;NZ3(`}W zWN$e8@yn~~`&9%qfKb-)c4!;u9fOs8nsRDJbf`SIi(DT!o%GC}1FTTq~3JchG;GO~Vx0im*mtW~Z@Ds57+bjMF_?g`;3atJ0J&7&ZlnWqp z^%ur6kap^;w|8e1XeB}Z?zho`Z$1sNOsCQWoFN7MZ&a{C5a)4gkke@*NQchqk*=x||7jU@0fmi(9%z}lKJz--SC$d&P zODE8l`(vA~7mV)cMsOrO;I6D_z)p8phN0s@C|@|IKPqd^-B%DN^%uC_#NFLo9X#=- za_X}Wx?iHxbj|1DdD2`4Z`VL)I=>B8Eb?L3f659=v)ZPso!v23WrIDrR;lcT*52Z( z|LyfjvLW!-G=-No%}?p+=nW!~TY0ALm)pJBqY|;R`SyXSZgAzJprD)ITHg&yWqZ@F za>8Y0@*pj^Yv+$Ujl-AFrd)jUIH$`Gt4Cm=f2_>_85qvF2>#@ydeY=uYha#xz}k zT^A@>lW)IWmSq8WQ=6>$-P4;1)UTfixD!ih($&vadgpV*MSn5b+mnLHs8=pWk(uq0LJMBfN=K zJ_s>I(qUa_BsCmx3Jw_<)Ho-yKl}wT9-kgN_Gg^)*E=mbGNYg$4510lI6Zgk@b`N( z@6~H_RJ_^pCTOAMvC(MbouoK1HA~!$u&SyzV`W7@)%?hww|>_sVRK}Y*YcGmyKyru zrR9ZNT+LTEyG(#!u4S~1*L!2yG<#|zgnZXRdE&BxJk@R!>9*Md4r!}ZklSLzNV8#> zKHP#HURND_ZB+?!wXR6o*77s5f)>KYasw2zx%xKTmL=$SBR)v}XigACwKMOHD{j7f$BUte5Bma!HuJ0=(##F%OF;v34<`!|ANvGg> zXgxIFcYo#K^Bv8iPqEqrOt?B^W1_Ng)xZDe6>fW$bI4wa*3~;0^UlBd%V&l?O*a4c z)%~MzBx`Cs?U8jI)rMj_n9>mIIz#|=Yn7cL#i{nj&G=SlT{ikV;te_)mm2#>l5@3n z(NS!eF_O+~W6OC?k(EnW4QdJi1S0$V{Z->Nx+x7HE$aOmj=ub!T6C}NX~uZxKH`3G zsh1Dqp%J@A-o0af`JC=dI;4*VNnH1qU`wb0zRIYv)g)UiVwdPk44XCUO&`f;5d-%( zOT<6R7H9C5PLHb|W68`=m>M@Le{9)FJRFv=U zw24ZmV2w!9p|qM!zzgubAZK4`Sa+B8F>~_^@e=aGRkr%%3z~iC%%|X2yH7sl3bc!& zOA@t}FJdpAzC7+a*jPXmdh9Xt5Hb4K{Qp>ldsWjM32$r*?&UNfV|cbZSDy{kr~ z%%BHKnk1nkRG@=nluL5XGEyZ^q&+MsV9$qdES=vNRZyJ}Wk$lxraj+vpF^co_$O-Q zuQGlGq=d^e0*rr}umOJJOlusE=~ie2#6?~$REZ)!mboPoQ;rM_T%652={FO;4!Q5w z6fKzGOlqSZ$B&PP@6mWF7-nd-u12^d_(<^~6xQN4M*v3#mug!%dgYW=8u zZSqYtz?OTmLB^YTFTIcn!J7xpY;Ul(I*>*MI@t~V{X5Nj&1-~=W;Uh)OM?4@($M(S z+;}Q_rryk(Y~y&lQvQV{fPC%OkC=rmBAJcNo%-u{xu@rgbDy2Jlemhrs){O6Fp}Z3 zI_V!eI~ge{dMO@Co{(jenjE=7g+H`mxDGni_#B1!W(<=Q*A3$`7@E=HG|^epk*v0gY>C8uHJ)_8AhzCi(bO?HFh+VW zRm0epoR#=czQ0{S3UHSwdBbuJVjfu2&LGH^iC>67JXURGfM2+#%)Oi z+k)pG_V|QK+_-ktE@&%!rju!k5U)r{I&?S3Cx|yFG3ChWHw_=CjLRQUr0ygZVbQH%=_5-ueOG4g= z-3KKtk{?iYAbnCg16Pp*6chr}tKdO53%IX!{0NpvZbkh3Oryvp$Bk5a&bT}>=yo?X&<@M^9i@d%y|XI*PLhC7|B z+h!;5#a1F0j8r?8f8n^USwngpQx(;uhFB+DLZQBLDau&W18qdo=A zr#Uwdw^QHZMvo1*IPjTvqUB=P49^483jVpm|DJ`_Wum8c`wL7#7}(4m@Lv+T7WR}X z^b_Zr>Uj0Du3n^3WuUcV#DPX|ejh7V+buya+bL|#8!})SLY+FQuw^lBQn{OPt}>@{ zuz@geJ(XGrb0@auX3(1<_2YPjjdos{EGE3w{Zp{DX6y)6UClRs$tDrx;h;dD<$XF; zANCfQ6_`%!j8?fnQdH@zWk_rxC7+1#Ide+OV_^pRoDqzF&ICEN#3zD-GFRz0kj458 z%H2@3_4?wMdy}vav|pNB=#NwRkth3>=io<<=Je6KM~aOf7wCTkiWp153!KRzAOv13 z@XdbJ27S2*K~=r`xmR5P^;xoBUi{wOMMt{HQZjn6b9nKRfY3+y(Q+e&W4E^qFV%5~ zFYW(bH3)QB2(tk$d=_K@5Bx8H66{-v?0`>{ z>W#ABi!!OddW%F1{j>57$_Ry6_NSXQMK!PA!h1n$!5o@y8OYjMrSAYBr`O|FtXg$ML(EOYij~_ovtUwQf!z5Lm1>d|B+vjxhn88ROENf)4v6jkfU2 zr(0~>xt}80&rLO0f*Rqf&#=bd zUR)zrD|$b@pwgZ6BmI>wqCuY8EtxvUBYQL-p?pXvQPm{sn!}m9rBdaByWR0)9-8v36lZuRY}@T$i85H48F61}oTSuS#ZsCOoT$Qb^0{ZhX_k7OqgPNO@+4r5JuG3)4;@0AhcB zDFD3s$!|pzj-%*353QV0YDblIVRK7OtiVra3WM}KgZlkdInQj&X&xIq?uy;inh5nC z5F?k1P5nMo+_=<*)AlZ&C4uMFO+v#AX>x6IKfaH@##6#u2lIiXwEI zD(ulDlPuc7EL1m1qgd=)sZm~8yL}p}6P-A84H|dp2@He`xzNlJxOsfL%oaCUKz|~O zE@+A_IPQb)QVPJq7`Bk0J3_ca3zcII9Mm|>Eb1`S?q%wa(geG8AV5=Mopw22;@1mT zKX_noqmtNmB7;Z8O9wH9t*^ZGrbu#Id&@LLL5N=bt>Th? z6^6!-mMo~H`B@iEl0y%IBjT+sB}i_`%g2KW6~VgRz=#)lc;oi|%vyCj@ZH9f zyqSeC!h{ewn8!KzMBXa>dYedAs(oZsU4NVR!1#yLa1E>7w!EDef3`}N4vrtXY9r{> zZbM;;bd-cE+1lq<;%{7qLk?8ab2gYRR&Kj=fH(#mv??$DeItlz2p(W8_C?52LK{In z<==v`BS{|BNJ%j&pA!PA2}!a2{6H++GgV%$0YIE22U5?;XAS9DmPmn_{Xw!VgNg}$ zy}zgI#BVI+f{U~V#g_ipaC#zMK>-muc?2d$&D6@Jk~Fwb&o|pi(u!CDErDmjza|ly ztYRja+mQ@tXTCI{$~hEJJ}FSdcwzxCo1i8{N z+&^cdZlR!}lVOBq2tvEWCJ5k#mVPNHv~ym#W0^m3KB+Cd87SP3o_~ALiYU>qUSh^iKXlZG0;PZ zudA{f&8Ld4&$-ekm}?99NSTEzb5bOS9?8cl*L3*GVnD;#_Fr@!=$7qt5 zQ@1l}T2#fp#o}?D^_eUhJ=lW}*py-oj)py)@d6PU$DQ+(1Bds%4XGnI<6>hwOLSuS z9E2pM>x4hPSA5ce&X+5f5Z2nuyY|<5jLP>kd@}hNM${^!0#`8!=8o}S#DCa zNQik{=zl-M!{JN${VpUJJqiyKQ{Ajr)VM}Ig&5j@yBD@^-OG)B_*}8jI|tG@_*qAM zkzv8yUBgqFmZZT2Jua?@5^ifec|6cdNwYNLQePvxr(g2Sco%1b4MS@(b~I52xvqt#Oh7wf z12D0C5!#RmP1u?De$P)J-Cv&q;D6eGeE;(MyrbvqdB0k6v-ABp-|6*O0P=^ZR2=aF zn)aZA_aC6F>(dV-D~0X(`A^LFpO`7z>kE zok&VB3lph0(N_FS4jHFx6rrX5w)ZG#5-Aq1B)9l*aV30T$AHYj08)xFtS5wz1wcx5^lSrg3q4C&|jyrR;^N7gMbQ=$p$ zHWQG;V@%YTO7{c$4uiViPiZcfq^A3gmGOqRIP$BV$y9Oz^ZuEpV5OAjy9r`^=O)=I@?4 z`sNoeXG6a>iKouZ3eFjee0=%k^O;im;MBf-Zqy>ZO=>kU{329wA`esR#vN8*w8_B% z6`M|nrVO~wg9dj-X0WI-Y|{G0ACdg1ts_g`BYQmdvD_nSf^-2M$`0c@+I3KwI39ui z;ExEkZmYb>xvN47lYzOen&e#Th zD}SCoWlQ0$naez%cuWM&$poVJ@Yh;s2wN@1$Fxx~1NfhxgbPd|bg}4XSR`6{wtuY1 zO0zJWi4+ir8FRIQlIFl~i+OchWV%xTD+xEn^?I*U4 ziEU#hwkA#{e7?NroIhVxcmLXZ_ubvqRo!b{du{FGBI$|Qtfhthddi$mfsa4knbS+L;_-3$!R+X+ zWpKd~8-wB{HMsuO{p?bWN;uiSAo1WQjr!0-9L`ySCRYULIhB{P$7rHFu#0>aTY4kr z`Hogc-jvWFr}`~Bu3|Kr;=fDEwacla$ZQ{TGc$~*HkAi28d;BAm2#3s&jELZ_stb4 z$bgizk}^XVMwVP^2r>UAoYX@L$<7)cM9EP?>4_8+tV>`HN&-y}oEoD%+oH=CoB2D>fvQ-8z(VJd9MI{s2K8no!L{IeO%Szhq6PIWWQZ z(HZNyX=aK__gQu`2{y+goqXU~T{qp>z>7hm;Tk^*&CjL=L(=^$tR?3XXsm1@P)Q^I`ESrxViszT$M=aX+hTo90)tL<@}| zr)CvwIgCdZngQ(@b3#UAE~LT`ZyIr6uc{H8+xj(_*M_&lvW17=e7&Y_S)Lx&k}IRB zUQs8#HGSJY?qk#3A@+Bk^~V2wH8XUFZm_Nv+c4rz)}FyoH67Nct@FoG!o(vg_Qh+q zvi+#4WfjLsIAZdtHnVNa6s)F&#^~k^&J;!iG}@}wF4F8@%(At-_($PdZsVUdz`r&9 zelI82TxDQqib0ooIY`xm*!ca65!H+p_&I2O!!gLK(~fc3rQ7U^fyp92#4`tWs-5X# z2k9nubFh`K@LHUKZ>#G@negkL=@fU#4~FjRy>(@jt(0Q5RJZ)AisFA=Rdy*A$f5M1 zEvCIgYi0S`_^KMuo@mtmOzqiEL`_FIf786zf$uL~yRdm~KloTrCsl)Lw>xA-n%7Tl z&p%*~ecsvmMtu9IA9Izm$z^oRJN5ZzFPTF6iZyQkcZ>eOf`2$VyDWv&uG?TY7&h>oBlPdMC z*x7{P@&|VP7q%}SGf<<; zz}G^uZM+t&Sf90(nf;Wujy1ehO(PB&J%?A!-9OPKq#<$to}zZAc|-gm4E?5Z3+}76 z?p6A(W;K9R97M^jBVM}FD=#>hdcV2{lpVbg(`J&E#NdN)-lKsuvo>X$ck5HKuXYYW z_TYzgOxu%t2duTN5)?6}n$0n+%dU&PMr!!)uI;hISmm|P>g7!6J zC*8%r!)PLyTHUtz6BBLuT6w*BMl5PoNA6Gho5?|$f7~WN<>N8S-7?wMZQo{TATgkM zpX%l`>K8A!W9wE$@9}=o#gF;z^YS_TC0B4OR&9+~rYRDF)jl0d4^f1;hP%*QY)*Mz zWpAjo)YzPVk~)fBL?U`8d+;OcDO+t#oVrr>UclIWl@sttWk!_SFO^kPuaDjUDNBV5 zUQuKWF84-IGyHcYRQMMB1Z3HS61t}OLdOBRuxER}1~MhC1BEWeYk@ZrNUh(#UgWqT zus?D95=i_4ehqX~3cUP6R+KV(`XEl|`}%HO?TDQs#KK<5ao_)FXrTHc!OU(xw((v; z%0Q~=V<0ZhszF8qzTx;J$*bU6_x=mD?9s;g}qX{E`zerge9%TCHxevyhxQ@LVg18<+MLt16(c+Q;p zM&lNWf01Z}ftJEnDG1p?=G)bP*`!9-Jyg^RC#c8blA8`}FE18h5xiT0} zqRk1mt(=oY{F=(n5X<@)|2}eoUY$seqA$+LLw(!d&xq4mWtlV zjkrR4@g(r_Hjc-07Rbj>NcVm(L7M#V!Qz8 z^^b_YTq02L#M zzTSQKfiIQBalrX@_knyMyyRii|8)QQ?(PdLtXKSg*u)Bw!x6xns&(MiadN}`eJ;{0 ze7`pnpqTW8_R*G*_{p$t{p#kD85w)>2XICpV8iOfcUTa1j^T>Wjn|~`mw`Q_*_YY5 z?%*$VM}#XqljYOGs_lIL>c6ru?NqDG;h6~^IDJzC&27MyLa!`>G#d~w*SwaHi5&-lg@uS zfgT6*z)$|9uSh6cxf18i74XS2V9u#}hZ^&o>wX139^$(nsz4uE84M?f?SpOUFC4gp z4=FL13ewAjbU*wZ_1dnL!@YS}4e)e*pVgh%VzR;+WOmDk2Q4P)YpuRBsvJL7HL#x} zbYxi1Z~?$d?0Uydpy8uohR}04e}c>+s}WJ7#h2{>kh_h3io!xV^^iCb_-nE%?J=C4 zP=vB7atm&mp}hMb)5`!CIs zJ(kvz4yMpVVcM$G6Ny6SK_Q-jF`hLdl068yi^LKOi=X)DZjs(AOCF7N-?gJ&LLr7Q zM6|*UCXP?^shrbK9!<}WKDPZ1ZLQxVvVdo*7*O7MnJj$Nyqaue3*ii6pOFN$tXkj* zO1(z%p=`fK)gI7)jsaokAE2z1vQ~CO2iL)UHfbb@%YvOE$EHry74=zxnz|Lo0AWj= zUO&Rb>Lus%mcqKt7^4QaKR2$}+si2;ED`Ck^rY*i_(;X}8hp^x!oH zPjEW7r+LGMn%A;`Cw_z{&yahgc&j#010&s!y=f*!N?Ui@ybz9!I#+emYXut}&m$x< z@(U>t%+a{9nRYvox;Cba3Sd~|Cy30=X7h$E+?454Gk@$GX6lyW4R-kn*q>0xhfCwH z%t8j>n;(YwuL40(W1u<5O42m2a)O$ZSVxOeX(HHcY1ERUbW-)ChVu=qp7XwxcR&!BF1 z1(xGtw-Cn%s=z_|kKJe5umqH?9o*6$WTsTWm=R|Do7)?Kq({fsK57Z(v!$kAg5W2f zDwf<(Ivug%0kgv^C82t;}w&(gm>R4)`5_ouE@OvyBOB^-T&| z1AZ>Sx*uZ(cii-JBjAludEyk}82fcI#P#nJq`{6=Fft2z{GMv{GI*lQtO&oeiDYKf z8i6p=ghD)2-|Nif@$ofuRvuCQiD?hGrDr6} z=F;A49So`C)DHS%JUVghQA1OW|D%t>dwGM-7+*st?hVTkw5A`@ZWvBKNI9tOE=*MD z(bC^LHI#wd!L<=`hVn*|PwxgM7Ahxm*y#bJX;vZT_dZsRUI1{ccQQY%L#*a`t#eXp zLSb&qp+QF;IauG=1Hsj!#Eo&k2sP(ik6)A&V@@n}%%?Mail@imSWAUyo?iO6)RRc} zX-={fuft+?Acn+_GqG6*uOU~Htq~T$o-zxT>tf!*F6L4+U<{w7uoVAnG{V4!7@(Cg z;)EB*Vr<MuoH@S%m-kfLHpYvCvc!?|N+1=9pO~FC4eAsjw%F38FOTBO#OpoG3BgD6|tPd$YeA4a2+BAQeHR_WOlvk(?-jw7Oh%n=IS2_h( zv$ukIl+~`)ow_yblrIm|%@pj1GOODvMo&^BW$I+qTEeTW6^WSJY~^P&kQ#}W>uNAH z!hfnf)Hlh}{mm#{1#&t^+u;vZ$+bdj%Gr*Q%C;LX%Wq-sp7FKYEQCp zbPGvvRLlPpKg`e8#6z_O!v&J*c3*;@S|ZuWV?(?TRSPH8X%l7*dZ{bcn%4@uXc`eR z#E%WLS9BoQBxS?X1Yu0eJKCsn`!7e@H%3bJuZmIUn%*^01tV#@qF%%XuLGo&Ga0Sl zIbS9Te}(650ba;$UUBFx^>CpS_P(VFX!zU-e2D1=8V18v-&U9K!oQUMQScGleSx;H z2YNR0O)vZ)h6_|xdB^Hpc{e9Pv71@lwzpqK0Yn>qpaor53s&ufaLw1s4$3dJFfjLo z#0n0-uRpCEqC#zcaF}{buK7iJ33*#mVa?N9iEP&33h>0%;+miz&fSt?Pg95!#9g&K z$#c66cD7_#4e8W0#zvf10y$}k1_ePPcn#!N?!cvDI!ad z{yu44&Q7Ko+ab&|R@h*-xNqel%?JvrMnGj)y4Gog2Rpm`8Ko}0V0}vNDoGx` zf_^JsMLZsNV|cDRpo*^1qcjP3g@FK*P*W25F_2E~SB{y2#j0%%putOvf~o0mOa#wd z%4*E>OrC#S<9ZkEg_qz_{f6afBEhY+tG()&@;>SjQ3uTYk4aIj%yc$06L=#z_OM6p ze>s`&>t|Sr!J6Y!zMRZZri65e4{}2a0UC2lA)&nevI0n=JIM~t&R0R9=9u*6-NvjYRF>oBO$wygmuvd+t0`QSkHvm}F#3CXnE-@k-lN_8HZ z;(c(_9N+cTWHvM<3`BhpcX8*XG8WJkO&D;nCj2h5j?Yf+Qn!<$r?wNbNxzG?Ceu}b zD!#c5eAc@ym^reNGpvEAWq|vagEGAR1ZGD>-P5x}@xN~Ro_u?rg7RnT{-Ij-F=7)8 zocBa!0y8kQm|XuXIzsBR0i3y0wJ;*}n+h2v@J5UB@fMp7_G?y6So^k%#d_N>(2w&~& zTHI+)-Sx>NAl1ipV~u~4R=~-PsM>4A{@Of(fNUm7EEU%^%1g6m+>wnRvCUFyXAR@R z=-J}fCJq&Pb-~hF{bg3duj<-pL1e+z@?VEbirRW3YyfoZ3W`Z0LC#gOSgU|4PUj~h zzXLXMAopA}_y>cfoB)WPHZj1%dT8N1;AfkzB{LpEtXQjw3XY(pEiqoo7RMe8G@naQ z$*6s0q#67>VJC`$Fz-~qSXi*5HR}T6A{4I)*aP=4s2cHcDX2X6NHjPz!!wrvv))F7 zyWL{gY38aOzw|X_HDFSlLp-;VVk_lGt~(ZLHWf013amA&R_s~9%HPQtKR~?`W$EL{ zJcCn(1k~Pbp@Y|vpdF5o-|uPl?maY)erkfrae{~Gqh%eXfG}iQpN1K$gVz-mOW;cD z+XS;R^pS^hsz&;Uw<>!bnuCW4qiN?=UjCXL$uhfI2?OxP*kuv9RYPe7tlTpW3E?me zeXO2KeJVh_tx>$^VtJTv$&zQ%L0>qdx_g;-y9?tddoDF-1MLnM|0!iC3Z_W-v_Y?=OBihfLq{-K;alni zN53koFk1ty?~7Lw9ve$xph~jTANFG6arGS5lqz8IXRUzQao8x!vXtbwP$t>bToEgR zB_x$NcO#J{=abq>EyCWoMRZ`C6rr0LZ(Z22)%GWhh})9BXnB$=jHC)f5Uyy1nqQpY z=-}oph#?jZ&xj!Q*fLpUqU!G~cm+j)`Wl^MqD)$Fo*8#UYqu33Ep!|3o2ds})HWo8@I3e@v&KcD$g{sg&bp!4wjkg;SQ*HNDL5eV{`>^b4cic`oY}%- z*61+Yh3vXaGns_ry6`XvrX3rXAW;A)Ta@XsAPrA)-1Va^k75(exj0e=1m-bJNFDKe zxI|pfn&}Pq-e$0nwrX_H7)r6ksHOUjie2(*YDmTAL--NTA~*R7@j*o&s%6rrIG+~H8x#f|)^Tk1 zouEZgQDUh6oe8OQ$JKz0JaP2~@w=cUQ8_>!$V0LRbIv=#PsgcA)!%msm5!Gm2gc(I0IkRC&la*96s!7>D9EuCVw67

    $=Nes$vsW~pOCtUnEt6JQ zB;j*;bp=-#ovJS6l6GN&5ww-043cuq{uCaXq|oM))D*SLbp2E#d`gL656~4Jo*?1o zp-!QM(HI^nwyo^h(cCvq{bIL;9@{C-35P&C%P=@^v^0zNz=nZ;ir80nqHs`_jDb_C z25{kdL=s7W2tWzm4MDEAT09>b721I=X$?%GwJtUfe@yA-VzFLtXwo3Byw+Uwdz;KV z7sdVQ{-Go<8;9Pz-B0i)VXwF)`P&^Ve|l9|b4au2UlCr)BuO3Nvgq0hK@Ll17tbTk z@)(DRjwguO7eR^Iq(&nPdEDk}=5w@*K3Jnp8K=9dnl&0o`k>VuwY~skgA!k#i)dFG zU}BJcyOVXamj!4EvQS=^r?;gJ#agIOay6*p)1l4ZNDyMqfP6P7%6`H|ac{}~y#X3! zzXi_komQEj9bF@1(n%*ksO6=e74o+sjJ(J`xSz7O$p>dO##zb0u(P!n7mYP$S07Iz zC5HZ)N)XHvQC3JR^goj8p^a-^-~bI?4>O#;HoWKKIiB^7hRsFY@-%`Gs|=t!lrpa0iNM?gpMgK@u+{_jC-G6$Q%{C; z2Jz8^bE_h6gi2TsZH%uHHwdBpxV=~gLCq0h4!1!kk>!II!2L+vP5ycAh6oei_eKjQ z#g9PxZ;h70=!R28Y64)zsffcsP=E3>j}jr@HjR%yL`Hi4V|>M_qc2+7l+P&&vnOOM za*u}F0iMe=Cup)?yrl9M_*0B%uWw*hiD89tuGgB2Cv#GwT;qXL^(liSGHT{ZIor4R z9CcttC&4nSDQ7I6ec^w!&)K0JI2(fp`=;4C;effTc|{X%734J1AkeFU=YeG75a%W_ zu<$vYLHDqqU=%W2o(hJWY*b@^SXe zsAy9gVfEkkqMPEReZ3SRusYz=P+H+~U>HFlM4^M>QQ29TAr%mAMbOjtwcbZz#2lVE za3hSTIOq`!?rTMkK-}dqCW47zbtisrX@sHd^L=3@B6yzv#_(bp7&lNaPi~}S=q#EI zp*#f3dP9hXXKa44Pqmb>t(%Y4QCQ2FW1ze&(kPYMz7XRc(Hk#GG63Tm^+C9GAxf7a zDRuS_lY$H%us74ju%!v?h=H%t1BXd+ROKc^GdGTfnCzC=BnQ>hreYkFhofc&jRfe{ z7oXz!ev9jR7S)@0yrl4kfx`MjQr_m_aQ#E~Y;W~CMTvJX`>h+F(6(BRcb42G)pzjk z2z7^qyF4QXMbf* zn199?L6X!fCK6Y>1ZgUan<^H=V8<3(1%DwnC7yCBDn5#{y10GeS2xtu_n~fq)|Cuu zCHPsXWi)FQ}H zCq`rR52F-p?VN-tCk%Od_I!7n^5dE31rlWCAif+PEYUqn7|P)PG;0}SQ01wD2{_APiNEQUp9p>{5D7mKT(;iJvcmw@|_rQ z>6C;JY^f__AAk4*J?^?Kpz`-t+{5yV6Pag=pcg&t7jp}E39Tv|5?huz$Fm(YDT8_e z77H+bG2DlW44D%;FIcN8EZm{p%V8SIwMgw)Y7c+xew-V)Z8WT9JceqibQbw60jIv2 zn-RD4&F}rjzd|G~sxnI;Vw?V%FYnY$-!E0^8uxAJkFC^Xe6Get(2SouUv$#j0?0yU zTVD`?AlPJ9A0ZfkzyUDxEcbL7wm=%e!BEB2OK(N3v)>2k+Vu4;IdU{}`%SZJ%d}n; z#ve?o@d~RmmIE7|f_n0MOHsB-rPDTq8Xb$0nBQ;j*s#r*R8bdlbr8J5`uUKXMsLtA z2y;*ES5CzawAvuP7=yB|L3wjBcoJb(jCXZq1_wz| z^KmOFSWP&r6IY#gwWJ*BcGarN)?O@idKO%oEI(f$>Ek}vS69b8Z=OFz*u{As0h|)K zC$|9N`rY?jgd<>Rpt-@Li=Uq#E78l@?s6*Z`%i+~oAHl_N-zGLJOboMDys3r9T#VQ z9)T}~o6gudhxv#o5E|Smo=q8uLRiv#1Ce9trdW6}K0n&NiyJYYuL|I2;MeT;=JX5J zs;|HQ@^d`}v#9^`B#;ycc7G!@A@ueM9OQVt=(*X!BDRSK=6<{zrIP`xzCHuVSA0Kg zx%o8{(D_ZQ9JcK6sh(c~EvqT6Oy?p0OiOhDd-y)D-wXd@AAbM`uL8{s_6@LToC+N^&4cH$7uD8&_I6`T1JmO| z_3m~`ZBlG6m+q>+ZEiEeRqfar)!GTWq-u?_!9AMSL|^Sv06(s^aj5AjfAraWDSK7c zai0aX!eQ(V=+F79&?y3Fm+}wQjL)Za(_k6YFTfpdER<6Tf8UQEF@@Pff~yc-YkNf8Avv@gO>F1a(2?_?ZFXu*z0}dzn4MgS>`P(BY%0 z@;P3V^Ix}(n_tSdLiP3)rc^owwKhzBLwz2y!YFFWDGw38n$5a5?@vmf$Po1PW^kwb zyg<1e`?dSL!2K_RAGvoixpb(Q#?f&Mm#n`rGnjC)v)! zPlgq&oQf8;g;OK^>G#h%JlK*kQ?KfbaNq7J3%R@;g?#N>9dN&rcEXf1s^yf0-~vo$ z%KlUX^x1$uNy3D;2Tk^tc!caS$r;f{!qXaXPt}yQcVU!{?)Mh{EZgF{X_WK&X8S@HR%pJc7GrPDxIu5A8xW3gd|lKD*0rWto*m~ zF}>2T@#ZlcvVU=z?_w1a38}v79VHk3oxKRV41fvKN|C2jK#AF#KrrFmV-m+4L_NvT zZy*dx^2dNdQEoqhwt%x0{{e^X&8my}-YF0BeFha~LfT8iJQ^$C}S|8QY zyT+x^WK1D(*gNsvJ<60sc1b0VZ{tZlk>+V3Bk?K+6%`L7z>PPosHzxAXNZt~{q;Gu zZX@Eto>B4S06ub0kKhCjLy8(GpMy9ljvgg%C}8E&YGad9!^AO|YJ zD6%PleTsA1m+H3Rue=f;Egyn%oC+?X+lz3`2T^Q?#yXh&l3cSWn)j3u9*KHYH;8bQ zqh};BGRPyfX6=M-D~F9LsNq|EM!FA|RQHd~6jlIt=apx=k%Z58YR!lZ(PFEJorreP zKef+io+VNYAwBWBOCsDhbt}1EF_xy%77mSt_S3So*Q*>3;^K|pxy(Ds7|@8YldWi_ z^XKUc(~KJoC4TM@#H2mDGSr-%+R@)G*>WE!jqu&j@xlbwA965AZbTIygQqWQDliP_ zjs(pWQ4KqTo9QzZd6>4i(p+4D_h!&@mcG6~bde^U0r53_E0eRz7nbUI*L&PjMf+Y2 znrDaK5A?$MO9K?!>1KNb9hVa4j8G+2ZN-Y>ePn81_%mV zmjXm0m8tLidH%ifsfgI)FRYEIijku}#2pG>u7?Fsj!Ln`()&h(S$5~MIis$VEw`}C*PFA@%DAA4Am}(!;)!nSJJv3+XIV#7L z!3|BiX%He|M2ie}6P??~@hzIKOux$OC}xuhE|*+nkAhag2sBCA3$U5laTJ#yioMDf zF+VBk{~Hkb3EwNocJQPVzK)jYW_e4czRj9`j4~8>g(@i)%T~c%4W>9{LXwvBhd0~^ zAU4=5)sdB;!bYKZ8gVN_tTj1ni!ygQwyG&frJAUqn+kL9qo<97DR!_IOOXm*PQHOG zkI}ukpic0FwcjPove{8LXtTl`O_vgu`%$f*snkk2P2DehlA=EzSX46{FcnsCzHY#f zt75gP-_B}i-lM^H5$;2jY^Fx7`q{ib>$Kt}veB&1R7}7c>lYJEx4~}aLQQx0$4^dy zQirGn7G9g^A`3a2h!{r`N5hC!!=m}sznrXx1WOW=pjEICzKi+2Dmd#dsUyt!>1d-G zl7L9gR`}I*s;LyBmYb#=zi_@Rq}j53LkAO_^j=U^L^4>sBBi=~f&eDexVwQ&w$>Yk zRzfy~36oa^o`qUYLcC0YBU3c>1)jT8l^hD8S~Z&E9e0hFML0s4=|>e@(57!;^YB7( z0!ayt_GJWEv{YPetJI?%^y%>h*)I%5`M2+dGg#QC(cNS4*a{sZdQ$YmN%6~Ic+L0y zdYH^O?_b?#9=LlVhdJ*z3HX}LQ|HKBU5`vme*THnRlJjfqWMrRzGdAflEpC82wnCk z)xlK1=Sqtc;J7L!GpmCBrD#w7P?j>KC~v;#O5U6h$@?u!lTO^MTZFafe7M-%uLzuC zWt}%LU*||(2W;u>)%wP#iP_PD>Y8N%VgVX`RqOIUeo*waYvt$EnXoj|^63caexSR~ zt>DeGb|3^P{PESnIPOqQ@V%1{g8L-v^syWooCw@NdzU;ZY0A6%R*8=l%C=i5$RGB# zkg9u0(L$dYFzBBf2_-$z1XHBF&HCdgXbpozeTk*byLBr%@8}^Ju{86{K6wW$H8+up-gsQTR=Gp<(=a{S? zo;9=@W9b3V+ynH3j2C~P3J-8v!kkf{GQ-yw32<_zreFZH&yB~cAPDVuq19yl95&&|IGfelGPGBh#E4T1P*=gNU9c z8Zp2?Zg9OK<4j292eK_tn^(2GRV~VPUmumFio`Ya@*gu~IOpCECMu_z6%%deCvEvK z{{GREbxcZa`%=%RuAn84Fzx~1!(>Vd10s3R@HjV`OQx}fI<2NTt$qa0b|(6_={p)^bJxRL z)n@VRPbs!9Va_OKJA2Y!wxP|1tDn65t3TzC7#PcE-~;o<3@R`n-2){Wg+EJE`N<)f~>1MPE@6Z2YHD|DYETF8CXHnMj+T_T2lKN)!Ss zQkoYpe0(%PX8K^t1yWm{kGVjGGOfs{EQ*>-^%I{bF03f8xIykoU4ul|0KQ?e18L|S z66;33J0*z9Er6~%`MjeqixNEcl!CBs2Md7v$hpQ8FUk@dh(&>TZ^1_>Z+z!hx(pRQ zqXa`mK0fnI#g!kB=@+Nj6SjsqO){``1$-=xVagwGNrY&qiJ8^XYo-G|iW`x6RwsMV zh2rGqq%8pP&~&cHES4M8KI%c07?K{AIXM^#lN6-Xt}*7x1uy?PjE{?lcT4iGbI`^jqR^0rT2r&)X6w}Er# z*=h)0#Sm<=_Sr~7NiXHr!M0*3`%OEeTiR%%H0c~Xmj3Ldz})DI3;F)i_95ZJn{3s2 zX02Y39r4{4i0pgZlJ1GbyoQ=+9xbbs2m64n1q&X%u265*5bHIfRc7@|3)a)#@3V&K zf>n-koalGz$_LLI70J31B0(Par}hI}6Ly}lgbr8}B7lvSDKAl|wkGX-iQQwB!dLCq zX0U^4(EjbCbq8}}3?4|5Cuy`muWSjsWKhrdWueLaF@64dIjuXc^Cf(@h?azXtN+_J zR>#kOq;08NgQ81JP9xi3YjHCJ8n!wND~G* zx8+fAVHqnnbl{2pw|_CF3A3brMQGKH<+TOBqi@5A!Tp zWqL9+N2wxGy8M}-DL~RnE`KlqeDs{Uugw5$tWDfj+w-?&)rK=UZ5)+^R_O`wMSD2& zbK|L$+SuvgZG;d^{)s2PerF#j=LHuntl<@J)}IhH6hYI6t9iAY|3@tuw{8%^s~2l1U~$fR<_tAyr!kGgOSr-jc_nXGEU z9A%D!1;P>OcI?v34sCiy@a)d|!>m(TJf&DxZ=KYjS#Pv2jt>UE`9G7z3$Z2h97xAQdMP=1FVQNXX6T*~Emx7|%JCLmIT~g9MWXof$#s-hXp=_qt2`5;_U|wSAt* z4GjH{+|SQ@dk7>}%1<>&ZS6ZU#5T#0>6$AkbZs`($&UNzxxHu2Y0+x1*$f>H{iWO+ zsP27_2|FIEYb4p43qD4Z3g%THd-d9M2fVW-`dkUPY#Gl7B1;Y@?*e~*1LeR@sl^aPnIR5E3MO|F43<9UxiLa~TSA62yQm&Ob4x z8Q(x7shagke$}bx-?qh{4>UwnpFqE)4pPX!A3{7pssP~4@%L~g0lMcv!gFExPvGR; zH?;n8=lunA5kh(cW>ob10~4hXKY=pN-&D6hzvd^9KZjw6o%W9w1kL{VgR=4wTRzZj z|0&rX$9o)&N)TEBjE2WFW%U5774hS)!;2}w*Dnitw6WkT^-hic-9>NBjPVnB0}gguw&=#$_4-|jO+V!68fL;g~M{7lN7Y>8Bl8c8^`~@ z(r`Y3o)v*~pFnH(tN+R}1OhvW^S%Q4&=x{~LWI7rf&X9UTj1g3!UOO>@#DK)+*hC; z=s+tlbkCd{uaNyQ@V{~I)!zaGnjR}*Zv0BypjV(m=~Z_Gw`(V8^O=e(=o75|Q!$qH z>XVWHYkg$LE557O+GgM0VJv11Uuzpbo44#&iHhKoash(u8vwBS(`h)D);7e?;&2E53Z~7z9v%+WR0;b}LykW%&9}eig%F$hbzX#ey#qf0 zn&(8GHoQbEb7 zmK@|vHNWz}%qoj_WA8X~LWuy-1`tWqO9>dDeL;5#`eS^qgf}<>JFH=l!6fgc#CIDO zBWCNUN(ptv1{WdQW60<*FPquuS$f zQ*a#BmMU3u_4Xb#pnO56wRSK6|rP0o* z0bHE0B2EVbx$H^jcpANC4Un3;d z2(%tE*%#K39BK)wa?<>EVWB%%I=6IvY7+w-%OB9UpAbaTS zkOCZeJIol*IdJQPm5nSsJ;={9Fekv;Z=f)jcB$kJm`xQS2mnTx9VsvDVt`g1oNYt$)3DqkX#6k57?dUp7S21?(gLrp zidg653MO~7c(BHIK4T-KmR#M?;{{Q?h`@i??xB9(zduFc#ey3l77!;x&2NKhE(gTm zTwix)?sjzwQ|uQ_2-aIUm<;Bl?3W#o{SdWjWOGegDNSeS9;tFUP$^P7w-AJ3cbR%&<5kx7*Q%!mN`H$Z zW4hVc%e5O={5Uowvdi(B@c7&;#=+^J;FA52FK$G0f`;Xjk~Q=2dESLZkT+~wV+qP9 zCczF1dCO%535x*534j=RT8*2{M|5I@Y_iq2prr;fZs`lfXq%o z%iWjhdGI=d+ZhW$;U?CW_le*;D*Mr@RJ{sALseayZ{fZHSPh1fJ@Zuy5!<{z*3}Fq z+x$WnY=Z77w0rsJY;(1A>s&xsSYJ2A82H5u=3wNwbrgcE$g;E}1q*~#RZ-S= zGX6|O5Ca2EMR1tfrD2dLia$qMKe*kC7S8z}i-Op(9fVmtK_IigG+4P1#9NJ;QWVU< z@~teLf-;8(Flrq`Xf#pH!fmp1u%W6Hp-Z4?N@Z9=*=HW2n1!M|t5c*~v$)Y6YAvCg zG%_wFN*5m%67sOImY!O5v?~eM!v$P6oY;o)E^Or_$9XERuY9)c_(!kf7J*kd8Wb4E@K!+;Il^N+stF4|Gd$(T?7Bz9%B0&W;hazTC z^J(r!fW&6EU#20>6F)U1#*=m{QgB)ZjbsEL}M0C1zXbcpG+b8`k-tzQL4bV$e;xZ z8JHN5A?bE6fRugk>wnld-iHU}z`|dS!n}@P3(e$Ml7kDr9Q~8#HqgO)O-ELa z1CU#X!aXqyfCW=bgY-0VYze;pEQ9SuPO_zmQrn8Qve!iSYEQ#v79U^Sn{S9M9OPrJ zpt?JIGPb-*F7}+jnoaz(xzcj8Ns6}dszo8^!m(f}gYayA;?69^C8p z`y(j$`GCZu@g0(&n-m1xVaw)LPoGB37N5y?;w8X?qb9b$8c(dR2w*4tQ{*tDTyo+H z@zxcu$V3JbhP@8e`3h}T1P2nywAD!*yf@2oM>_&9k4t0I+GTaHN*zb)GFuE0$135t zW-z;A(HGEkA8%kP3v*~K;8q#zd(w<$nZ7NJHj9%WH8^d9g=FU$be=1M&EXj>L5fiN zG$n$F;|xU%YD-u)-Fd%Q(@&b9=`T!uW*tt>o1LKP=S^}=(R|CBgI{56QF+V*QXDjb z2bwC(R(2wjC0-K!+!=qWW=)^MiP4+SF*1&j0&Qs2TuEtsfRR@WgNSMphLAS|(S9ce zT9_r&*GJ0)ZBT5k8~=Yq)i;;_*oqUQv$(R(O6RAah1wW0%c1T#XSc45t!@>S2x1{2 z^8dJc=itnqsB1K~?U^{4I1}fIZQHi(WMbRq6Wg5FwryLJ8v5O*!3; z{(`NpxH%|k6q*1Apa-fIz3R+RR>`TMq9M6;QzUY^x)hYfWg1rf7R*dkTPNC#x=yE^ zugi1S+%E3S=%|3-$wfZjzezE?} zK|KLS2Q9RFs)@nhwMWDU-mx2dLX{`btr~Tl>VO)=EP}A$D~BVq=0In}9Ya!+u(L+> zhG+b5Z)dy@`~vz)|47y4u=b)PsK4e8B~Z0&{LNc@8OrDyfgn$jrp1iq4}Z^)5hpMv zS(ksas%L|BK|4Ctqqto#YhcG3fn@KD@ciU5xt&fMYfLjupVjv1nt&qao$-QXPsKIt z(n(pJFwXOu-hIaQQ$td`g#SDb zGBZx*Sy64(cBg6Gs9r<8^Y%Ghyga_NXZekws{847$}I zoo)sR;$FQ5FMp@qt0lM6)~@aH2L*ls2pa}Ti^ZiF*{NP6prf3^f1OfNiRn%U+o4LI zC9prmsom4TW6X*=4?(cO0k&c?S*lCnQX90z0of&({ItUY&2ERppI>evR3m?= zx+A^*$B-UMhejOpLc&mE#P0;)ZhYg;8Mv&UFCa{oT zqfkE!fnu+>9KyTPy(e96mFG+W337TO1P?mroOkwduFv28I@jm#9B-gc_1EFJNxoHl zPQ%#FA9LQncNT~R)h~i)qQ=E>f`pBOG}F5S2d#z+el6?7V+2m(_u!b6xGlYN!H&N3 zVHz}xcf|tb$$;zrw|dO2Gd7BG$HUtpAHO>C%xAC26G$9jot!$DY$Zb8yH~8~RlQdZ zb{^ES>gYw)5_yI?J8F%uKG__7yC-BYS$IE5PHOTXlw9SAqBG2;Iz~;V>K`7C`7wYN zJ5{(!B25a%#C&7&5)bn4r-8zH*`)C41z@7!TFI01^}R*!*|+%EuCtx&lJKe9U4hJ= z@P=>NLnVj3(K?J5g1-np4{Z9G%Y7Kk8)*y6r#td^_P)KDgQ9U07|u%iGRs=nuc@ z#v+4%N%B|!BkD(+Yqi8j7Ofi^>8{H^UU&CQgBSNxVn4UgLqlcMfm_~b>+=^y2G3cQ zxw)x9nYphT7$KHZk!a~0(JJgr-zu%)!xwihR z=-Z)_*cP7U!+{k24!J*ZUjEjCX5Kb(=2;XN4SdoU08!1!gED@z-;o^&2D>_oMUh5% zPpIjh{TE(_<}XETtftXAnQp(-q^eu=Oah<5XFz`DI+s=pZbMOY2Cfuccs@!rov#+g zKVtpKr_VCz*mz!YJZbpqKn!|?BJsgb$~9;K*r(^VH4cF3TTsQnUouO#mG32|dHt>L zt_+C>j&*^>JC~m*#X)r20_P6CO`Md9j-BFngnu1yKeH-7F(&7YpB$hkzd9w{Y7a0( z=DvjmoAb__Q<)aM=v=`@MclV+I^3|z9SKVu#F0x>S!RUie0S*P^Ser4^Xt3(qHT5X zsbdx?{B$N&pa(}X6?zOtrxT{)rwAZ_7mXr`wR#;CI!@Ah5ELv zRqszobVZz4GB(rmuVUr=yx;ccf8>6?JPZT*c!|x`K}$3*MILGPtS1Wql<4J6TI@j> z-LetJ@)B?4(-aB?OV`_I8?yLms@^^_raUy;A5tlmn>ml! z{b0MafVa%jcBjK}Q(`q-bcn`P^IPGNI6H7ySy0 zUH4Sd7UzRU%(}$V0{db%aBnGe==>dI8Sl9J61&Bb{7Dk2Vf9+2-4)ugk8>1mIdq6G zBmENQI1~a&s^*k8d?4-kn>te-ReZV>G@Z7WtxlZ$m9#(m-`#@UxArVoF%)5GM1 zPwNL;IsoqlY=lJrjb*4|<;z|5ft`d$cb4X?U`+FDpyjN1OB(LyHB{C>u4;p-dwY}0 z(_@deWpXxH&dY>Q0mQjCmYQ5^5zpY7Hk01A_UAgHZiYLe`0XF)NyqkeL_5dB+;A0A zwlpk`{{N3zOGDiRTJNS}Unt*j1xG9nZ)L%H@AI`Z<=V7DhO}43D;#69vd{R&mhn=*kgR`8nY)J<17_}Vl;ML** zsyH8ukhq`K--!x<#*D*$<)_kLQn|Z^6-SHfLjs)2dJtio459 zkvJbx!CHhstWeRvRl~5MK{N8Egxek|v@mLSbkR^o;0H1Zz7tEkl~(8dJ;|4b5JC1{ zq)}_RIf9LfL;D$aCNl0tG)V}FUEzS#_S#ztzqz(V|DEd8-g(unI{@_s2 z+|qG)IMIU!nrF*hZ=&Nl9z^Q5lx8B->H@Y$cL4uAPvk2gH_?ePTb7BO~yc z?nLNAGOBeqzs}73Via@a2lwa7?QjgA>%`-~x_kA!x+W#{{U_5ZBO?!<3rS1?RwA9@ za7HDUW4ze%3)X4U0=|-npY_42SOF@3LZCfd^__VXv3D(97uCt;2}y0FvbFKiayLC* zQ>Jx0ea46FVO2H7Br>g)?4(#Z+ruJ+%_~yWWm`@n2GiIOI=eGM73xyUiJ&`T%ZUWj ze2RF-r8JOq;7|&?s@Nifjer`$kDHk7980ou9>NW1kNwt5lrHZWi0?2?skf4~XE^Ke zzpV;AX8- z#RFAxRB_+Er5nd0#g2y25#$&veLQbRG}j}=a{jo(6(=m<2cEsn)~I6g#Vg#1({P zP9vPFh-_J|^W!R>7Z<}mV;%-vH%F_h)sSOkObu}^WI|vTR+pr=zBsGAVi0Cl>k@fu zbJgK%OSgOSV4-l=6pmVs)xLZ%M4xOcJV-d8yAiq6TY9(5Qd~dOoOMag zQ*k`#0d*LiX@6ozLzzIOp{Vs=j?@LEmvZSdA&uaa|Hy)bETe95?s$iJviS- z_Y>$g1$DFM?A|)O=1W`c^~4c*fx~`Mi*h$GXc1w{{JqK+gWX``O7-YQg}vgL*Y-A% zsWjrZ7|gi3@o<%x*xbea*wJAI)}JEHHwpCCvA~g^`0(;pb^%TMu~sj^vRfl*obx3-5GWy!opC%x-=JQt}A>`l7T(@-&CU~(to2hpN3P6 z#4oLcFHYiyGjVP4LK8 z-GjUTrp`Fs{~I9->+&zrr&JnQkamQ_Iv7!ckvV6%mra@wu8tyA?U(6^Re~8{(hETL z{q6}8C*{W8%wq-ITZL%}eb+0vU#Ow9{3*&k!~-aWXnxtUC?R)P9qb<7e^Nx^r6yWo zaZd<^3X_jhAs7FOp$^?j*t6KzmA!p@UmR9(Y^Wz6>um~<^GJ_Gt#z)CqIJ9E-A};c zca^XzBMWX`{+5j7syO%VA#p;1pqOxfMjoynRJ9_4b)mIiD$I~Ku5uN@%ioZFdIXbVEi%xQm z*>9!uOX9gJw1$@M5fh3HnR}2oHy=HzZb%T0OvC75ZO1h6z?gLx|M*(@E@<2pCvz91 z+XuuRT^D@Kyt7K=_=yl3N&g-VfH)v%fi*R~mF)N_eUxc+#e}N!`-|we+8CSy{C4Ul zlIXD^jr%`+EGJ4U7=}$*@V}=if$ds7vhcB%h^(|-89mcxVt3t(SNWkti{~!&?dk)+ zmxl%CUqJ^rzw#^J&8@MLJmFhpDO zeU#U2cY$u_k?ZcIWe+iYv%&3od&JWXldiu5QAnYf6CZ(ACcVmXFWdhMYNJ4`i6@%R zj_uIPyKE*;`)B%m_*xwBvJXQ~@46!mJ8|Unw_y`Gtg004GBw8g^Klayc>()eI06G_ z%te6LBq8A^hN8l=6K4QQP~O&vtP^+Va1iWtUDACX%XEPub>MR4c1y79K5ogz`y` z5|5q)G26=6d*0mx{rSc#?lk)RMhnaFMUT+G$>^^ZwYnPJCtR{@>C<-&Iw`qwB9bfF zC(~j6H-!Xuke_7hg~XBgo-AHzcGntPYlYt2a(7Qe2eLW)6tNCPz!xl{JsUuO#|@0# zZsf^D=*dx!|LsfnZ)zzFKih+&UD;aIk+%&Dma%X<3 zkSB%lVq9Bf>&GP@;k>%VZewx7t|+o@wi|Q0kjKhXGu#OM$6`TlNScN5#xj#Cz1@?s zVTPWW9Gj^wN}rBS{lmGTpe*FQsL=O8Nt#aOfmG*@abm+X#{FYEadQGag}CM?y5(At zs=KAB8j);SjN3rA%^a=7WXO=G!G12Ou>$kiqIAK}|Jk>d)BM@FvlLDI*+2g4@TqI9 zleml@#Xb4lK7o3#>`4u%y&SdJIM8{XRcW;?k3bDLp^5@TrYMVQL{w4B6v}|LTxx<} z9ZM!z&}0W0W*sBFaC&qVIZu`RjC1NJJSese)kHIH3D_f8rIRtWhRRgVBh7BqvKg*d zIJG>B>Fbu?f^s?D-sLqm%Qq}#u9;V7QY3dKBPv_si69pu-Ck)7W!qZ?fMq?GE$nR7 zPVh?TYZDaK!UQAP@8Tf$Go=k~?!F_dCMe&OEpnEm7fE8H(wLbMR|IgOOm(?tiLROo ztg_h9$PiV{hHd|{zlCmANz0WQmuF-=xq(J9+awGK*?hA$^j+_;@d6Je{J@xG(Ze;ZCDWv71%4tcf5s-%F_8ULyt|8uNyL-NoHO%UY0ojAi4c;jWit(OCqmWkT z7LllYdhC`Xp~0%F_sLfb4Dus_6T3EF3@BXezf(=pG#GZW!u%DnYj4Ps)tJ>lW|T+2 zkmV|0^FJ~21b?-Jl#f7vEyM)8{<0fK!J_xF4htZuwyP@|%YaBvY&+x?^K|N9F5o^% zb8r+{nR-2ajsR<=8C(Z&TJorQTBAmBi9`Id` zR=>2)zjSm;!0j9YZS@*~{#PwMxG0Z@EPO4x&}~cx4}MKo2a|OmWFAZIo0V>Zn8r9= zc-XFM*+5J&(S+BS!lqs3AwJ-Amn^hd&@bs!E&r-~M6#>f760X_-uxZ{sTTR^e6Ccn zN+E}%q7_mhQ_AXVmU+lrBw!Www9k?Oi!ct{6$J2JLgYGUsKzP~6&A5J8YtrjALt%{ zts}L<7zOr6(!5>cnZfq_W3+SJ0(v!`Gsi?A6o_Q?E^{`mLTataBUlPiE$1miVsoPm zj6xH6*0g!5&&Sg?Uh&R#pz3!tp1CC%iRm6Tdyo+jP@xcB(HaG@UB@gO@Z6=w4Y~9b z_^^ev?1$Yd>OJ$74K|}5{Mc}g|KwW(@I0*gR1;Z5w0iR>CRaH+FuxFH*I zz*y2Y?Fq&ohW|A1cN&Q+Z=60RWt}Gh#scmx@885P+vXw5+G%X!esRx2zqKB`8XUTi z6*W9i&3OZ^k386?J3O`~?`?D3`WuftT5^sz0oNteHHVuU3%>7T%Tz8AvMH2{cVeYC zFQm#e9T&pxw^Z;d4^-=jVdRfxO%k=>oZ4iR^&_OSR=X zhtn&rGLKSi^8mc&!&L=d#3dW6*IxaYrK9@9v!+^!j&cR7;ybkF?se1Xo3@tl3lRu4=pkZOY#+?MqVdCa7nW)9-5~!3kt&N-i{$`%S zw^z-{1Pq%q(o|j$*F?Td2Zs&G`^1UBgQpD0&cd*|X=#0vJjc1v8fl!C%OI?c5Lqlk(-gR>4_*z$P zbDpUhw&R*nMFuo)5E1}E>hARDncOTu#`K5V%)jYOXJ{!|1u_{J8<`F}FWf{dA@4m{ zY;m-d)^%5#0GLR94Z13AiUf+FdFITv+UTDb+M1dzNb}Nh8xUBiyUz0p4-KLn9btb; zz;9v-@>Qg+oAz}p{qa503?cU)JJojw;jrua7z5B&mZ*w1`@MP?%_3t+KXl?0ZW%wI z%Pv@|>^um8IN_^D-ZEHg`Pk$f1zzR~a&OPlZ#6uF&lrB85k*-ro=A0dfR>B4!*?0x z`7mASTj6>1$*J%XO5dE9O}jnpl7@0g;{cM%+V3a%VrIy0^_9LUi`USADqr6yFoq#;J3lc-incVxuI80k`)9$Y$dk z>97pak@(r%<|#U1hwEeVDO(IKqxDc5=#Nm{@3JrqBh}B&GxNPVXzp0D6tlJ=GGgNKkb%D&84>%txixg_|G(;xGqkw5D zM-3-(9YMV%#m!H}AY4#$f+_@|RA+R>NhBp%@k?7ws7@!B)(?G>=>m#(yJW-H|pr+0S_T zv&a-*AY1q)b$<^a_XEhCdPrWP@W@%X|4(;XsX+4%P<%{%jVyOsO}}ZT$y#Ov1gI#= z_&MVf`8jhFY;t4t0X&UA#b~3_Je^NIgZzH zjzC(~bOpCDdP-D+scN>@Ea_~Yt;*37eedAq3{T5<4f%C<5l;zrs~XlDGPhJRHzK;A zF*YsS6D>sa*_tzm`M{o#Rh6E$z&8N(L+96CKfGKE&!%syv(`vS4kFvR$H{0j<@z&Q z3hpz@Ea$_X>Z(IAV#TI5Hs!FvkCqEv`M+yCV~IR+Pp(*sLn(D{o5Sw;;|`ilBpxFf zYp?j!VR=RFcRR1@)5BXb%rd-?~z~~I4uvsACW93>% zIHgY?(h0B_Uu)^%TSQ34RJj0{i8%=)=yE(O$FzBJpd`y?TR(a#CJD(h=3G{xQLi1+ z#|n|O>+HI(o*zK<^oyl5MJu|=U(=TNEJ!<(aK-{O$<4#2mxAAd6VDfY0FYM@wVsBT z^N*v)Q>vm3?HSZVt>6YIwC#t=d!ix%ppE8q`}d@Y7O#11Gx#tsKIfkqYVo= z#l9!Bti2%9@?Ift-JY1VqoSJXI*}A5{DWXXynoU%jpP06}C+PNwWXOYQzP0V-QjfFjV}pBa&W08KBKSibD5=TjD``p${E@Vu>x6W5y$Zl^?J$o-3miy)E4b}}kq)!OMR zqI$XS0=m`EEEsY*(_O>(x|#;#M8tT^1)}&v;m&@bTB$y z^e5Zjv8(u(v|kZl1nFjE<|lv8Ff^C*`1UFP#%}W0y9>YkQfNUN_q`FSCf_)z_z>!l zYg0Jx)hm6Ts+xDI*-s)c@Lh0GcO=7MqMtLw5p}z-*8OMt(r_na*~1mJu6ZCb4O>~| zI?{HsU3x(Du5a(|rBC;};dxR(&c9?CzqHXN{?s_d{kuCFN$0vz=6X2<2dxC3hi{l~;pX2ERF*k|^?IR`xbeGsw8({iSqCcn=;1|34+vBrf#Y}${@gAUA zeI7XLtw1|~6_j=%H=>4*2)(6xX|wqJHX!pIR};z$GHt&oE$6m#Pf|!#O@5Yc4l0}@ z$=eog-&@RSe6miqQP&+NF?FpExmTT?>L&8ZrMPFSrFF z)Vqh{99|5Qh0I%i4PM~>X&XrC-f>hdl|gRQoFo#b&`=nWCa?;&DUFA?cbxsg(^JlN zw^P>Qw)folPV~D1dGoXgvEkC=L`*EWOJgN*H@jg5!bnHyk>1Tfuw**MW&NQ|?ad~H zEG5uP=D^&bcvDfpNj^F@o1LBQYluKc*EHA)LV8C0gPh_`m0s?=vG9U zSE!!L=So-IzF){_dZyNv<(g>^lN33P{8$=I#X`^1b)#W_vtd66!V~S~r}&OFYH%4a zAP4G-*-&F|^RTlci_ehg95s=)t7gN@YeYN~><(Y4&M00RseXM?Iu_Y@oa2B9%Qemv z_4=3fvEP=w5h*pC;-gE(!6>Z;z=UEn3himpr8Vr>uJ9XB(zsH8eOGB6tw~_{bQY>%C&VyV z*s44_5Bm0rVsd}N{0K)qXSY6t{x^MpPL{)0PtHoe@4@lzo<0ZF>jJnx57QbRBiYs- zn60x#Z8`S2tW1;a6E9QwB-(95@6~~xHHqLS zs;gNP0!{qh+V3Z!U-v@0J8fzTk<#DiD}>)L$W1(Om_aLgwxjr9qfo4$E$!`;Hbe|> z`R8!_CCzW=QS)v20_QyWF!Ddcs`U3e{sy`;APIRUtt1~MSb?1IqeL_GJt&G1Ub?Yx z$zHv#&*(QI=@cB1_=L5zPyWme^V(4@tvIB8nHLVSQn+k!@ZoG#`r@~ zje&PIAaS$$u7WDo>~jHB(#7d)=X4p^|A#g|L(>JVldWN$b`@H6Tlo&`#hRL6L4H zFh#0N8RQciDKK8BNEa6Pgct-JviD#ul!)6kNq>0pwb}Zmo416pK5^%-NS&UN<|ZR# zm|@#T91Kku9B($8Fh(U+$69RBD6!M5VQLt+9?Nu7Az%sYz_CP zPj{b$!G(Jv;b#Zp!?PXicpRIyX++Jo)0=r02Ps+iN&TiJ_n*|C-Foqx3@>lo+tK#1 z7<6jGFe49b6+SY}ganaf(YOSndW)rvQK+K@_)SF^^SMvImNz4j8LZcY@NX}E&c^;9 zM5ScqK+(a<+p(;XN3>0OL%?LT1dakML9_B@;f$aGQlFh-Hc@h~FR9^wVHNAxovnje zyy%xIFl;}4tzw%USpD!UWCCLlI5zY^7ZJH6 zhvAISEzH;I0o*96%e!1uQ804muyIIHDrD8yG@Cpf387*2>v6hV$-66?5OG;(pB|Me zGU#0_;QN(hw0ZSSem-9hUQ>PCU3?x4cK2R`hS9WZPQXPTv>*>?Y1{)ZX@p5rOh{@b7Eq8SOyN&yWxJ_IwC`IL&LkwXwtigf zN7{WiSn(WV)!H8iIA;*m37tFbcp`_nzCpNH(X_w z1S0KUt#?T$j1MBE{Aqa?N;;aA`@i?HojG{O!zM3I^>0cna*3c8O96nFCPHcKo~L*uP^P(kcJw5qD_M)1Y~=RU)mcIFM-?(W zv$0sni?^c9MYfrz)JpxI=M!0{XigXLs&}?Vl9Ek&j1on@msU4LR_hDyvV{J}PdbP- zXr?Y3j)4dZDs|!c09l-t!;G90KMLVjm7LH9ng|Vb-#LP=MukonjQJzY=Db+z)#eDO zKLjDco|1H}!qK&x!^wEAod*|0AC0p?`#csNjh8~sY-^6|^NK2|K6EO=*EVnZSHj9B z0yes8;Z{8VB|AozY!)e*QkNMNP|C82(hs3jxtT6O;_ z<6t(!^l6@p_xbi`l((qgki*rjCLd}cO2rLbV?XyMlNjH9rJXiA6}qq)?p*M=s|=BZ zM=LP&c^wh?`~>!VonHIBz32W21qO~RhGvoq>wW4TRk+K1%5{S|=U3Z<08 z#%@Bg#IVMg)+lMPFd9~b=UB2DI!erzO1w3ciGONYZ=|7cbv`>9`EWW#H_~s3Tq~H> zrIjd{;((B7Ez`&4YytWu*wOd;d-I=w>xS#eC#k#kNcl<9Z{2c_*em3_MQEqtdo{w#Q7(}w%8g{-$X)Vws96q~|DdtA&IuXpvBoa7oo(VNh~fV7n$zmx98HRa$-PR+=bbz$<@ zX1q^AF4^ac+~ehw&l=Az@CnvvNHroai!Bn;KWh9ut>Bv8vr9&?rwvg#uTlK!uH}VD zW31g8i9H7#DBW0cUcqQaxAejWpuT#0DhGw|ngQSH4wkG= z3HS=NKjtq(xq+i4L7X^~t2;Y3=4l}}ri8igqD3rnKg>2^5{{iatX>9lN`xLr9LgJU z{)saY{s5{(%y7@MlnTqv=u#+Qe8$VkCHBX&;XS5KIcLP?Ou=bzq ze`~TR-9H(!utT%dQh&aN1kN>lsoam#%iNKP7a&7An`D{nd7%YY{5Y|LoOGNX;u}Pu zXP3bHlvUf3fi&@KE?0l*AoI({sJmvbXzNDH)M@$f%DL7RAqGdeO+BBwzKCA3W6oEd zkx$08C?$t$8?T?x`8ta<+a9RUK^uC__5C=Y6n54llnEHDmkx$?R6HO;GZ-Y z&xuXeBI;eKhh=4-X`aR}$UH3hPRBP9vk|0_7D@-~DHNAZD77f8Vkd7pg&=o>t}x{( z3yp~#tAi&cQYm{+zb28)HfEV#ZGje(#X0PySyGb!+V*48G5_K_WFV9}fEnF?CFk3} z2>IZj_ed#4k|APxrf6R^Q)C~tEX_P6VcV=@R;oY>nd4@vI!jc9QpIiRNQnMRJlKi= ziWt(~@DgztOSMq7k|Hy^lc7Z1cRGeZVBVO+LK{vebu<0U^1&8G(hR{DQHn2RQELDy z@DXcXaz-gTZDE-hlqTKr^PVBXz@SKC?Atb&MJn29-I%mF6`o?&R;bnEG~{S+8`FeR zLcu=yYbdt*dBM;*VpQfpF}I2HSI|1+GtXSA`Y9?bu~23%f?-#tL2l1ZXd@v&F=m@8 zF+Vi(Quw$aQ0U85JyStN0pE$*Z&VZMyNG5qvSLa}-KC_U+zCTDyD;s{J6PF_wt&kB zbP+cKIHkhjl2TBnge}ebYHB;m78L^44x6qU88eI_vH84%^jFqowrEErA+(Q(+wSC! z*Ye9nyfD8mp?HNEw>J1dKj=2_p$$1o)J_LK)_mLu>4G0OY2~kxZ5}tsg}D843>b%O z7oyCdDG|%G{SmTF)kLHJqgS8FSHKLt&Qgctbp^F&U%|8&k4wSlQ8~U2TyQuPHTTVf zX!@9zn^eI>9FvL$I0WLyo$Qyck2HICY26IV6`RrdT)AES@*>5*xv>M*kU2-Yf4(+Y zkaFXHEl^b>`p|H{wsOaHACjZ6a> zfG}Sprr1d`3;(ark1Qq)!iLWT8V1WR4Ppp}g6^N@f0eqUDEd8(^1svmjrd`(r9TEB zv@NCin{=)4=9L%ZEt;!0fNx{(T+%z!N~(yAx@N&2bJuE!%Sa|fpN>-|MEan?kQkYl zmY~7;s3D>_cnmW$BMTtyvcZhD9;79;?A;cgMBWujHL-2hF%^u7g!8^M@@;jKN|kze zwrmPA{e}y*8kA&4A@UG9+gGPH915Jm8Z~8m2M?)AaJ3<4h>IP+iA%TSyWRtXzX$KQ z%GX!9d7k5TLXSE8kKGWH>ve${lpV#X!EXsl|IgawG<|hniU%j{4&^m7%2Yv4YSG5*ve&RmFhFh z+|fno>3^!r26S2InOCJBhMBsgxshOp$x-6`>=KQ5i8*W%2N90$$oP23KTrH-OWJk06Jt=9)9-kl8^P?TqmZ7U`07_4GuEQXG@0RSDv-aN8r zUA?QF4Xd-30L6J39OOym4DgsF4U;Yb@CttwbfZP?ieBV$1m$$p3VK#;5Iqp0yfHxF zFL0HiC3{e5xXL>f69P2bjh~lPOO-!YZQIO@p}bTVoBXPNPai7EMJePS7sZ4cq06KX zJugu^B~}UCoirD@)tHz8_tT&#^4Z4PEAlszs?D=P$?9d-#^h_BeA0DZ|KU3SwmAmJ z1EdE*BTGvo?VqB?lGR|JKWC8YUkr)?F__p&$tVe=?HPN2fmcM;?Sd(LXF2O9dr(|L zRx;O)QQ^`1O9ZIcGp6GUB!dF}TQq-iz^_8PRuBqE)!yDBOGIdN0_l+KkU&@@3XU|g zBi}p)XKYGjhZc?>8dXU-z*v_Y;5(Ui_VQXIRaalk6$$L2K%IE5_5lfO?zcSY6Ae(k zL{7|$iCi>yDv{bn%kwv3yR`{Sy{_I7=QQM{{#be+jNh4Ienj-=MG%`OoDRQF_LN@D z%89D@o-X5?gu-Szja z6+k~;o~l7aD2cKRO@=&6tsC7Xn=%F=<33S%{`ez60`LSJ`XDiMb8eImaRP|~;SE0C5C9N2y+U7F!?j)hfxCFuv zzdS?9xx{%LLKFjMjQP!GA}QWQb)p@YVmTrNdd=qF6E6Tc!k?BEn?S#1~!qk2( zWPj-5pmzI+PmPxlpO>`cc~c+d)iRTahw6IgNtcu;Fs@p1mYa#cDq&ctVB*;7oCOS+ zz7BVATdZ}dir~GmYtGV&_v^IXMKW$n^sk3#y%}+DT(ug_4^6T+BerjfknxG^z1HDz zg`L0%J5%RiuU5_lk+q1()oGy?pZ_!J@-PVy^r6>z-+2`xs6xd;nNx5 z+AB+M7veDD5pTDx_IPd`7}0Jm3thk>bPRS<@$t(llf6w;U`V$|GxQbH>)o=U+tD0@ zdGU*89}LstfD!KdYth1D;#qFTJp>_fHghs+V5nj*ZknT=lF0Joa}cPvm5nTYBSyo_ zI~z!j0tN#bG($YlchmbZMLrd;gntpiQd~E;{~W$`b9#O2)K=ht%rQ38;*zxSq9XzX zWh6YW?n`17pA#Dw*V{6dP(FC@BJpyVdel~`krG!SHpxA?Y-bB@YTAj(EZ2HN0u&+K zEyrR7anOk57o4t>Z8qosrW;-oQOb_}nuqoNH>XO_cXw(KwB}}yKUb>abJs!k8KOtU z!6l>d7FL25{!;=dN#@NyICfcbl7$5TQM>XE!7w@s;@*;Pnn2UXlkhIn< zpLyp|TTCFmh1`Mq;I+gNaFmEoTNjM)Uq6_#HfK#rNTU$ws9 zwMYx#cnZPh=|fMHJ$NqaCrt2{(_{kihoi1LEG1p5t?bPLO#^p~u_=2qoS@|voGpGd z?r~4d5(dJ9bxbeyD}7p?kD6KaR+HLkyH7o;<92TcmF7P26&@#=(Z#2Wr$GZ#FuTn- zaG!^cX=TNpW;2`yWkUGC+bJ#<80r6M6N$Q$Lh#b@wxHp#|EGC4R(P0)dnh4|OBlxV z`{LjPeDeFL^eYX~Wex>w!oH&q9B)K*I-ChRRsx{T3pJ!&-hI;lM!-H$=l%`z3 z69UDk?SMA}yAl5RMs&zq zWI3nVL>5ac)!}opO;7DYjOr(Wci!OWXT8~p0p`dkQw^%*a^~5@kd&+pYYW+;9Tz#= zJ8&2I)3Gzjyc6~NZk->rvGk&^mC;8 z(f^c*SMTzLH)gzrGS>{%E&EVJ3+Xm&)K0nF+NhfpzR314qq>{@gs%vNFS&`*=U!6* z4WkTq^{?doX@&Beec@u**(7KRMAyaoXb8eZ9<>!j+rCX(9vaRc{x9UbeXlT=&+d&w1GTdJF zYVL`!Vd((?nCEx@Wb znr|_gSzi#DRVwdx+*R_Cc<`%WM*{V&P5yo&X6MdY?t=OPakrj-h1(S}-hAu<6auWu z=|7)g1;J8FjQZN!83ka^%bQUvD{89+Tl9#Gb@FszmsIQp%=ocKAZx4tz;l8Bad9z# z<7`FJ2Cj>!t4&Y0N5t{(AV;Y(YM$ctnHT#yw3$PXXy%PMM-02*RQ>(F1D&7Pf6XE3 zPY_^4D}t#HcDU@bZLtj+x%dh(1;LQjZl~*5kxMf*D2}$||u9qUF&`y8I*b z2!s}gs;V3(GTCW@!`!8s*W)&nX1aK`lm`}MKkNk?7)XH zoo{l%JX1)zSm?}*7O&xNTtu|fat12%0SxkZiZ4S#E!M>A@#hIAb+J*zRs6e)Lc7OP zc1tnHd>{diRwG^C@gVo1rgfrMGSm6*k3npBfRjup$yz2q?>zA1Z9Sa=jpa9d8j!w9 zxsZYTH^FG@2T}Y$pW4#6ODcBWGWjxOL;=AVWJPu z8JmaLMkFVmnpFqwUw7?{2It2!=LA;WZ-#kOyTDlFnSP%KyHSW5MI&WDHy*ao<4&8wF1lpj+w$m4(P|NoHN1- z$|8l?R8x@zEEiD`O?K@(ee{42Z?>t743xgZ< zy3P)~i?sVjZgy_fZbR`L+eM@F2qo7U$QeQN&mFP}E-d|uNgtom^I6@w`Ol2o80HtXR&&<(v(FB<%dEpOh=q7_FWI-Wu zyU7r;I{Y%9@<)-XiK4HiB0$OgzgyR)J>QV&pp0yNM}V^Wp2Y^u8t4hIM=AXhImZmg==|+nSAieoC~WWfrv=q&)F=fjw~8JWG?N`B+LZWyW-ES1 ze&2!cvXN9SbjA>?MFy$ClP>+Sh-1lNC|TFID_Nf1=TW})M^F2=d^4^yO)5@yn-%qg zr8aHWmGEq%G73~F@&Y^RBd&6F$X2Cm8#~>avT#0fo;`)E5uX2wz(vl0EvK+Fx;~8y zw?SOhLJjqds^=`v%977AkK0!5`^V!QQg?DDGTh(f_M2&6LXr1Q1VKlowf!Al7*4(R%*9S}e_33n!ZGM(3AV){Hf@{`Ss zL*|s;yC1JKnFAi0yoa!0^XTEIVXGR$jMy;ft4`kI?m6GX^@0*GIKSfYeO7-|4Mso5 zKkEVmp{Jp6^-y{!Ox6WaTHW{}Ow*me8XNIt< z9Q|eSbi>zX?JnsVivy zMG6#`LUDJui(cHJMT&ciyX(a%6nA$ma)FDxTXC1-?(S}neqZwPUh@BEliia!n?0M| z?3|h3%+8D;Sp~m6ODu#TeM9a0U^Ld3;&@w3!(<5|@teW?+Os;+&Yqj38lqF= zJo{{-iS`86i;o4cxX_KZi&we&w#=}=9sK0ixL07YICZeNp0|!&EokBwz zP=%c1i$CFu-ixp~ZZ1=Dn(MkJX+AuWl#G%}zvF@rl?F9BDEes~;arFNJ{B+^sD$tD z<5UtM8fO8+Kh&vfz#e_Db)#q}|M4CuAMfpzyMobrHgOh_{SH8nc?lL2SgexYg4U?$ z9nZLrgPB6%G@k&-A3Z@LUwPiTgma$AQFXYE*iU`4HYVP09j~%w-ps#Z; zS%M^UFG*B)BvCj!?XVi=tyjt7rg*c#OAQ$pU+OtpJ=1A)&>z9@_AfSCnSFBNP~Fr} z8_ZiC>tyyAv!E!oOz9LgMYJ7GQXG4Ku3vV(MU>8m?}PfZm5`nEA}q-poO zVh3&*ww)$QUwlEgXDl=vL$g2b0ked*#$C5H&2&`bRCL9o>Y+6da%ON(@M?q5#e1h4}*-vhhMc1w`5OikRXfpKr!=RAbG!Hj&fN7~IR+wRG+ zbp7>ZPs$16%N*{cTmp(7Sa9hXZJuqr09QBETLRB6T;D7v5zp6YHyQ?U3au9aweFV% zTV-J5WBZmzzZH#u{w^OPcZM?afms`UG6k7aO6g2lz^1KsZdE_~{U|9|;&!>mQNj&O z`#mt3cBhJ`!1{J|>NECPB=aqipoxa=X2ePU&lU2U$@`7a7;K`pB1Xd;L*JS* z(i^or-=FB=9h46*-%&^oq&+@Fwe9=;K0SvL{Ooa@_f0oO&>-wsP*wBCL#qiL4nwtrld!EUR( z{VW{?LtxZ=4St6OWf@Vs1|0|l+`0EL!`cv8OLpcA=O~yv@Lb^McA=bKf2ufxwz^FJ zabFXfkbk__$LLVjm_g_OvNy2PD-?>U+KSg!!IUQTOEHi@X5A>iwt=O z#sv~876BDN#yn<%ELW68HsE;KVIMyIMGDPY`?nS&MlxnqqDm%9S!yw1_Fw!B6kyM?ob?*qv^fBbAjpl zFa6)e5UMdjnHr*}aT6f24bmnXQyE>t&X|)Gm;ZoXHL3}LQig_x(*37^oy)MGMZ@Z0 z?$GhYHb~lfU-LAE0^ds4GVDRE^#L~3bMq7ZDcCOc<0jh@n?i_rt0`5l`nlXTNmzEZ zz(OdgqbnVVB!+INw?Y(a&pI}1EeLZ<_M*r#yBuY0C>}^)mK8&DW_avf2T&NsF@r4o8Zqt9NdrWs`GDMI5|E!m_4&kNN=zA z!@&OguOZjr7zezzVmYjuIi=OlZ67ux=--C1Fm!J=j87|`HPIku^goRWFfHd|(l#bq zupxz0Ejc0?KVkfTVos2mMnQc;{Dw`tdt61<~<1@tCjc5xgop-y z?jb^bKE>tzFg21cM3}k0oedsZEfJm^&oP*j%^M1v#C>Azt0wZ1Y9ICKjbQ-w)6ifV zW7rx$4qp<(&tq2lU1x+Fqw04zUWP#fT zBq>`qf0SRrRPd-tNK&Y9WIwQvu1on#>I>QnN{uIp%S5lR@wUCCyx~wBLa`1HnIoPa z1798L5J6Z^%`k1jTQzI;!MNiB4o;=99tAwo;c%b@IpPXE@D(l{XpzTE`BKQ; z)!Q3&NBeEpYpBl3Tgx0YarxVzq*h>t=T}_t@-AnkUc5ljk{%`AXwzu#jbvh+w?y0G z{#!5?3AtH#QX~E7biWlEb}5W9uCki~nDJ*HndKfv9R>gcF#H+t?Bj-I&u%ZTzQiV- zL=`w{cybRe{?=8Tj18hVja!auB#S*@kd;z`JO!e%svA!1MN$9q>a_8t4Rn`5Mp$QN zl*B~Zn&<8M>`D2CHzLUDG=~(*R9~$w;R7!5s+?0@T`2w2WGD$)fYPzYJ8^ZqEWyXJ z@n1m}3eED6Scr)Spbg*#cZWPcp^TS{afXcoAvXI>nUq3-!kobJ`x?5vL^7=K^@Z5l z6A}Ua!%{#=S@XKjdL!Uk*@Z!USin3P?b)<9U#`Y~I(E7{^6Sh~VLZW9ZivAAJr8B6 z+5qk3S7rQfGgb|Gn8Z_2$q#fKR|j5yqVcV_dU=LO8wMF#Q5VKbYWRJ#+G{7^UAH=0 zHe$HYdp@F2(w4?0%VFJVKystRiL>>I5kA1cD8j%5gv4RveMHHaHpwRY{JE+UgFTtS zFpe3~4aGlpSYJy<0?A zDB#Ednr!opGt5d5K${Ex&sp4k`U#6Kksg`aV@UI(M3W&6YcznJW9Cb)q4Vt>8x~4d zP`CQfeA(+AXzbx6^N)aFqXw;S5a7uM?=7J3$cxh+I)(d37P7BFXXU^XHJVkwN{vd} zqO+z(x2ycJRPz;VXmp-6DB9l z4u-SopAiBw)f(i!q*Bqn^GLLezBv;J=as-=@^indH0euQDJZ4k`f1Bwr)4bXFzX81|lOgbno+A8Kj4cKmXX)^o*{Thbw|!#@Wp<9u#D{R% ztSH9*`P$X2&KV@LTYRFCKD@e^Y|dpnqR`G|U^*pB_aW`~r|F5Rpssvn_WUyt3T0?U zZ8e39MiA8SYmevSWu}5~yjW*x`7FLxMZELTH-57pyECthX{nl~lZvdI|HXS}x*o+% z2GEahY8!?;?=cKs{sZ>5Fa_54#06`FYF3#L6ecLB*P@2lo{ zNDj=;OEg(fSrMI9z@-QXp}+N>`@@F+IZbm~=a$jhv@>C|?w5&_!Wr#~{=?7$$7y3? zvkli8m{5`dqg%-%@w~h5tN4q@w?|v{nB zzm&HdB%F4xaCiA?AQ=&cZQmz>urBQ#ll2jCg}h5#79xlG9Y9g z@>E1Vx2(G__pq`zjrFAeEDN7pkk0Q>%vD%h!X!xEqKEVoy;=HD`ETFVRK-7+m!L~i z^nUw+*AQFw#3+P$DFJWAlX|K8NAs_Ksr|AU zc2mZ<@K?^`2#VdHM}68s{xl1%p?2Rw3x&1u>tFwcnWo-hrs>uH2WFb4^d-h;6&GU_k;Wuo5~ogX$7a@$T@0qL)5C5_dq^ZK-h_CaN52UL#~cM|mhv~*k< zLJ&iR%DwTjj^v@TgyixU*#!PC+vbfEU5b$PCRdZqUbEU>P==v4sp?{0J{^e8X7VCm z#%AEr&4Fha2x-}QR_{l5SAL!PxUkXK{E{FPiKO4LmqVwB!L;({-I#tT5x`9nUlrpZ?D|RSHJ#2hjLEQ%9|DCGcF)>qE9iDiv{w0VA-e+`~iH%pNQ z6_B{!hb$6xfn;$PH|fORxTpXXZM&n>nAR9k+#ZH6Vw4qykBm2&n>XeS29CBmFxI8H ztW3oiG5fC%`M)s+u%!OtSC7TG1eMi%8RQpFVmp7t|IfzQ3J(9V`u8Qxs{Qv~No>`D zX5We52@mLlzC-k64|W&9&QNfEoA)YEN<nCGGln7FskzW?9G4Pd#hzfzha&XH55HT;|L`|^{r zeYr16xtZkWXx`TyePFeKHj5q~gI$;L_TyZ+L0v*3-RZg|pr z7+;*b%m^ek%^M8H*%9pU?Yce4rV!h!yB~fPyF^*%^%pUq5t@)#B+ z4)bX|cb%ZHjZ1XkjanIq-ToLQ*u9QHVRk1ir zbE@(()AKoXy~lrm8df*3vq-#0h@8tmII$1#0re=wl_%BiAf$LLWF3^0I!-46UM`)E z3)E$yDFaD=wCPi`10S~EjA^fy=s=pQjwUTsWBYUYWfsBO{k*=!Qpf0DwEw`4yzJcB zB>{|H6Vz)oy3;lx*E5PKFP)7y-yD^XqYwX`;2gx-Vm9@fnjOe96p?EWqtn?t#>kXwD@Pvufa&g;U1pDsM1k67F~3 z??xb3EYh?`ia3e-n4IXab9`gunC)ooeTJ1$egPraK75dCzS>XDL`}eJNmha4#J#i@ zHJD2R8r^9ajO0H5(cbs(>?!>?r23j;XnQS;C8Woiv-(rY3hT)6s{DxPamfxRLYN*T zYPoR(zN$1tPtf-GFacl4Df|y@&O_X!K*7>Bi8&eoA&nGy6eP3-Nc~7{=cdQLoNN{? zWWf4>Z(iNcyEc(QXw{Do>~_gW87qxik-6aPF?y+KZ6mgw1VlK)|CyXG1WY|qgcU~! z>H~KY6>=m}oa7uTv#v=xz$(4b#yS^KlPa*r>dV`$R3QO8HZ4~Yvh6Dtjq#QVBPJZl zHMl|IWYzf20n*$7m&gOAmGNlR`26D=)%eF@>M_<0y*a(|+a-Q1aC(O$F5tHBu0fyn zprgcL6!W@yOhgc8^q~c8T1iOSImtTpjcVPOSE(i=2zwjd1p!~> z^i7NEm7nM$!0EgvT&Ubs)c(AdP4#mp>J?|)h#8l^zrGcX9oZMAwN%zg*@j`n{2;>m zU5kyOpot+t9(i>Je_|6G)CoGkk8gvK)O!&5)r`f>NDZWg z+#gT!yA(;x`Si6oHe^(4qk$U;c`_235&9*9p*LlD_G&}qKj${>38>w)Ka415Zak*s zG!w2{2BoR8PTD-T4-Hh+x5w*kme=yGZPIfceH)Ot_WbxWX7QT_wZ5gAo*id#F^X?S z6gAbjY~=z6_1_qA!91#6HlvW4Uo|G+Ime7&;JM7w0`QzYO}NS#um@M?uy43?k~HuR zv0(1ko9w?T^t~D%Eby_eM<)eM5Kbr!t5xJX5DIk0%^n%$NS4k;cVHIJ{ed!4dYN}^ zxr2y)uFz*4&aNE>_AnW>{XP9$t*=6sc=1Ln&44?sT~P}Cxn7@CD5?HcSA@FoZBBdF zV=cQ+OhdRv=kQj=S?!XhetCA>>vQWPUkLTM8{b}|Yq)T5R6Mi`3judP zlL(70|MR@>P!KstR96irp0d{xQ#$TTou++V=PXBZ>3e#c)22@7(w+i0Ow#V$E|>CQ zxbhrq-pC-!+QBR5qMjt2`; z5?AJVK{4WfV0_m9EzMXKBT#fwEudXE-2$?oj@;~$=Lv2;=rgrUe=wApcP9(S9){DZ zJ^9TMLx!6OvVj2)eB$Y~E}NQF^VB|ffcVRH?`!1Ny)GIq+M3Z;M*C5TDEl^z!N;kJ zvRi^G9G7!~n%|FmdF~i8@0K)8_DEnVO=yCEZ;Wmdk=cgKkR)NEFAcER5+x^N*=9B=fS+Hay;H+0&- zzvtgq*|fwo`d1$V6yM(Q%~eO(=r*Vwvw37lF&%6FWLfDALnz4cN|@2=f#b7P*)nj9 zq3_{{&Cr+kmO$IALS=juAy>|%jb&Qa0j3+#oF!d%P9%rIb#BWOuSQ%98?pJPD1NW! zVeeDMay;86g1QoWvAa}*jECZ=3t`a!|-;}6xc9?c7oM%-#5am*~zZxm%Ayf}5^;49&e zcD5)h1%n;)JbEmTyJx9(5Bk!Bk!#ewPeM4{>;5r%F52<70}2j5sMw~Chh)6c8~J1e zu9Ke~vi#mkjOxULgjE93(%KEeuiQF(@;5feP*)WqWtvGep2^+MpYxiJE&eutG4Rzv z%ZGXt+U_)A&TX=qj1zxkJW@jvKkNgzt|U7~P@*ea=HVT=y_@*!zq3hkUKSnQIlbh>IiSkgn;Pi|!r7f)rG_cW`gc^AOmDMsy4a+;LlxS2F|L zyi*(`ho88+BG>!A@x6QWJZ1kh&0s0H;j;e0=(|v5XT);ezP46^$tLLa#xraHQFU5E zZS5&5c%^U@UtwLczV$a1B?zl({PpFmYCYL4Nkz1c?nYZje@!WiZ1>(#$rw~^!ep^D^^DJS`W?>uJ76INZluU}2jB4MvTjUy&py!@@ue z`5?i9K@n^~5N!@!3_TNNU)b+Yg7By!JW;wc-fa~))Z*lB1^X2hgKr{VQ6@U8<@0YX zO3f5{C|9U;+;il6F6<6?C^WPKk&J*s#KP;OLI4_FH?62=jB9_QYbM)yw&C9|jPT2h z(IVc^-x|X_V#uWGj$3ZMETOrx5viV1zw@>@;QSu1B;}Ci%$zANZ#i0<5=1vVr;tNM zAYjjDYbR`f+=Mb4qAgynD#SkDJNRPteg4F0(WdC`)+qYG6PZ9hFq%iQFtGnsPoex# zM=!4yn^5^0HIFYm&v-qEYt8&CCR zYw-)1qsSBE^MTbyk2{mSzv;P_bp7F$xD^MU1Z=yohEVK%l4&Q=mCftl;G(> z+^_UBlI^ln=XZyFVt=pCK<%D5zasGdd@l!fTv;u1crdfrn+P|&O1nC%-Zu=BE)6Hy zm}9?RKqoj0%Yf|jTv#So2AubkzC2D6zVaB*zDOM(xjc-2&zy}Q+3+E!6KYbckfwQj zsr1oe+O^s2_HI*Btm7%XXk`~`Xs#j9PIbV+l0kGpcvk?NU8!j4ryh;Ca4>B~)EBDm|Rbmh-I#ODm{m%|)<3%1auD1CY7W>%b1Q zwn3Vmj<>d9%>oU-sC*R$`jJ9m|5KbUvG$(fq82f4HoLvHW4L3u+ZxDi09*dGO_~;4 z^QwMRz3SUY?g>+e)!j5?5Hg5|ow&9|AFKtOg^Rnz9}%-6Om#CHWR>x0e711Fmoy5{ zZevJ)Mu$&Xl~!d7`CHjqM$H}CqXSfvpDantMC0NPJLd|N`c{0j)Xhf`9lCeL^G6o%(Ad?5L}3yJ8T%p=JMDsHC>t9b&f4?3exd zX85r|H@`LCf@K&8@_Xu}I&VW?>Vz-rPqceP_oo^tG7NyXY@`PuU+}W46gZOh;@DwjZa_b8G`jeniSM~se=ec~Di*1J&^BR4fIc+5`?H*|CRjNvRF?D# z>inB!#?1|6Zy-EVXZVRn()_=#N)PjgDRG3FSX-$Saww__g z3yfZe{g-v5t9<5K0NY<3BluJEr--T*R9?u``>YcMHReYQMQ>VET zzx;GBQH^g*W1skysjh-ogZ;6xU!9eUo-GXJ=YsF3b>!IcTIy{h2dgRYM`hvq5cn>OV`-| literal 0 HcmV?d00001 diff --git a/packages/core/package.json b/packages/core/package.json index 7756c08d7e04..86f97f7d5099 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -77,7 +77,7 @@ "@ai-sdk/google": "3.0.73", "@ai-sdk/google-vertex": "4.0.128", "@ai-sdk/groq": "3.0.31", - "@ai-sdk/mistral": "3.0.27", + "@ai-sdk/mistral": "3.0.51", "@ai-sdk/openai": "3.0.84", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/perplexity": "3.0.26", diff --git a/packages/core/test/provider-mistral.test.ts b/packages/core/test/provider-mistral.test.ts new file mode 100644 index 000000000000..6e3176695f67 --- /dev/null +++ b/packages/core/test/provider-mistral.test.ts @@ -0,0 +1,282 @@ +import { createMistral } from "@ai-sdk/mistral" +import { expect, test } from "bun:test" + +test("Mistral sends promptCacheKey as prompt_cache_key", async () => { + let body: Record | undefined + const mockFetch = Object.assign( + async (_input: Parameters[0], init?: RequestInit) => { + body = JSON.parse(String(init?.body)) + return Response.json({ + id: "response-1", + created: 0, + model: "mistral-large-latest", + object: "chat.completion", + choices: [{ index: 0, message: { role: "assistant", content: "Hello" }, finish_reason: "stop" }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }) + }, + { preconnect: fetch.preconnect }, + ) + const model = createMistral({ apiKey: "test", fetch: mockFetch })("mistral-large-latest") + + await model.doGenerate({ + prompt: [{ role: "user", content: [{ type: "text", text: "Hello" }] }], + providerOptions: { mistral: { promptCacheKey: "session-123" } }, + }) + + expect(body?.prompt_cache_key).toBe("session-123") +}) + +test("Mistral round-trips native reasoning in assistant history", async () => { + let body: { messages?: unknown[] } | undefined + const mockFetch = Object.assign( + async (_input: Parameters[0], init?: RequestInit) => { + body = JSON.parse(String(init?.body)) + return Response.json({ + id: "response-1", + created: 0, + model: "mistral-small-latest", + object: "chat.completion", + choices: [ + { + index: 0, + message: { + role: "assistant", + content: [ + { + type: "thinking", + thinking: [ + { type: "text", text: "The user is greeting me." }, + { + type: "tool_reference", + tool: "web_search", + title: "Example result", + url: "https://example.com/tool", + favicon: "https://example.com/favicon.ico", + description: "Example description", + }, + { type: "reference", reference_ids: [1, "source-2"] }, + ], + closed: true, + signature: "sig-123", + }, + { type: "text", text: "Hi" }, + ], + }, + finish_reason: "stop", + }, + ], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }) + }, + { preconnect: fetch.preconnect }, + ) + const model = createMistral({ apiKey: "test", fetch: mockFetch })("mistral-small-latest") + + const first = await model.doGenerate({ + prompt: [{ role: "user", content: [{ type: "text", text: "Hello" }] }], + }) + const reasoning = first.content.find((part) => part.type === "reasoning") + const text = first.content.find((part) => part.type === "text") + if (!reasoning || !text) throw new Error("expected reasoning and text") + + await model.doGenerate({ + prompt: [ + { role: "user", content: [{ type: "text", text: "Hello" }] }, + { + role: "assistant", + content: [{ ...reasoning, providerOptions: reasoning.providerMetadata }, text], + }, + { role: "user", content: [{ type: "text", text: "Hello again" }] }, + ], + }) + + expect(body?.messages?.[1]).toEqual({ + role: "assistant", + content: [ + { + type: "thinking", + thinking: [ + { type: "text", text: "The user is greeting me." }, + { + type: "tool_reference", + tool: "web_search", + title: "Example result", + url: "https://example.com/tool", + favicon: "https://example.com/favicon.ico", + description: "Example description", + }, + { type: "reference", reference_ids: [1, "source-2"] }, + ], + closed: true, + signature: "sig-123", + }, + { type: "text", text: "Hi" }, + ], + }) + + await model.doGenerate({ + prompt: [ + { role: "user", content: [{ type: "text", text: "Hello" }] }, + { + role: "assistant", + content: [ + { type: "reasoning", text: "thinking" }, + { type: "text", text: "Hi" }, + ], + }, + { role: "user", content: [{ type: "text", text: "Hello again" }] }, + ], + }) + expect(body?.messages?.[1]).toEqual({ role: "assistant", content: "thinkingHi" }) +}) + +test("Mistral preserves native reasoning metadata while streaming", async () => { + const chunks = [ + { + id: "response-1", + created: 0, + model: "mistral-small-latest", + choices: [ + { + index: 0, + delta: { + role: "assistant", + content: [ + { + type: "thinking", + thinking: [ + { type: "text", text: "thinking" }, + { + type: "tool_reference", + tool: "web_search", + title: "Example result", + url: "https://example.com/tool", + favicon: "https://example.com/favicon.ico", + description: "Example description", + }, + ], + }, + ], + }, + }, + ], + }, + { + id: "response-1", + created: 0, + model: "mistral-small-latest", + choices: [ + { + index: 0, + delta: { + content: [ + { + type: "thinking", + thinking: [{ type: "reference", reference_ids: [1, "source-2"] }], + closed: true, + signature: "sig-123", + }, + ], + }, + }, + ], + }, + { + id: "response-1", + created: 0, + model: "mistral-small-latest", + choices: [{ index: 0, delta: { content: [{ type: "text", text: "answer" }] } }], + }, + { + id: "response-1", + created: 0, + model: "mistral-small-latest", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }, + ] + const mockFetch = Object.assign( + async () => + new Response(chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join(""), { + headers: { "Content-Type": "text/event-stream" }, + }), + { preconnect: fetch.preconnect }, + ) + const model = createMistral({ apiKey: "test", fetch: mockFetch })("mistral-small-latest") + const result = await model.doStream({ + prompt: [{ role: "user", content: [{ type: "text", text: "Hello" }] }], + }) + const events = [] + for await (const event of result.stream) events.push(event) + + expect(events.find((event) => event.type === "reasoning-end")?.providerMetadata).toEqual({ + mistral: { + thinking: { + type: "thinking", + thinking: [ + { type: "text", text: "thinking" }, + { + type: "tool_reference", + tool: "web_search", + title: "Example result", + url: "https://example.com/tool", + favicon: "https://example.com/favicon.ico", + description: "Example description", + }, + { type: "reference", reference_ids: [1, "source-2"] }, + ], + closed: true, + signature: "sig-123", + }, + }, + }) + expect( + events + .filter((event) => event.type === "reasoning-start" || event.type === "reasoning-delta") + .every((event) => event.providerMetadata === undefined), + ).toBe(true) +}) + +test("Mistral preserves metadata-only thinking chunks", async () => { + const thinking = { + type: "thinking" as const, + thinking: [ + { + type: "tool_reference", + tool: "web_search", + title: "Example result", + url: "https://example.com/tool", + favicon: "https://example.com/favicon.ico", + description: "Example description", + }, + { type: "reference", reference_ids: [1, "source-2"] }, + ], + closed: true, + signature: "sig-123", + } + const mockFetch = Object.assign( + async () => + Response.json({ + id: "response-1", + created: 0, + model: "mistral-small-latest", + object: "chat.completion", + choices: [{ index: 0, message: { role: "assistant", content: [thinking] }, finish_reason: "stop" }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }), + { preconnect: fetch.preconnect }, + ) + const model = createMistral({ apiKey: "test", fetch: mockFetch })("mistral-small-latest") + const result = await model.doGenerate({ + prompt: [{ role: "user", content: [{ type: "text", text: "Hello" }] }], + }) + + expect(result.content).toEqual([ + { + type: "reasoning", + text: "", + providerMetadata: { mistral: { thinking } }, + }, + ]) +}) diff --git a/packages/desktop/src/main/server.ts b/packages/desktop/src/main/server.ts index 75ff50292871..05eeab00d9ee 100644 --- a/packages/desktop/src/main/server.ts +++ b/packages/desktop/src/main/server.ts @@ -181,9 +181,9 @@ export async function spawnLocalServer( } export async function checkHealth(url: string, password?: string | null): Promise { - let healthUrl: URL + let healthUrls: URL[] try { - healthUrl = new URL("/global/health", url) + healthUrls = [new URL("/api/health", url), new URL("/global/health", url)] } catch { return false } @@ -194,16 +194,17 @@ export async function checkHealth(url: string, password?: string | null): Promis headers.set("authorization", `Basic ${auth}`) } - try { - const res = await fetch(healthUrl, { - method: "GET", - headers, - signal: AbortSignal.timeout(3000), - }) - return res.ok - } catch { - return false + for (const healthUrl of healthUrls) { + try { + const res = await fetch(healthUrl, { + method: "GET", + headers, + signal: AbortSignal.timeout(3000), + }) + if (res.ok) return true + } catch {} } + return false } function createSidecarEnv(): Record { diff --git a/packages/session-ui/package.json b/packages/session-ui/package.json index 96611da331ec..3cf74b043b67 100644 --- a/packages/session-ui/package.json +++ b/packages/session-ui/package.json @@ -39,6 +39,7 @@ }, "dependencies": { "@kobalte/core": "catalog:", + "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/ui": "workspace:*", diff --git a/patches/@ai-sdk%2Fmistral@3.0.51.patch b/patches/@ai-sdk%2Fmistral@3.0.51.patch new file mode 100644 index 000000000000..141b14a689b1 --- /dev/null +++ b/patches/@ai-sdk%2Fmistral@3.0.51.patch @@ -0,0 +1,709 @@ +diff --git a/dist/index.d.mts b/dist/index.d.mts +index 1bde0b9f8cbe6771a52c1041095c9dddfe8e5b6c..0ca2ffb2a0c9327aed5ddcf0004500dc8b42569f 100644 +--- a/dist/index.d.mts ++++ b/dist/index.d.mts +@@ -14,6 +14,7 @@ declare const mistralLanguageModelOptions: z.ZodObject<{ + none: "none"; + high: "high"; + }>>; ++ promptCacheKey: z.ZodOptional; + }, z.core.$strip>; + type MistralLanguageModelOptions = z.infer; + +diff --git a/dist/index.d.ts b/dist/index.d.ts +index 1bde0b9f8cbe6771a52c1041095c9dddfe8e5b6c..0ca2ffb2a0c9327aed5ddcf0004500dc8b42569f 100644 +--- a/dist/index.d.ts ++++ b/dist/index.d.ts +@@ -14,6 +14,7 @@ declare const mistralLanguageModelOptions: z.ZodObject<{ + none: "none"; + high: "high"; + }>>; ++ promptCacheKey: z.ZodOptional; + }, z.core.$strip>; + type MistralLanguageModelOptions = z.infer; + +diff --git a/dist/index.js b/dist/index.js +index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..267f34e20ea392b7a85ad5259d72d50605a6f971 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -128,11 +128,14 @@ function convertToMistralChatMessages(prompt) { + } + case "assistant": { + let text = ""; ++ const structuredContent = []; ++ let hasNativeReasoning = false; + const toolCalls = []; + for (const part of content) { + switch (part.type) { + case "text": { + text += part.text; ++ structuredContent.push({ type: "text", text: part.text }); + break; + } + case "tool-call": { +@@ -148,6 +151,13 @@ function convertToMistralChatMessages(prompt) { + } + case "reasoning": { + text += part.text; ++ const native = part.providerOptions?.mistral?.thinking; ++ if (native?.type === "thinking") { ++ hasNativeReasoning = true; ++ structuredContent.push(native); ++ break; ++ } ++ structuredContent.push({ type: "text", text: part.text }); + break; + } + default: { +@@ -159,7 +169,7 @@ function convertToMistralChatMessages(prompt) { + } + messages.push({ + role: "assistant", +- content: text, ++ content: hasNativeReasoning ? structuredContent : text, + prefix: isLastMessage ? true : void 0, + tool_calls: toolCalls.length > 0 ? toolCalls : void 0 + }); +@@ -268,7 +278,8 @@ var mistralLanguageModelOptions = import_v4.z.object({ + * - `'high'`: Enable reasoning + * - `'none'`: Disable reasoning + */ +- reasoningEffort: import_v4.z.enum(["high", "none"]).optional() ++ reasoningEffort: import_v4.z.enum(["high", "none"]).optional(), ++ promptCacheKey: import_v4.z.string().optional() + }); + + // src/mistral-error.ts +@@ -407,6 +418,7 @@ var MistralChatLanguageModel = class { + stop: stopSequences, + random_seed: seed, + reasoning_effort: options.reasoningEffort, ++ prompt_cache_key: options.promptCacheKey, + // response format: + response_format: (responseFormat == null ? void 0 : responseFormat.type) === "json" ? structuredOutputs && (responseFormat == null ? void 0 : responseFormat.schema) != null ? { + type: "json_schema", +@@ -465,9 +477,11 @@ var MistralChatLanguageModel = class { + for (const part of choice.message.content) { + if (part.type === "thinking") { + const reasoningText = extractReasoningContent(part.thinking); +- if (reasoningText.length > 0) { +- content.push({ type: "reasoning", text: reasoningText }); +- } ++ content.push({ ++ type: "reasoning", ++ text: reasoningText, ++ providerMetadata: { mistral: { thinking: part } } ++ }); + } else if (part.type === "text") { + if (part.text.length > 0) { + content.push({ type: "text", text: part.text }); +@@ -528,6 +542,7 @@ var MistralChatLanguageModel = class { + let isFirstChunk = true; + let activeText = false; + let activeReasoningId = null; ++ let activeThinking = null; + const generateId2 = this.generateId; + return { + stream: response.pipeThrough( +@@ -561,18 +576,19 @@ var MistralChatLanguageModel = class { + for (const part of delta.content) { + if (part.type === "thinking") { + const reasoningDelta = extractReasoningContent(part.thinking); +- if (reasoningDelta.length > 0) { +- if (activeReasoningId == null) { +- if (activeText) { +- controller.enqueue({ type: "text-end", id: "0" }); +- activeText = false; +- } +- activeReasoningId = generateId2(); +- controller.enqueue({ +- type: "reasoning-start", +- id: activeReasoningId +- }); ++ activeThinking = mergeThinking(activeThinking, part); ++ if (activeReasoningId == null) { ++ if (activeText) { ++ controller.enqueue({ type: "text-end", id: "0" }); ++ activeText = false; + } ++ activeReasoningId = generateId2(); ++ controller.enqueue({ ++ type: "reasoning-start", ++ id: activeReasoningId ++ }); ++ } ++ if (reasoningDelta.length > 0) { + controller.enqueue({ + type: "reasoning-delta", + id: activeReasoningId, +@@ -587,9 +603,11 @@ var MistralChatLanguageModel = class { + if (activeReasoningId != null) { + controller.enqueue({ + type: "reasoning-end", +- id: activeReasoningId ++ id: activeReasoningId, ++ providerMetadata: { mistral: { thinking: activeThinking } } + }); + activeReasoningId = null; ++ activeThinking = null; + } + controller.enqueue({ type: "text-start", id: "0" }); + activeText = true; +@@ -638,7 +656,8 @@ var MistralChatLanguageModel = class { + if (activeReasoningId != null) { + controller.enqueue({ + type: "reasoning-end", +- id: activeReasoningId ++ id: activeReasoningId, ++ providerMetadata: { mistral: { thinking: activeThinking } } + }); + } + if (activeText) { +@@ -660,6 +679,13 @@ var MistralChatLanguageModel = class { + function extractReasoningContent(thinking) { + return thinking.filter((chunk) => chunk.type === "text").map((chunk) => chunk.text).join(""); + } ++function mergeThinking(current, next) { ++ if (current === null) return { ...next, thinking: [...next.thinking] }; ++ current.thinking.push(...next.thinking); ++ if (next.closed !== void 0) current.closed = next.closed; ++ if (next.signature !== void 0) current.signature = next.signature; ++ return current; ++} + function extractTextContent(content) { + if (typeof content === "string") { + return content; +@@ -686,6 +712,30 @@ function extractTextContent(content) { + } + return textContent.length ? textContent.join("") : void 0; + } ++var mistralThinkingContentSchema = import_v43.z.discriminatedUnion("type", [ ++ import_v43.z.object({ ++ type: import_v43.z.literal("text"), ++ text: import_v43.z.string() ++ }), ++ import_v43.z.object({ ++ type: import_v43.z.literal("tool_reference"), ++ tool: import_v43.z.string(), ++ title: import_v43.z.string(), ++ url: import_v43.z.string().nullish(), ++ favicon: import_v43.z.string().nullish(), ++ description: import_v43.z.string().nullish() ++ }), ++ import_v43.z.object({ ++ type: import_v43.z.literal("reference"), ++ reference_ids: import_v43.z.array(import_v43.z.union([import_v43.z.string(), import_v43.z.number().int()])) ++ }) ++]); ++var mistralThinkChunkSchema = import_v43.z.object({ ++ type: import_v43.z.literal("thinking"), ++ thinking: import_v43.z.array(mistralThinkingContentSchema), ++ closed: import_v43.z.boolean().optional(), ++ signature: import_v43.z.string().nullish() ++}); + var mistralContentSchema = import_v43.z.union([ + import_v43.z.string(), + import_v43.z.array( +@@ -708,15 +758,7 @@ var mistralContentSchema = import_v43.z.union([ + type: import_v43.z.literal("reference"), + reference_ids: import_v43.z.array(import_v43.z.union([import_v43.z.string(), import_v43.z.number()])) + }), +- import_v43.z.object({ +- type: import_v43.z.literal("thinking"), +- thinking: import_v43.z.array( +- import_v43.z.object({ +- type: import_v43.z.literal("text"), +- text: import_v43.z.string() +- }) +- ) +- }) ++ mistralThinkChunkSchema + ]) + ) + ]).nullish(); +diff --git a/dist/index.mjs b/dist/index.mjs +index d2eff622c1b84a96bdeb4012cb0206a33012a04d..3bff11ddd6136ada45809568828cbc8f2493a42a 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -116,11 +116,14 @@ function convertToMistralChatMessages(prompt) { + } + case "assistant": { + let text = ""; ++ const structuredContent = []; ++ let hasNativeReasoning = false; + const toolCalls = []; + for (const part of content) { + switch (part.type) { + case "text": { + text += part.text; ++ structuredContent.push({ type: "text", text: part.text }); + break; + } + case "tool-call": { +@@ -136,6 +139,13 @@ function convertToMistralChatMessages(prompt) { + } + case "reasoning": { + text += part.text; ++ const native = part.providerOptions?.mistral?.thinking; ++ if (native?.type === "thinking") { ++ hasNativeReasoning = true; ++ structuredContent.push(native); ++ break; ++ } ++ structuredContent.push({ type: "text", text: part.text }); + break; + } + default: { +@@ -147,7 +157,7 @@ function convertToMistralChatMessages(prompt) { + } + messages.push({ + role: "assistant", +- content: text, ++ content: hasNativeReasoning ? structuredContent : text, + prefix: isLastMessage ? true : void 0, + tool_calls: toolCalls.length > 0 ? toolCalls : void 0 + }); +@@ -256,7 +266,8 @@ var mistralLanguageModelOptions = z.object({ + * - `'high'`: Enable reasoning + * - `'none'`: Disable reasoning + */ +- reasoningEffort: z.enum(["high", "none"]).optional() ++ reasoningEffort: z.enum(["high", "none"]).optional(), ++ promptCacheKey: z.string().optional() + }); + + // src/mistral-error.ts +@@ -397,6 +408,7 @@ var MistralChatLanguageModel = class { + stop: stopSequences, + random_seed: seed, + reasoning_effort: options.reasoningEffort, ++ prompt_cache_key: options.promptCacheKey, + // response format: + response_format: (responseFormat == null ? void 0 : responseFormat.type) === "json" ? structuredOutputs && (responseFormat == null ? void 0 : responseFormat.schema) != null ? { + type: "json_schema", +@@ -455,9 +467,11 @@ var MistralChatLanguageModel = class { + for (const part of choice.message.content) { + if (part.type === "thinking") { + const reasoningText = extractReasoningContent(part.thinking); +- if (reasoningText.length > 0) { +- content.push({ type: "reasoning", text: reasoningText }); +- } ++ content.push({ ++ type: "reasoning", ++ text: reasoningText, ++ providerMetadata: { mistral: { thinking: part } } ++ }); + } else if (part.type === "text") { + if (part.text.length > 0) { + content.push({ type: "text", text: part.text }); +@@ -518,6 +532,7 @@ var MistralChatLanguageModel = class { + let isFirstChunk = true; + let activeText = false; + let activeReasoningId = null; ++ let activeThinking = null; + const generateId2 = this.generateId; + return { + stream: response.pipeThrough( +@@ -551,18 +566,19 @@ var MistralChatLanguageModel = class { + for (const part of delta.content) { + if (part.type === "thinking") { + const reasoningDelta = extractReasoningContent(part.thinking); +- if (reasoningDelta.length > 0) { +- if (activeReasoningId == null) { +- if (activeText) { +- controller.enqueue({ type: "text-end", id: "0" }); +- activeText = false; +- } +- activeReasoningId = generateId2(); +- controller.enqueue({ +- type: "reasoning-start", +- id: activeReasoningId +- }); ++ activeThinking = mergeThinking(activeThinking, part); ++ if (activeReasoningId == null) { ++ if (activeText) { ++ controller.enqueue({ type: "text-end", id: "0" }); ++ activeText = false; + } ++ activeReasoningId = generateId2(); ++ controller.enqueue({ ++ type: "reasoning-start", ++ id: activeReasoningId ++ }); ++ } ++ if (reasoningDelta.length > 0) { + controller.enqueue({ + type: "reasoning-delta", + id: activeReasoningId, +@@ -577,9 +593,11 @@ var MistralChatLanguageModel = class { + if (activeReasoningId != null) { + controller.enqueue({ + type: "reasoning-end", +- id: activeReasoningId ++ id: activeReasoningId, ++ providerMetadata: { mistral: { thinking: activeThinking } } + }); + activeReasoningId = null; ++ activeThinking = null; + } + controller.enqueue({ type: "text-start", id: "0" }); + activeText = true; +@@ -628,7 +646,8 @@ var MistralChatLanguageModel = class { + if (activeReasoningId != null) { + controller.enqueue({ + type: "reasoning-end", +- id: activeReasoningId ++ id: activeReasoningId, ++ providerMetadata: { mistral: { thinking: activeThinking } } + }); + } + if (activeText) { +@@ -650,6 +669,13 @@ var MistralChatLanguageModel = class { + function extractReasoningContent(thinking) { + return thinking.filter((chunk) => chunk.type === "text").map((chunk) => chunk.text).join(""); + } ++function mergeThinking(current, next) { ++ if (current === null) return { ...next, thinking: [...next.thinking] }; ++ current.thinking.push(...next.thinking); ++ if (next.closed !== void 0) current.closed = next.closed; ++ if (next.signature !== void 0) current.signature = next.signature; ++ return current; ++} + function extractTextContent(content) { + if (typeof content === "string") { + return content; +@@ -676,6 +702,30 @@ function extractTextContent(content) { + } + return textContent.length ? textContent.join("") : void 0; + } ++var mistralThinkingContentSchema = z3.discriminatedUnion("type", [ ++ z3.object({ ++ type: z3.literal("text"), ++ text: z3.string() ++ }), ++ z3.object({ ++ type: z3.literal("tool_reference"), ++ tool: z3.string(), ++ title: z3.string(), ++ url: z3.string().nullish(), ++ favicon: z3.string().nullish(), ++ description: z3.string().nullish() ++ }), ++ z3.object({ ++ type: z3.literal("reference"), ++ reference_ids: z3.array(z3.union([z3.string(), z3.number().int()])) ++ }) ++]); ++var mistralThinkChunkSchema = z3.object({ ++ type: z3.literal("thinking"), ++ thinking: z3.array(mistralThinkingContentSchema), ++ closed: z3.boolean().optional(), ++ signature: z3.string().nullish() ++}); + var mistralContentSchema = z3.union([ + z3.string(), + z3.array( +@@ -698,15 +748,7 @@ var mistralContentSchema = z3.union([ + type: z3.literal("reference"), + reference_ids: z3.array(z3.union([z3.string(), z3.number()])) + }), +- z3.object({ +- type: z3.literal("thinking"), +- thinking: z3.array( +- z3.object({ +- type: z3.literal("text"), +- text: z3.string() +- }) +- ) +- }) ++ mistralThinkChunkSchema + ]) + ) + ]).nullish(); +diff --git a/src/convert-to-mistral-chat-messages.ts b/src/convert-to-mistral-chat-messages.ts +index 3c6914f8da615d7517bc43dd56198298d0a50247..8cd6f4c7577f746ef41e8a0aee682234c473667a 100644 +--- a/src/convert-to-mistral-chat-messages.ts ++++ b/src/convert-to-mistral-chat-messages.ts +@@ -3,7 +3,11 @@ import { + type LanguageModelV3DataContent, + type LanguageModelV3Prompt, + } from '@ai-sdk/provider'; +-import type { MistralPrompt } from './mistral-chat-prompt'; ++import type { ++ MistralAssistantMessageContent, ++ MistralPrompt, ++ MistralThinkChunk, ++} from './mistral-chat-prompt'; + import { convertToBase64 } from '@ai-sdk/provider-utils'; + + function formatFileUrl({ +@@ -76,6 +80,8 @@ export function convertToMistralChatMessages( + + case 'assistant': { + let text = ''; ++ const structuredContent: Array = []; ++ let hasNativeReasoning = false; + const toolCalls: Array<{ + id: string; + type: 'function'; +@@ -86,6 +92,7 @@ export function convertToMistralChatMessages( + switch (part.type) { + case 'text': { + text += part.text; ++ structuredContent.push({ type: 'text', text: part.text }); + break; + } + case 'tool-call': { +@@ -101,6 +108,14 @@ export function convertToMistralChatMessages( + } + case 'reasoning': { + text += part.text; ++ const native = part.providerOptions?.mistral ++ ?.thinking as MistralThinkChunk | undefined; ++ if (native?.type === 'thinking') { ++ hasNativeReasoning = true; ++ structuredContent.push(native); ++ break; ++ } ++ structuredContent.push({ type: 'text', text: part.text }); + break; + } + default: { +@@ -113,7 +128,7 @@ export function convertToMistralChatMessages( + + messages.push({ + role: 'assistant', +- content: text, ++ content: hasNativeReasoning ? structuredContent : text, + prefix: isLastMessage ? true : undefined, + tool_calls: toolCalls.length > 0 ? toolCalls : undefined, + }); +diff --git a/src/mistral-chat-language-model.ts b/src/mistral-chat-language-model.ts +index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..847d26f9dfe03572a969a122f8c96b8bbfda8066 100644 +--- a/src/mistral-chat-language-model.ts ++++ b/src/mistral-chat-language-model.ts +@@ -122,6 +122,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { + stop: stopSequences, + random_seed: seed, + reasoning_effort: options.reasoningEffort, ++ prompt_cache_key: options.promptCacheKey, + + // response format: + response_format: +@@ -201,9 +202,11 @@ export class MistralChatLanguageModel implements LanguageModelV3 { + for (const part of choice.message.content) { + if (part.type === 'thinking') { + const reasoningText = extractReasoningContent(part.thinking); +- if (reasoningText.length > 0) { +- content.push({ type: 'reasoning', text: reasoningText }); +- } ++ content.push({ ++ type: 'reasoning', ++ text: reasoningText, ++ providerMetadata: { mistral: { thinking: part } }, ++ }); + } else if (part.type === 'text') { + if (part.text.length > 0) { + content.push({ type: 'text', text: part.text }); +@@ -278,6 +281,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { + let isFirstChunk = true; + let activeText = false; + let activeReasoningId: string | null = null; ++ let activeThinking: z.infer | null = null; + + const generateId = this.generateId; + +@@ -326,20 +330,21 @@ export class MistralChatLanguageModel implements LanguageModelV3 { + for (const part of delta.content) { + if (part.type === 'thinking') { + const reasoningDelta = extractReasoningContent(part.thinking); +- if (reasoningDelta.length > 0) { +- if (activeReasoningId == null) { +- // end any active text before starting reasoning +- if (activeText) { +- controller.enqueue({ type: 'text-end', id: '0' }); +- activeText = false; +- } +- +- activeReasoningId = generateId(); +- controller.enqueue({ +- type: 'reasoning-start', +- id: activeReasoningId, +- }); ++ activeThinking = mergeThinking(activeThinking, part); ++ if (activeReasoningId == null) { ++ // end any active text before starting reasoning ++ if (activeText) { ++ controller.enqueue({ type: 'text-end', id: '0' }); ++ activeText = false; + } ++ ++ activeReasoningId = generateId(); ++ controller.enqueue({ ++ type: 'reasoning-start', ++ id: activeReasoningId, ++ }); ++ } ++ if (reasoningDelta.length > 0) { + controller.enqueue({ + type: 'reasoning-delta', + id: activeReasoningId, +@@ -357,8 +362,12 @@ export class MistralChatLanguageModel implements LanguageModelV3 { + controller.enqueue({ + type: 'reasoning-end', + id: activeReasoningId, ++ providerMetadata: { ++ mistral: { thinking: activeThinking }, ++ }, + }); + activeReasoningId = null; ++ activeThinking = null; + } + controller.enqueue({ type: 'text-start', id: '0' }); + activeText = true; +@@ -416,6 +425,9 @@ export class MistralChatLanguageModel implements LanguageModelV3 { + controller.enqueue({ + type: 'reasoning-end', + id: activeReasoningId, ++ providerMetadata: { ++ mistral: { thinking: activeThinking }, ++ }, + }); + } + if (activeText) { +@@ -437,7 +449,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { + } + + function extractReasoningContent( +- thinking: Array<{ type: string; text: string }>, ++ thinking: Array>, + ) { + return thinking + .filter(chunk => chunk.type === 'text') +@@ -445,6 +457,17 @@ function extractReasoningContent( + .join(''); + } + ++function mergeThinking( ++ current: z.infer | null, ++ next: z.infer, ++) { ++ if (current === null) return { ...next, thinking: [...next.thinking] }; ++ current.thinking.push(...next.thinking); ++ if (next.closed !== undefined) current.closed = next.closed; ++ if (next.signature !== undefined) current.signature = next.signature; ++ return current; ++} ++ + function extractTextContent(content: z.infer) { + if (typeof content === 'string') { + return content; +@@ -478,6 +501,32 @@ function extractTextContent(content: z.infer) { + return textContent.length ? textContent.join('') : undefined; + } + ++const mistralThinkingContentSchema = z.discriminatedUnion('type', [ ++ z.object({ ++ type: z.literal('text'), ++ text: z.string(), ++ }), ++ z.object({ ++ type: z.literal('tool_reference'), ++ tool: z.string(), ++ title: z.string(), ++ url: z.string().nullish(), ++ favicon: z.string().nullish(), ++ description: z.string().nullish(), ++ }), ++ z.object({ ++ type: z.literal('reference'), ++ reference_ids: z.array(z.union([z.string(), z.number().int()])), ++ }), ++]); ++ ++const mistralThinkChunkSchema = z.object({ ++ type: z.literal('thinking'), ++ thinking: z.array(mistralThinkingContentSchema), ++ closed: z.boolean().optional(), ++ signature: z.string().nullish(), ++}); ++ + const mistralContentSchema = z + .union([ + z.string(), +@@ -501,15 +550,7 @@ const mistralContentSchema = z + type: z.literal('reference'), + reference_ids: z.array(z.union([z.string(), z.number()])), + }), +- z.object({ +- type: z.literal('thinking'), +- thinking: z.array( +- z.object({ +- type: z.literal('text'), +- text: z.string(), +- }), +- ), +- }), ++ mistralThinkChunkSchema, + ]), + ), + ]) +diff --git a/src/mistral-chat-options.ts b/src/mistral-chat-options.ts +index 54b29c08517d348995b6ca093b11160e453d5c8b..de30c3e7d924889339e38b1067cb26e9ada05d11 100644 +--- a/src/mistral-chat-options.ts ++++ b/src/mistral-chat-options.ts +@@ -64,6 +64,11 @@ export const mistralLanguageModelOptions = z.object({ + * - `'none'`: Disable reasoning + */ + reasoningEffort: z.enum(['high', 'none']).optional(), ++ ++ /** ++ * A stable identifier used to route requests with shared prompt prefixes. ++ */ ++ promptCacheKey: z.string().optional(), + }); + + export type MistralLanguageModelOptions = z.infer< +diff --git a/src/mistral-chat-prompt.ts b/src/mistral-chat-prompt.ts +index 13f1dced55ac4be084128127a57fbdd58115bc28..172b11dde3dd326c2f3befd99237474ed8c79285 100644 +--- a/src/mistral-chat-prompt.ts ++++ b/src/mistral-chat-prompt.ts +@@ -23,7 +23,7 @@ export type MistralUserMessageContent = + + export interface MistralAssistantMessage { + role: 'assistant'; +- content: string; ++ content: string | Array; + prefix?: boolean; + tool_calls?: Array<{ + id: string; +@@ -32,6 +32,29 @@ export interface MistralAssistantMessage { + }>; + } + ++export type MistralAssistantMessageContent = ++ | { type: 'text'; text: string } ++ | MistralThinkChunk; ++ ++export type MistralThinkChunk = { ++ type: 'thinking'; ++ thinking: Array; ++ closed?: boolean; ++ signature?: string | null; ++}; ++ ++export type MistralThinkingContent = ++ | { type: 'text'; text: string } ++ | { ++ type: 'tool_reference'; ++ tool: string; ++ title: string; ++ url?: string | null; ++ favicon?: string | null; ++ description?: string | null; ++ } ++ | { type: 'reference'; reference_ids: Array }; ++ + export interface MistralToolMessage { + role: 'tool'; + name: string; From e7ecee5df24ae16d81d01f42f4b6d71ebd70711e Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 23 Jul 2026 17:40:42 -0400 Subject: [PATCH 064/150] fix(core): isolate tool hook outcomes (#38571) --- packages/codemode/src/tool.ts | 2 +- packages/core/src/plugin/host.ts | 24 ++++++-------- packages/core/src/session/runner/llm.ts | 2 +- .../src/session/runner/publish-llm-event.ts | 31 +++++++++---------- packages/core/test/plugin.test.ts | 2 +- .../test/session-runner-tool-events.test.ts | 16 ++++++++-- 6 files changed, 40 insertions(+), 37 deletions(-) diff --git a/packages/codemode/src/tool.ts b/packages/codemode/src/tool.ts index d44e3d0f4e27..93b5a23598ca 100644 --- a/packages/codemode/src/tool.ts +++ b/packages/codemode/src/tool.ts @@ -29,7 +29,7 @@ export type JsonSchema = { /** Either a validating Effect Schema or a render-only JSON Schema document. */ export type SchemaType = Schema.Decoder | JsonSchema -/** Executable tool tool exposed through CodeMode's `tools` object. */ +/** Executable tool exposed through CodeMode's `tools` object. */ export type Tool = { readonly _tag: "CodeModeTool" readonly description: string diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index 7761aa18235d..640fdea14825 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -394,19 +394,15 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int }) } return toolHooks.hook.after((event) => { - // JS plugin boundary: marshal the canonical outcome out, copy mutations back. - const output: Record = { + // Decode first so plugin mutations cannot alias the canonical outcome. + const output = { tool: event.tool, sessionID: event.sessionID, agent: event.agent, messageID: event.messageID, callID: event.callID, input: event.input, - status: event.status, - content: event.content, - metadata: event.metadata, - outputPaths: event.outputPaths, - ...(event.status === "error" ? { error: event.error } : {}), + ...Schema.decodeUnknownSync(Tool.ExecuteAfterOutcome)(event), } return Reflect.apply(callback, undefined, [output]).pipe( Effect.tap(() => { @@ -417,16 +413,16 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int return Effect.logWarning("ignoring execute.after tool status change", { tool: event.tool }) return Effect.sync(() => { if (event.status === "completed" && decoded.value.status === "completed") { - if (output.content !== event.content) event.content = decoded.value.content - if (output.metadata !== event.metadata) event.metadata = decoded.value.metadata - if (output.outputPaths !== event.outputPaths) event.outputPaths = decoded.value.outputPaths + event.content = decoded.value.content + event.metadata = decoded.value.metadata + event.outputPaths = decoded.value.outputPaths return } if (event.status === "error" && decoded.value.status === "error") { - if (output.error !== event.error) event.error = decoded.value.error - if (output.content !== event.content) event.content = decoded.value.content - if (output.metadata !== event.metadata) event.metadata = decoded.value.metadata - if (output.outputPaths !== event.outputPaths) event.outputPaths = decoded.value.outputPaths + event.error = decoded.value.error + event.content = decoded.value.content + event.metadata = decoded.value.metadata + event.outputPaths = decoded.value.outputPaths } }) }), diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 3d92a5c8134f..ff00a8337bb1 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -130,7 +130,7 @@ const layer = Layer.effect( // Durable publishes are serialized so tool fibers and step settlement never interleave // mid-event. const serialized = (effect: Effect.Effect) => publication.withPermit(effect) - const publish = (event: LLMEvent, error?: SessionError.Error) => serialized(publisher.publish(event, error)) + const publish = (event: LLMEvent) => serialized(publisher.publish(event)) let overflowFailure: ProviderErrorEvent | undefined const providerStream = llm.stream(prepared.request).pipe( Stream.runForEach((event) => diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts index f84253c39d2a..d3d21fbfc82b 100644 --- a/packages/core/src/session/runner/publish-llm-event.ts +++ b/packages/core/src/session/runner/publish-llm-event.ts @@ -1,5 +1,5 @@ -import { type LLMEvent, type ProviderMetadata, type ToolContent, type ToolResultValue } from "@opencode-ai/ai" -import { Effect, Schema } from "effect" +import { type LLMEvent, type ProviderMetadata, type ToolResultValue } from "@opencode-ai/ai" +import { Effect } from "effect" import { EventV2 } from "../../event" import { ModelV2 } from "../../model" import { SessionEvent } from "../event" @@ -12,7 +12,6 @@ import { Snapshot } from "../../snapshot" import { RelativePath } from "../../schema" import { SessionUsage } from "../usage" import { Tool } from "../../tool/tool" -import { MAX_BYTES } from "../../tool-output-store" import type { ToolRegistry } from "../../tool/registry" type Input = { @@ -28,9 +27,11 @@ const record = (value: unknown): Record => typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record) : { value } /** Derives canonical model content from a provider-hosted tool result. */ -const hostedContent = (result: ToolResultValue): readonly [ToolContent, ...ToolContent[]] => { - if (result.type === "content" && result.value.length > 0) - return result.value as unknown as readonly [ToolContent, ...ToolContent[]] +const hostedContent = (result: ToolResultValue): Tool.NonEmptyContent => { + if (result.type === "content") { + const content = Tool.nonEmpty(result.value) + if (content !== undefined) return content + } return [{ type: "text", text: Tool.stringify(result.value) }] } @@ -47,11 +48,8 @@ export const createLLMEventPublisher = (events: Pick() - const failureSnapshot = (tool: { readonly progress?: ToolRegistry.Progress }) => { - if (!tool.progress) return {} - const metadata = Tool.jsonMetadata(tool.progress, MAX_BYTES) - return metadata === undefined ? {} : { metadata } - } + const failureSnapshot = (tool: { readonly progress?: ToolRegistry.Progress }) => + tool.progress === undefined ? {} : { metadata: tool.progress } let assistantMessageID = input.assistantMessageID let stepStarted = false let stepFailed = false @@ -292,7 +290,7 @@ export const createLLMEventPublisher = (events: Pick { yield* ctx.tool .hook("execute.after", (event) => Effect.sync(() => { - if (event.status === "completed") event.content = [] as never + if (event.status === "completed") (event.content as unknown as unknown[]).splice(0) }), ) .pipe(Effect.asVoid) diff --git a/packages/core/test/session-runner-tool-events.test.ts b/packages/core/test/session-runner-tool-events.test.ts index 2ba096b306a3..702999719c3d 100644 --- a/packages/core/test/session-runner-tool-events.test.ts +++ b/packages/core/test/session-runner-tool-events.test.ts @@ -118,9 +118,7 @@ test("provider-executed success derives content and retains provider result stat test("interrupted progress metadata remains in the terminal failure snapshot", async () => { const { published, publisher } = capture("anthropic", { interruptProgress: true }) await Effect.runPromise(publisher.publish(call)) - const exit = await Effect.runPromiseExit( - publisher.progress(call.id, { phase: "visible" }), - ) + const exit = await Effect.runPromiseExit(publisher.progress(call.id, { phase: "visible" })) expect(Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)).toBe(true) await Effect.runPromise(publisher.failUnsettledTools({ type: "aborted", message: "interrupted" })) @@ -129,6 +127,18 @@ test("interrupted progress metadata remains in the terminal failure snapshot", a }) }) +test("failure snapshot retains canonical progress above the default byte limit", async () => { + const { published, publisher } = capture("anthropic", { interruptProgress: true }) + await Effect.runPromise(publisher.publish(call)) + const detail = "x".repeat(60 * 1024) + await Effect.runPromiseExit(publisher.progress(call.id, { detail })) + await Effect.runPromise(publisher.failUnsettledTools({ type: "aborted", message: "interrupted" })) + + expect(published.find((event) => event.type === "session.tool.failed.2")?.data).toMatchObject({ + metadata: { detail }, + }) +}) + test("failure before progress omits partial output fields", async () => { const { published, publisher } = capture() await Effect.runPromise(publisher.publish(call)) From 6401eeaea0f185c7d5102759a237c996901f9dd1 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:16:11 -0500 Subject: [PATCH 065/150] feat(ai): preserve raw finish reasons (#38423) --- packages/ai/example/tutorial.ts | 7 +- .../ai/src/protocols/anthropic-messages.ts | 7 +- packages/ai/src/protocols/bedrock-converse.ts | 34 ++++++-- packages/ai/src/protocols/gemini.ts | 21 ++++- packages/ai/src/protocols/openai-chat.ts | 39 ++++++++-- packages/ai/src/protocols/openai-responses.ts | 5 +- packages/ai/src/protocols/utils/lifecycle.ts | 4 +- packages/ai/src/schema/events.ts | 16 ++-- packages/ai/test/adapter.test.ts | 2 +- packages/ai/test/lib/tool-runtime.ts | 2 +- packages/ai/test/llm.test.ts | 2 +- .../test/provider/anthropic-messages.test.ts | 55 ++++++++++++- .../ai/test/provider/bedrock-converse.test.ts | 29 ++++++- packages/ai/test/provider/gemini.test.ts | 61 +++++++++++++-- packages/ai/test/provider/openai-chat.test.ts | 20 ++++- .../provider/openai-compatible-chat.test.ts | 5 +- .../ai/test/provider/openai-responses.test.ts | 43 ++++++---- packages/ai/test/provider/openrouter.test.ts | 38 +++++++++ .../ai/test/provider/pdf.recorded.test.ts | 4 +- packages/ai/test/recorded-scenarios.ts | 10 +-- packages/ai/test/response.test.ts | 22 ++++-- packages/ai/test/schema.test.ts | 8 +- packages/core/src/aisdk.ts | 4 +- .../src/session/runner/publish-llm-event.ts | 6 +- packages/core/test/aisdk.test.ts | 1 + packages/core/test/generate.test.ts | 2 +- packages/core/test/session-compaction.test.ts | 4 +- packages/core/test/session-generate.test.ts | 4 +- .../test/session-runner-tool-events.test.ts | 6 +- packages/core/test/session-runner.test.ts | 78 +++++++++---------- packages/core/test/session-title.test.ts | 4 +- 31 files changed, 410 insertions(+), 133 deletions(-) diff --git a/packages/ai/example/tutorial.ts b/packages/ai/example/tutorial.ts index b109ef6230ef..3fc0603b7349 100644 --- a/packages/ai/example/tutorial.ts +++ b/packages/ai/example/tutorial.ts @@ -78,7 +78,10 @@ const streamText = LLM.stream(request).pipe( Stream.tap((event) => Effect.sync(() => { if (event.type === "text-delta") process.stdout.write(`\ntext: ${event.text}`) - if (event.type === "finish") process.stdout.write(`\nfinish: ${event.reason}\n`) + if (event.type === "finish") + process.stdout.write( + `\nfinish: ${event.reason.normalized}${event.reason.raw ? ` (${event.reason.raw})` : ""}\n`, + ) }), ), Stream.runDrain, @@ -194,7 +197,7 @@ const FakeProtocol = Protocol.make({ event: Schema.String, initial: () => undefined, step: (_, frame) => Effect.succeed([undefined, [{ type: "text-delta", id: "text-0", text: frame }]] as const), - onHalt: () => [{ type: "finish", reason: "stop" }], + onHalt: () => [{ type: "finish", reason: { normalized: "stop" } }], }, }) diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index fc4ce9ab5393..4f5ad902a034 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -601,7 +601,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques // ============================================================================= const mapFinishReason = (reason: string | null | undefined): FinishReason => { if (reason === "end_turn" || reason === "stop_sequence" || reason === "pause_turn") return "stop" - if (reason === "max_tokens") return "length" + if (reason === "max_tokens" || reason === "model_context_window_exceeded") return "length" if (reason === "tool_use") return "tool-calls" if (reason === "refusal") return "content-filter" return "unknown" @@ -836,7 +836,10 @@ const onMessageDelta = (state: ParserState, event: AnthropicEvent): StepResult = const usage = mergeUsage(state.usage, mapUsage(event.usage)) const events: LLMEvent[] = [] const lifecycle = Lifecycle.finish(state.lifecycle, events, { - reason: mapFinishReason(event.delta?.stop_reason), + reason: { + normalized: mapFinishReason(event.delta?.stop_reason), + raw: event.delta?.stop_reason ?? undefined, + }, usage, providerMetadata: event.delta?.stop_sequence ? anthropicMetadata({ stopSequence: event.delta.stop_sequence }) diff --git a/packages/ai/src/protocols/bedrock-converse.ts b/packages/ai/src/protocols/bedrock-converse.ts index 8393cd511f04..0f0316c51792 100644 --- a/packages/ai/src/protocols/bedrock-converse.ts +++ b/packages/ai/src/protocols/bedrock-converse.ts @@ -8,6 +8,7 @@ import { Usage, type CacheHint, type FinishReason, + type FinishReasonDetails, type JsonSchema, type LLMRequest, type ModelToolSchemaCompatibility, @@ -435,9 +436,10 @@ const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request: // ============================================================================= const mapFinishReason = (reason: string): FinishReason => { if (reason === "end_turn" || reason === "stop_sequence") return "stop" - if (reason === "max_tokens") return "length" + if (reason === "max_tokens" || reason === "model_context_window_exceeded") return "length" if (reason === "tool_use") return "tool-calls" if (reason === "content_filtered" || reason === "guardrail_intervened") return "content-filter" + if (reason === "malformed_model_output" || reason === "malformed_tool_use") return "error" return "unknown" } @@ -466,7 +468,7 @@ interface ParserState { // Bedrock splits the finish into `messageStop` (carries `stopReason`) and // `metadata` (carries usage). Hold the terminal event in state so `onHalt` // can emit exactly one finish after both chunks have had a chance to arrive. - readonly pendingFinish: { readonly reason: FinishReason; readonly usage?: Usage } | undefined + readonly pendingFinish: { readonly reason: FinishReasonDetails; readonly usage?: Usage } | undefined readonly hasToolCalls: boolean readonly lifecycle: Lifecycle.State readonly reasoningSignatures: Readonly> @@ -583,7 +585,13 @@ const step = (state: ParserState, event: BedrockEvent) => return [ { ...state, - pendingFinish: { reason: mapFinishReason(event.messageStop.stopReason), usage: state.pendingFinish?.usage }, + pendingFinish: { + reason: { + normalized: mapFinishReason(event.messageStop.stopReason), + raw: event.messageStop.stopReason, + }, + usage: state.pendingFinish?.usage, + }, }, [], ] as const @@ -591,7 +599,16 @@ const step = (state: ParserState, event: BedrockEvent) => if (event.metadata) { const usage = mapUsage(event.metadata.usage) - return [{ ...state, pendingFinish: { reason: state.pendingFinish?.reason ?? "stop", usage } }, []] as const + return [ + { + ...state, + pendingFinish: { + reason: state.pendingFinish?.reason ?? { normalized: "stop" }, + usage, + }, + }, + [], + ] as const } const exception = ( @@ -624,8 +641,13 @@ const onHalt = (state: ParserState): ReadonlyArray => ? (() => { const events: LLMEvent[] = [] Lifecycle.finish(state.lifecycle, events, { - reason: - state.pendingFinish.reason === "stop" && state.hasToolCalls ? "tool-calls" : state.pendingFinish.reason, + reason: { + ...state.pendingFinish.reason, + normalized: + state.pendingFinish.reason.normalized === "stop" && state.hasToolCalls + ? "tool-calls" + : state.pendingFinish.reason.normalized, + }, usage: state.pendingFinish.usage, }) return events diff --git a/packages/ai/src/protocols/gemini.ts b/packages/ai/src/protocols/gemini.ts index 57ad06603721..1c285da235d5 100644 --- a/packages/ai/src/protocols/gemini.ts +++ b/packages/ai/src/protocols/gemini.ts @@ -382,10 +382,22 @@ const mapFinishReason = (finishReason: string | undefined, hasToolCalls: boolean finishReason === "SAFETY" || finishReason === "BLOCKLIST" || finishReason === "PROHIBITED_CONTENT" || - finishReason === "SPII" + finishReason === "SPII" || + finishReason === "MODEL_ARMOR" || + finishReason === "IMAGE_PROHIBITED_CONTENT" || + finishReason === "IMAGE_RECITATION" || + finishReason === "LANGUAGE" ) return "content-filter" - if (finishReason === "MALFORMED_FUNCTION_CALL") return "error" + if ( + finishReason === "MALFORMED_FUNCTION_CALL" || + finishReason === "UNEXPECTED_TOOL_CALL" || + finishReason === "NO_IMAGE" || + finishReason === "TOO_MANY_TOOL_CALLS" || + finishReason === "MISSING_THOUGHT_SIGNATURE" || + finishReason === "MALFORMED_RESPONSE" + ) + return "error" return "unknown" } @@ -402,7 +414,10 @@ const finish = (state: ParserState): ReadonlyArray => ) : state.lifecycle Lifecycle.finish(lifecycle, events, { - reason: mapFinishReason(state.finishReason, state.hasToolCalls), + reason: { + normalized: mapFinishReason(state.finishReason, state.hasToolCalls), + raw: state.finishReason, + }, usage: state.usage, }) return events diff --git a/packages/ai/src/protocols/openai-chat.ts b/packages/ai/src/protocols/openai-chat.ts index e423137fca03..f0f5dbfb6b58 100644 --- a/packages/ai/src/protocols/openai-chat.ts +++ b/packages/ai/src/protocols/openai-chat.ts @@ -5,9 +5,11 @@ import { Endpoint } from "../route/endpoint" import { HttpTransport } from "../route/transport" import { Protocol } from "../route/protocol" import { + LLMError, LLMEvent, Usage, type FinishReason, + type FinishReasonDetails, type JsonSchema, type LLMRequest, type MediaPart, @@ -17,6 +19,7 @@ import { type ToolDefinition, type ToolContent, } from "../schema" +import { classifyProviderFailure } from "../provider-error" import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" import { OpenAIOptions } from "./utils/openai-options" import { Lifecycle } from "./utils/lifecycle" @@ -164,11 +167,18 @@ const OpenAIChatDelta = Schema.StructWithRest( const OpenAIChatChoice = Schema.Struct({ delta: optionalNull(OpenAIChatDelta), finish_reason: optionalNull(Schema.String), + native_finish_reason: optionalNull(Schema.String), +}) + +const OpenAIChatError = Schema.Struct({ + code: optionalNull(Schema.Union([Schema.String, Schema.Number])), + message: Schema.String, }) export const OpenAIChatEvent = Schema.Struct({ - choices: Schema.Array(OpenAIChatChoice), + choices: optionalNull(Schema.Array(OpenAIChatChoice)), usage: optionalNull(OpenAIChatUsage), + error: optionalNull(OpenAIChatError), }) export type OpenAIChatEvent = Schema.Schema.Type type OpenAIChatRequestMessage = LLMRequest["messages"][number] @@ -184,7 +194,7 @@ export interface ParserState { readonly pendingTools: Partial> readonly toolCallEvents: ReadonlyArray readonly usage?: Usage - readonly finishReason?: FinishReason + readonly finishReason?: FinishReasonDetails readonly lifecycle: Lifecycle.State readonly reasoningField?: string readonly reasoningDetails: Array @@ -439,6 +449,7 @@ const mapFinishReason = (reason: string | null | undefined): FinishReason => { if (reason === "length") return "length" if (reason === "content_filter") return "content-filter" if (reason === "function_call" || reason === "tool_calls") return "tool-calls" + if (reason === "error") return "error" return "unknown" } @@ -532,10 +543,22 @@ const reasoningMetadata = (field: ParserState["reasoningField"], details?: Reado const step = (state: ParserState, event: OpenAIChatEvent) => Effect.gen(function* () { + if (event.error) + return yield* new LLMError({ + module: ADAPTER, + method: "stream", + reason: classifyProviderFailure({ + message: event.error.message, + code: event.error.code === undefined || event.error.code === null ? undefined : String(event.error.code), + status: typeof event.error.code === "number" ? event.error.code : undefined, + }), + }) const events: LLMEvent[] = [] const usage = mapUsage(event.usage) ?? state.usage - const choice = event.choices[0] - const finishReason = choice?.finish_reason ? mapFinishReason(choice.finish_reason) : state.finishReason + const choice = event.choices?.[0] + const finishReason = choice?.finish_reason + ? { normalized: mapFinishReason(choice.finish_reason), raw: choice.native_finish_reason ?? choice.finish_reason } + : state.finishReason const delta = choice?.delta const toolDeltas = delta?.tool_calls ?? [] let tools = state.tools @@ -627,7 +650,13 @@ const step = (state: ParserState, event: OpenAIChatEvent) => const finishEvents = (state: ParserState): ReadonlyArray => { const events: LLMEvent[] = [] const hasToolCalls = state.toolCallEvents.length > 0 - const reason = state.finishReason === "stop" && hasToolCalls ? "tool-calls" : state.finishReason + const reason = state.finishReason + ? { + ...state.finishReason, + normalized: + state.finishReason.normalized === "stop" && hasToolCalls ? "tool-calls" : state.finishReason.normalized, + } + : undefined const metadata = reasoningMetadata( state.reasoningField, state.reasoningDetailsObserved ? state.reasoningDetails : undefined, diff --git a/packages/ai/src/protocols/openai-responses.ts b/packages/ai/src/protocols/openai-responses.ts index 2973acf00054..53fc62d3a0bf 100644 --- a/packages/ai/src/protocols/openai-responses.ts +++ b/packages/ai/src/protocols/openai-responses.ts @@ -979,7 +979,10 @@ const onOutputItemDone = Effect.fn("OpenAIResponses.onOutputItemDone")(function* const onResponseFinish = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { const events: LLMEvent[] = [] const lifecycle = Lifecycle.finish(state.lifecycle, events, { - reason: mapFinishReason(event, state.hasFunctionCall), + reason: { + normalized: mapFinishReason(event, state.hasFunctionCall), + raw: event.response?.incomplete_details?.reason, + }, usage: mapUsage(event.response?.usage), providerMetadata: event.response?.id || event.response?.service_tier diff --git a/packages/ai/src/protocols/utils/lifecycle.ts b/packages/ai/src/protocols/utils/lifecycle.ts index 6d0189df9a25..761cff3690f6 100644 --- a/packages/ai/src/protocols/utils/lifecycle.ts +++ b/packages/ai/src/protocols/utils/lifecycle.ts @@ -1,4 +1,4 @@ -import { LLMEvent, type FinishReason, type ProviderMetadata, type Usage } from "../../schema" +import { LLMEvent, type FinishReasonDetails, type ProviderMetadata, type Usage } from "../../schema" export interface State { readonly stepStarted: boolean @@ -81,7 +81,7 @@ export const finish = ( state: State, events: LLMEvent[], input: { - readonly reason: FinishReason + readonly reason: FinishReasonDetails readonly usage?: Usage readonly providerMetadata?: ProviderMetadata }, diff --git a/packages/ai/src/schema/events.ts b/packages/ai/src/schema/events.ts index 18454b84707c..d2195d8c1906 100644 --- a/packages/ai/src/schema/events.ts +++ b/packages/ai/src/schema/events.ts @@ -191,10 +191,16 @@ export const ToolError = Schema.Struct({ }).annotate({ identifier: "LLM.Event.ToolError" }) export type ToolError = Schema.Schema.Type +export const FinishReasonDetails = Schema.Struct({ + normalized: FinishReason, + raw: Schema.optional(Schema.String), +}).annotate({ identifier: "LLM.FinishReasonDetails" }) +export type FinishReasonDetails = Schema.Schema.Type + export const StepFinish = Schema.Struct({ type: Schema.tag("step-finish"), index: Schema.Number, - reason: FinishReason, + reason: FinishReasonDetails, usage: Schema.optional(Usage), providerMetadata: Schema.optional(ProviderMetadata), }).annotate({ identifier: "LLM.Event.StepFinish" }) @@ -202,7 +208,7 @@ export type StepFinish = Schema.Schema.Type export const Finish = Schema.Struct({ type: Schema.tag("finish"), - reason: FinishReason, + reason: FinishReasonDetails, usage: Schema.optional(Usage), providerMetadata: Schema.optional(ProviderMetadata), }).annotate({ identifier: "LLM.Event.Finish" }) @@ -365,7 +371,7 @@ interface ResponseState { readonly events: ReadonlyArray readonly message: Message readonly usage?: Usage - readonly finishReason?: FinishReason + readonly finishReason?: FinishReasonDetails readonly textParts: Readonly> readonly reasoningParts: Readonly> readonly toolInputs: Readonly> @@ -393,7 +399,7 @@ const appendEvent = (state: ResponseState, event: LLMEvent): ResponseState => { return { ...state, events, - finishReason: state.finishReason ?? "error", + finishReason: state.finishReason ?? { normalized: "error" }, } } return { @@ -580,7 +586,7 @@ export class LLMResponse extends Schema.Class("LLM.Response")({ message: Message, events: Schema.Array(LLMEvent), usage: Schema.optional(Usage), - finishReason: FinishReason, + finishReason: FinishReasonDetails, }) { /** Concatenated assistant text assembled from streamed `text-delta` events. */ get text() { diff --git a/packages/ai/test/adapter.test.ts b/packages/ai/test/adapter.test.ts index 912d89d1e69e..346013ced6dd 100644 --- a/packages/ai/test/adapter.test.ts +++ b/packages/ai/test/adapter.test.ts @@ -40,7 +40,7 @@ const fakeFraming: FramingDef = { const raiseEvent = (event: FakeEvent): import("../src/schema").LLMEvent => event.type === "finish" - ? { type: "finish", reason: event.reason } + ? { type: "finish", reason: { normalized: event.reason } } : { type: "text-delta", id: "text-0", text: event.text } const fakeProtocol = Protocol.make({ diff --git a/packages/ai/test/lib/tool-runtime.ts b/packages/ai/test/lib/tool-runtime.ts index 55fce8e0010b..56c7d91ea375 100644 --- a/packages/ai/test/lib/tool-runtime.ts +++ b/packages/ai/test/lib/tool-runtime.ts @@ -83,7 +83,7 @@ const indexStep = (event: LLMEvent, index: number): LLMEvent => { const stepState = (events: ReadonlyArray) => { const assistantContent: ContentPart[] = [] const toolCalls: ToolCallPart[] = [] - let reason: Extract["reason"] = "unknown" + let reason: Extract["reason"] = { normalized: "unknown" } let usage: Usage | undefined let providerMetadata: ProviderMetadata | undefined diff --git a/packages/ai/test/llm.test.ts b/packages/ai/test/llm.test.ts index 64346a8bb030..ca8829358564 100644 --- a/packages/ai/test/llm.test.ts +++ b/packages/ai/test/llm.test.ts @@ -191,7 +191,7 @@ describe("llm constructors", () => { LLMResponse.text({ events: [ { type: "text-delta", id: "text-0", text: "hi" }, - { type: "finish", reason: "stop" }, + { type: "finish", reason: { normalized: "stop" } }, ], }), ).toBe("hi") diff --git a/packages/ai/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts index 0b319f573b87..a6fcd628a288 100644 --- a/packages/ai/test/provider/anthropic-messages.test.ts +++ b/packages/ai/test/provider/anthropic-messages.test.ts @@ -448,12 +448,50 @@ describe("Anthropic Messages route", () => { ]) expect(response.events.at(-1)).toMatchObject({ type: "finish", - reason: "stop", + reason: { normalized: "stop", raw: "end_turn" }, providerMetadata: { anthropic: { stopSequence: "\n\nHuman:" } }, }) }), ) + it.effect("maps context-window truncation to length", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { type: "message_start", message: { usage: { input_tokens: 5 } } }, + { + type: "message_delta", + delta: { stop_reason: "model_context_window_exceeded" }, + usage: { output_tokens: 1 }, + }, + ), + ), + ), + ) + + expect(response.finishReason).toEqual({ normalized: "length", raw: "model_context_window_exceeded" }) + }), + ) + + it.effect("preserves pause_turn while normalizing it to stop", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { type: "message_start", message: { usage: { input_tokens: 5 } } }, + { type: "message_delta", delta: { stop_reason: "pause_turn" }, usage: { output_tokens: 1 } }, + ), + ), + ), + ) + + expect(response.finishReason).toEqual({ normalized: "stop", raw: "pause_turn" }) + }), + ) + it.effect("assembles streamed tool call input", () => Effect.gen(function* () { const body = sseEvents( @@ -503,10 +541,16 @@ describe("Anthropic Messages route", () => { providerExecuted: undefined, providerMetadata: undefined, }, - { type: "step-finish", index: 0, reason: "tool-calls", usage, providerMetadata: undefined }, + { + type: "step-finish", + index: 0, + reason: { normalized: "tool-calls", raw: "tool_use" }, + usage, + providerMetadata: undefined, + }, { type: "finish", - reason: "tool-calls", + reason: { normalized: "tool-calls", raw: "tool_use" }, providerMetadata: undefined, usage, }, @@ -674,7 +718,10 @@ describe("Anthropic Messages route", () => { }, }) expect(response.text).toBe("Found it.") - expect(response.events.at(-1)).toMatchObject({ type: "finish", reason: "stop" }) + expect(response.events.at(-1)).toMatchObject({ + type: "finish", + reason: { normalized: "stop", raw: "end_turn" }, + }) }), ) diff --git a/packages/ai/test/provider/bedrock-converse.test.ts b/packages/ai/test/provider/bedrock-converse.test.ts index a7280e09cd9f..809e74a0cb3f 100644 --- a/packages/ai/test/provider/bedrock-converse.test.ts +++ b/packages/ai/test/provider/bedrock-converse.test.ts @@ -290,7 +290,10 @@ describe("Bedrock Converse route", () => { // `metadata` (carries usage). We consolidate them into a single // terminal `finish` event with both. expect(finishes).toHaveLength(1) - expect(finishes[0]).toMatchObject({ type: "finish", reason: "stop" }) + expect(finishes[0]).toMatchObject({ + type: "finish", + reason: { normalized: "stop", raw: "end_turn" }, + }) expect(response.usage).toMatchObject({ inputTokens: 5, outputTokens: 2, @@ -299,6 +302,23 @@ describe("Bedrock Converse route", () => { }), ) + it.effect("maps truncation and malformed output stop reasons", () => + Effect.gen(function* () { + const reasons = [ + ["model_context_window_exceeded", "length"], + ["malformed_model_output", "error"], + ["malformed_tool_use", "error"], + ] as const + + for (const [raw, normalized] of reasons) { + const response = yield* LLMClient.generate(baseRequest).pipe( + Effect.provide(fixedBytes(eventStreamBody(["messageStop", { stopReason: raw }]))), + ) + expect(response.finishReason).toEqual({ normalized, raw }) + } + }), + ) + it.effect("adds cache reads and writes to Bedrock input usage", () => Effect.gen(function* () { const body = eventStreamBody( @@ -362,7 +382,10 @@ describe("Bedrock Converse route", () => { { type: "tool-input-delta", id: "tool_1", name: "lookup", text: '{"query"' }, { type: "tool-input-delta", id: "tool_1", name: "lookup", text: ':"weather"}' }, ]) - expect(response.events.at(-1)).toMatchObject({ type: "finish", reason: "tool-calls" }) + expect(response.events.at(-1)).toMatchObject({ + type: "finish", + reason: { normalized: "tool-calls", raw: "tool_use" }, + }) }), ) @@ -388,7 +411,7 @@ describe("Bedrock Converse route", () => { name: "lookup", raw: '{"query":"partial', }) - expect(response.finishReason).toBe("tool-calls") + expect(response.finishReason).toEqual({ normalized: "tool-calls", raw: "end_turn" }) }), ) diff --git a/packages/ai/test/provider/gemini.test.ts b/packages/ai/test/provider/gemini.test.ts index 50b5dbb1d253..66cfb3482023 100644 --- a/packages/ai/test/provider/gemini.test.ts +++ b/packages/ai/test/provider/gemini.test.ts @@ -373,10 +373,16 @@ describe("Gemini route", () => { { type: "text-delta", id: "text-0", text: "Hello" }, { type: "text-delta", id: "text-0", text: "!" }, { type: "text-end", id: "text-0" }, - { type: "step-finish", index: 0, reason: "stop", usage, providerMetadata: undefined }, + { + type: "step-finish", + index: 0, + reason: { normalized: "stop", raw: "STOP" }, + usage, + providerMetadata: undefined, + }, { type: "finish", - reason: "stop", + reason: { normalized: "stop", raw: "STOP" }, usage, }, ]) @@ -529,10 +535,16 @@ describe("Gemini route", () => { providerExecuted: undefined, providerMetadata: undefined, }, - { type: "step-finish", index: 0, reason: "tool-calls", usage, providerMetadata: undefined }, + { + type: "step-finish", + index: 0, + reason: { normalized: "tool-calls", raw: "STOP" }, + usage, + providerMetadata: undefined, + }, { type: "finish", - reason: "tool-calls", + reason: { normalized: "tool-calls", raw: "STOP" }, usage, }, ]) @@ -571,7 +583,10 @@ describe("Gemini route", () => { }, { type: "tool-call", id: "tool_1", name: "lookup", input: { query: "news" } }, ]) - expect(response.events.at(-1)).toMatchObject({ type: "finish", reason: "tool-calls" }) + expect(response.events.at(-1)).toMatchObject({ + type: "finish", + reason: { normalized: "tool-calls", raw: "STOP" }, + }) }), ) @@ -591,9 +606,41 @@ describe("Gemini route", () => { ) expect(length.events.map((event) => event.type)).toEqual(["step-start", "step-finish", "finish"]) - expect(length.events.at(-1)).toMatchObject({ type: "finish", reason: "length" }) + expect(length.events.at(-1)).toMatchObject({ + type: "finish", + reason: { normalized: "length", raw: "MAX_TOKENS" }, + }) expect(filtered.events.map((event) => event.type)).toEqual(["step-start", "step-finish", "finish"]) - expect(filtered.events.at(-1)).toMatchObject({ type: "finish", reason: "content-filter" }) + expect(filtered.events.at(-1)).toMatchObject({ + type: "finish", + reason: { normalized: "content-filter", raw: "SAFETY" }, + }) + }), + ) + + it.effect("maps current blocking and invalid-output finish reasons", () => + Effect.gen(function* () { + const reasons = [ + ["MODEL_ARMOR", "content-filter"], + ["IMAGE_PROHIBITED_CONTENT", "content-filter"], + ["IMAGE_RECITATION", "content-filter"], + ["LANGUAGE", "content-filter"], + ["UNEXPECTED_TOOL_CALL", "error"], + ["NO_IMAGE", "error"], + ["IMAGE_OTHER", "unknown"], + ["TOO_MANY_TOOL_CALLS", "error"], + ["MISSING_THOUGHT_SIGNATURE", "error"], + ["MALFORMED_RESPONSE", "error"], + ] as const + + for (const [raw, normalized] of reasons) { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse(sseEvents({ candidates: [{ content: { role: "model", parts: [] }, finishReason: raw }] })), + ), + ) + expect(response.finishReason).toEqual({ normalized, raw }) + } }), ) diff --git a/packages/ai/test/provider/openai-chat.test.ts b/packages/ai/test/provider/openai-chat.test.ts index 50bc34df15f8..fb926c030806 100644 --- a/packages/ai/test/provider/openai-chat.test.ts +++ b/packages/ai/test/provider/openai-chat.test.ts @@ -569,10 +569,16 @@ describe("OpenAI Chat route", () => { { type: "text-delta", id: "text-0", text: "Hello" }, { type: "text-delta", id: "text-0", text: "!" }, { type: "text-end", id: "text-0" }, - { type: "step-finish", index: 0, reason: "stop", usage, providerMetadata: undefined }, + { + type: "step-finish", + index: 0, + reason: { normalized: "stop", raw: "stop" }, + usage, + providerMetadata: undefined, + }, { type: "finish", - reason: "stop", + reason: { normalized: "stop", raw: "stop" }, usage, }, ]) @@ -1037,8 +1043,14 @@ describe("OpenAI Chat route", () => { providerExecuted: undefined, providerMetadata: undefined, }, - { type: "step-finish", index: 0, reason: "tool-calls", usage: undefined, providerMetadata: undefined }, - { type: "finish", reason: "tool-calls", usage: undefined }, + { + type: "step-finish", + index: 0, + reason: { normalized: "tool-calls", raw: "tool_calls" }, + usage: undefined, + providerMetadata: undefined, + }, + { type: "finish", reason: { normalized: "tool-calls", raw: "tool_calls" }, usage: undefined }, ]) }), ) diff --git a/packages/ai/test/provider/openai-compatible-chat.test.ts b/packages/ai/test/provider/openai-compatible-chat.test.ts index 43ae283e9f7c..f32b2bc2d9a7 100644 --- a/packages/ai/test/provider/openai-compatible-chat.test.ts +++ b/packages/ai/test/provider/openai-compatible-chat.test.ts @@ -232,7 +232,10 @@ describe("OpenAI-compatible Chat route", () => { expect(response.text).toBe("Hello!") expect(response.usage).toMatchObject({ inputTokens: 5, outputTokens: 2, totalTokens: 7 }) - expect(response.events.at(-1)).toMatchObject({ type: "finish", reason: "stop" }) + expect(response.events.at(-1)).toMatchObject({ + type: "finish", + reason: { normalized: "stop", raw: "stop" }, + }) }), ) }) diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index ffbd3294341c..72a2383b041a 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -856,13 +856,13 @@ describe("OpenAI Responses route", () => { { type: "step-finish", index: 0, - reason: "stop", + reason: { normalized: "stop", raw: undefined }, providerMetadata: { openai: { responseId: "resp_1", serviceTier: "default" } }, usage, }, { type: "finish", - reason: "stop", + reason: { normalized: "stop", raw: undefined }, providerMetadata: { openai: { responseId: "resp_1", serviceTier: "default" } }, usage, }, @@ -887,11 +887,18 @@ describe("OpenAI Responses route", () => { const length = yield* generate({ reason: "max_output_tokens" }) const contentFilter = yield* generate({ reason: "content_filter" }) const unknown = yield* generate({}) - - expect([length.finishReason, contentFilter.finishReason, unknown.finishReason]).toEqual([ - "length", - "content-filter", - "unknown", + const custom = yield* generate({ reason: "provider_limit" }) + + expect([ + length.finishReason, + contentFilter.finishReason, + unknown.finishReason, + custom.finishReason, + ]).toEqual([ + { normalized: "length", raw: "max_output_tokens" }, + { normalized: "content-filter", raw: "content_filter" }, + { normalized: "unknown", raw: undefined }, + { normalized: "unknown", raw: "provider_limit" }, ]) }), ) @@ -946,8 +953,8 @@ describe("OpenAI Responses route", () => { { type: "text-delta", id: "msg_1", text: "Hello" }, { type: "reasoning-end", id: "rs_1" }, { type: "text-end", id: "msg_1" }, - { type: "step-finish", index: 0, reason: "stop" }, - { type: "finish", reason: "stop" }, + { type: "step-finish", index: 0, reason: { normalized: "stop", raw: undefined } }, + { type: "finish", reason: { normalized: "stop", raw: undefined } }, ]) expect(response.events.filter((event) => event.type === "finish")).toHaveLength(1) expect(response.message.content).toEqual([ @@ -1038,8 +1045,8 @@ describe("OpenAI Responses route", () => { id: "rs_1:1", providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } }, }, - { type: "step-finish", index: 0, reason: "stop" }, - { type: "finish", reason: "stop" }, + { type: "step-finish", index: 0, reason: { normalized: "stop", raw: undefined } }, + { type: "finish", reason: { normalized: "stop", raw: undefined } }, ]) }), ) @@ -1422,10 +1429,16 @@ describe("OpenAI Responses route", () => { providerExecuted: undefined, providerMetadata: { openai: { itemId: "item_1" } }, }, - { type: "step-finish", index: 0, reason: "tool-calls", usage, providerMetadata: undefined }, + { + type: "step-finish", + index: 0, + reason: { normalized: "tool-calls", raw: undefined }, + usage, + providerMetadata: undefined, + }, { type: "finish", - reason: "tool-calls", + reason: { normalized: "tool-calls", raw: undefined }, providerMetadata: undefined, usage, }, @@ -1465,7 +1478,7 @@ describe("OpenAI Responses route", () => { name: "lookup", raw: '{"query":"partial', }) - expect(response.finishReason).toBe("tool-calls") + expect(response.finishReason.normalized).toBe("tool-calls") expect(response.events.some(LLMEvent.is.toolCall)).toBeFalse() }), ) @@ -1492,7 +1505,7 @@ describe("OpenAI Responses route", () => { name: "lookup", raw: '{"query":"partial', }) - expect(response.finishReason).toBe("tool-calls") + expect(response.finishReason.normalized).toBe("tool-calls") }), ) diff --git a/packages/ai/test/provider/openrouter.test.ts b/packages/ai/test/provider/openrouter.test.ts index ead7e7ea7c80..b4ac2fe2e526 100644 --- a/packages/ai/test/provider/openrouter.test.ts +++ b/packages/ai/test/provider/openrouter.test.ts @@ -4,6 +4,8 @@ import { LLM, Message } from "../../src" import { LLMClient } from "../../src/route" import * as OpenRouter from "../../src/providers/openrouter" import { it } from "../lib/effect" +import { fixedResponse } from "../lib/http" +import { sseEvents } from "../lib/sse" describe("OpenRouter", () => { it.effect("prepares OpenRouter models through the OpenAI-compatible Chat route", () => @@ -54,6 +56,42 @@ describe("OpenRouter", () => { }), ) + it.effect("preserves the upstream provider finish reason", () => + Effect.gen(function* () { + const model = OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6") + const response = yield* LLMClient.generate(LLM.request({ model, prompt: "Say hello." })).pipe( + Effect.provide( + fixedResponse( + sseEvents({ + choices: [{ delta: { content: "Hello" }, finish_reason: "stop", native_finish_reason: "end_turn" }], + }), + ), + ), + ) + + expect(response.finishReason).toEqual({ normalized: "stop", raw: "end_turn" }) + }), + ) + + it.effect("fails on a mid-stream provider error", () => + Effect.gen(function* () { + const model = OpenRouter.configure({ apiKey: "test-key" }).model("openai/gpt-4o-mini") + const error = yield* LLMClient.generate(LLM.request({ model, prompt: "Say hello." })).pipe( + Effect.provide( + fixedResponse( + sseEvents({ + error: { code: 502, message: "Provider disconnected" }, + }), + ), + ), + Effect.flip, + ) + + expect(error.reason).toMatchObject({ _tag: "ProviderInternal" }) + expect(error.message).toContain("Provider disconnected") + }), + ) + it.effect("preserves manually supplied reasoning details", () => Effect.gen(function* () { const details = [ diff --git a/packages/ai/test/provider/pdf.recorded.test.ts b/packages/ai/test/provider/pdf.recorded.test.ts index c3827a651c1f..1a4c65b73562 100644 --- a/packages/ai/test/provider/pdf.recorded.test.ts +++ b/packages/ai/test/provider/pdf.recorded.test.ts @@ -106,7 +106,7 @@ const readPdfRuntime = Tool.make({ }) const expectCode = (response: LLMResponse) => { - expect(response.finishReason).toBe("stop") + expect(response.finishReason.normalized).toBe("stop") expect(response.text.toUpperCase()).toContain(CODE) } @@ -166,7 +166,7 @@ describe("PDF recorded", () => { tools: { read_pdf: readPdfRuntime }, }).pipe(Stream.runCollect), ) - expect(events.at(-1)).toMatchObject({ type: "finish", reason: "stop" }) + expect(events.at(-1)).toMatchObject({ type: "finish", reason: { normalized: "stop" } }) expect(LLMResponse.text({ events }).toUpperCase()).toContain(CODE) return } diff --git a/packages/ai/test/recorded-scenarios.ts b/packages/ai/test/recorded-scenarios.ts index daf57cae2407..cd762dc4fd9b 100644 --- a/packages/ai/test/recorded-scenarios.ts +++ b/packages/ai/test/recorded-scenarios.ts @@ -125,8 +125,8 @@ const assistantContent = (events: ReadonlyArray) => export const expectFinish = ( events: ReadonlyArray, - reason: Extract["reason"], -) => expect(events.at(-1)).toMatchObject({ type: "finish", reason }) + reason: FinishReason, +) => expect(events.at(-1)).toMatchObject({ type: "finish", reason: { normalized: reason } }) export const expectWeatherToolCall = (response: LLMResponse) => expect(response.toolCalls).toMatchObject([ @@ -136,10 +136,10 @@ export const expectWeatherToolCall = (response: LLMResponse) => export const expectWeatherToolLoop = (events: ReadonlyArray) => { const finishes = events.filter(LLMEvent.is.finish) expect(finishes).toHaveLength(1) - expect(finishes[0]?.reason).toBe("stop") + expect(finishes[0]?.reason.normalized).toBe("stop") const stepFinishes = events.filter(LLMEvent.is.stepFinish) - expect(stepFinishes.map((event) => event.reason)).toEqual(["tool-calls", "stop"]) + expect(stepFinishes.map((event) => event.reason.normalized)).toEqual(["tool-calls", "stop"]) const toolCalls = events.filter(LLMEvent.is.toolCall) expect(toolCalls).toHaveLength(1) @@ -503,7 +503,7 @@ export const eventSummary = (events: ReadonlyArray) => { continue } if (event.type === "finish") { - summary.push({ type: "finish", reason: event.reason, usage: usageSummary(event.usage) }) + summary.push({ type: "finish", reason: event.reason.normalized, usage: usageSummary(event.usage) }) } } return summary.map((item) => Object.fromEntries(Object.entries(item).filter((entry) => entry[1] !== undefined))) diff --git a/packages/ai/test/response.test.ts b/packages/ai/test/response.test.ts index d3402be6c327..f05f8e167e58 100644 --- a/packages/ai/test/response.test.ts +++ b/packages/ai/test/response.test.ts @@ -14,11 +14,11 @@ describe("LLMResponse reducer", () => { LLMEvent.reasoningEnd({ id: "r1", providerMetadata: { anthropic: { signature: "sig" } } }), LLMEvent.textDelta({ id: "t1", text: "Answer" }), LLMEvent.textEnd({ id: "t1" }), - LLMEvent.finish({ reason: "stop", usage: { outputTokens: 5 } }), + LLMEvent.finish({ reason: { normalized: "stop" }, usage: { outputTokens: 5 } }), ] const response = LLMResponse.fromEvents(events) - expect(response?.finishReason).toBe("stop") + expect(response?.finishReason).toEqual({ normalized: "stop" }) expect(response?.usage).toMatchObject({ outputTokens: 5 }) expect(response?.events).toEqual(events) expect(response?.events.map((event) => event.type)).toEqual([ @@ -62,18 +62,26 @@ describe("LLMResponse reducer", () => { test("uses terminal usage when present and keeps prior usage when finish omits it", () => { const withFinishUsage = LLMResponse.fromEvents([ - LLMEvent.stepFinish({ index: 0, reason: "stop", usage: { inputTokens: 3 } }), - LLMEvent.finish({ reason: "stop", usage: { outputTokens: 2 } }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" }, usage: { inputTokens: 3 } }), + LLMEvent.finish({ reason: { normalized: "stop" }, usage: { outputTokens: 2 } }), ]) const withoutFinishUsage = LLMResponse.fromEvents([ - LLMEvent.stepFinish({ index: 0, reason: "stop", usage: { inputTokens: 3 } }), - LLMEvent.finish({ reason: "stop" }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" }, usage: { inputTokens: 3 } }), + LLMEvent.finish({ reason: { normalized: "stop" } }), ]) expect(withFinishUsage?.usage).toMatchObject({ outputTokens: 2 }) expect(withoutFinishUsage?.usage).toMatchObject({ inputTokens: 3 }) }) + test("preserves the raw finish reason", () => { + const response = LLMResponse.fromEvents([ + LLMEvent.finish({ reason: { normalized: "unknown", raw: "provider_limit" } }), + ]) + + expect(response?.finishReason).toEqual({ normalized: "unknown", raw: "provider_limit" }) + }) + test("assembles tool-call content only after the completed tool call event", () => { const pending = reduce([ LLMEvent.toolInputStart({ id: "call_1", name: "lookup" }), @@ -88,7 +96,7 @@ describe("LLMResponse reducer", () => { LLMEvent.toolInputDelta({ id: "call_1", name: "lookup", text: ':"weather"}' }), LLMEvent.toolInputEnd({ id: "call_1", name: "lookup" }), LLMEvent.toolCall({ id: "call_1", name: "lookup", input: { query: "weather" } }), - LLMEvent.finish({ reason: "tool-calls" }), + LLMEvent.finish({ reason: { normalized: "tool-calls" } }), ]) expect(response?.message.content).toEqual([ diff --git a/packages/ai/test/schema.test.ts b/packages/ai/test/schema.test.ts index 3c6628c2e511..4fdada7d1438 100644 --- a/packages/ai/test/schema.test.ts +++ b/packages/ai/test/schema.test.ts @@ -48,8 +48,12 @@ describe("llm schema", () => { }) test("finish constructors accept usage input", () => { - expect(LLMEvent.stepFinish({ index: 0, reason: "stop", usage: { inputTokens: 1 } }).usage).toBeInstanceOf(Usage) - expect(LLMEvent.finish({ reason: "stop", usage: { outputTokens: 2 } }).usage).toBeInstanceOf(Usage) + expect( + LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" }, usage: { inputTokens: 1 } }).usage, + ).toBeInstanceOf(Usage) + expect(LLMEvent.finish({ reason: { normalized: "stop" }, usage: { outputTokens: 2 } }).usage).toBeInstanceOf( + Usage, + ) }) test("content part tagged union exposes guards", () => { diff --git a/packages/core/src/aisdk.ts b/packages/core/src/aisdk.ts index 33a4d670af32..7c9e54b4f757 100644 --- a/packages/core/src/aisdk.ts +++ b/packages/core/src/aisdk.ts @@ -659,12 +659,12 @@ function streamPartEvents( return Effect.succeed([ LLMEvent.stepFinish({ index: state.step++, - reason: finishReason(event.finishReason), + reason: { normalized: finishReason(event.finishReason), raw: event.finishReason.raw }, usage: usage(event.usage), providerMetadata: providerMetadata(event.providerMetadata), }), LLMEvent.finish({ - reason: finishReason(event.finishReason), + reason: { normalized: finishReason(event.finishReason), raw: event.finishReason.raw }, usage: usage(event.usage), providerMetadata: providerMetadata(event.providerMetadata), }), diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts index d3d21fbfc82b..06fedf6fcf45 100644 --- a/packages/core/src/session/runner/publish-llm-event.ts +++ b/packages/core/src/session/runner/publish-llm-event.ts @@ -58,7 +58,7 @@ export const createLLMEventPublisher = (events: Pick["reason"] + readonly finish: Extract["reason"]["normalized"] readonly tokens: ReturnType } | undefined @@ -449,8 +449,8 @@ export const createLLMEventPublisher = (events: Pick }) expect(response.events.some(LLMEvent.is.toolInputEnd)).toBeTrue() expect(response.events.some(LLMEvent.is.toolCall)).toBeFalse() + expect(response.finishReason).toEqual({ normalized: "tool-calls", raw: "tool_calls" }) }), ) diff --git a/packages/core/test/generate.test.ts b/packages/core/test/generate.test.ts index c0ad30d3831c..575b0199fb9d 100644 --- a/packages/core/test/generate.test.ts +++ b/packages/core/test/generate.test.ts @@ -73,7 +73,7 @@ const client = Layer.mock(LLMClient.Service)({ LLMEvent.textStart({ id: "generate" }), LLMEvent.textDelta({ id: "generate", text: "OK" }), LLMEvent.textEnd({ id: "generate" }), - LLMEvent.finish({ reason: "stop" }), + LLMEvent.finish({ reason: { normalized: "stop" } }), ]) if (!response) throw new Error("Incomplete generate response") return response diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts index 40386568b68f..4111ac222fc6 100644 --- a/packages/core/test/session-compaction.test.ts +++ b/packages/core/test/session-compaction.test.ts @@ -49,7 +49,7 @@ const client = Layer.mock(LLMClient.Service)({ LLMEvent.textDelta({ id: "summary", text: "manual summary" }), LLMEvent.stepFinish({ index: 0, - reason: "stop", + reason: { normalized: "stop" }, usage: { inputTokens: 15, outputTokens: 6, @@ -60,7 +60,7 @@ const client = Layer.mock(LLMClient.Service)({ }, }), LLMEvent.finish({ - reason: "stop", + reason: { normalized: "stop" }, }), ) }, diff --git a/packages/core/test/session-generate.test.ts b/packages/core/test/session-generate.test.ts index 7471564610bf..b53dcb826dc9 100644 --- a/packages/core/test/session-generate.test.ts +++ b/packages/core/test/session-generate.test.ts @@ -59,8 +59,8 @@ const client = Layer.mock(LLMClient.Service)({ LLMEvent.textStart({ id: "generate" }), LLMEvent.textDelta({ id: "generate", text: "Transient answer" }), LLMEvent.textEnd({ id: "generate" }), - LLMEvent.stepFinish({ index: 0, reason: "stop", usage: { inputTokens: 100, outputTokens: 10 } }), - LLMEvent.finish({ reason: "stop" }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" }, usage: { inputTokens: 100, outputTokens: 10 } }), + LLMEvent.finish({ reason: { normalized: "stop" } }), ]) if (!response) throw new Error("Incomplete generate response") return response diff --git a/packages/core/test/session-runner-tool-events.test.ts b/packages/core/test/session-runner-tool-events.test.ts index 702999719c3d..4d6dc3c44007 100644 --- a/packages/core/test/session-runner-tool-events.test.ts +++ b/packages/core/test/session-runner-tool-events.test.ts @@ -255,7 +255,7 @@ test("success event data can carry provider-executed result state", () => { test("step finish records settlement without publishing step ended", async () => { const { published, publisher } = capture() await Effect.runPromise(publisher.publish(LLMEvent.stepStart({ index: 0 }))) - await Effect.runPromise(publisher.publish(LLMEvent.stepFinish({ index: 0, reason: "stop" }))) + await Effect.runPromise(publisher.publish(LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } }))) expect(published.some((event) => event.type === "step.ended.2")).toBe(false) expect(publisher.stepSettlement()).toMatchObject({ finish: "stop" }) @@ -268,7 +268,7 @@ test("content-filter finish retains failure evidence until step closeout", async publisher.publish( LLMEvent.stepFinish({ index: 0, - reason: "content-filter", + reason: { normalized: "content-filter" }, usage: { nonCachedInputTokens: 8, outputTokens: 3, @@ -311,7 +311,7 @@ test("content-filter finish preserves partial streamed text and never ends the s LLMEvent.stepStart({ index: 0 }), LLMEvent.textStart({ id: "text" }), LLMEvent.textDelta({ id: "text", text: "Partial" }), - LLMEvent.stepFinish({ index: 0, reason: "content-filter" }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "content-filter" } }), ], (event) => publisher.publish(event), { discard: true }, diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 77046d6d9fff..2b1c386cd4e2 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -119,8 +119,8 @@ const client = Layer.succeed( const reply = { stop: () => [ LLMEvent.stepStart({ index: 0 }), - LLMEvent.stepFinish({ index: 0, reason: "stop" }), - LLMEvent.finish({ reason: "stop" }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } }), + LLMEvent.finish({ reason: { normalized: "stop" } }), ], text: (text: string, id: string) => fragmentFixture("text", id, [text]).completeEvents, textWithUsage: (text: string, id: string, inputTokens: number) => @@ -136,8 +136,8 @@ const reply = { tool: (id: string, name: string, input: unknown) => [ LLMEvent.stepStart({ index: 0 }), LLMEvent.toolCall({ id, name, input }), - LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), - LLMEvent.finish({ reason: "tool-calls" }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "tool-calls" } }), + LLMEvent.finish({ reason: { normalized: "tool-calls" } }), ], } const model = Model.make({ id: "fake-model", provider: "fake", route: OpenAIChat.route }) @@ -682,8 +682,8 @@ const fragmentFixture = (kind: FragmentKind, id: string, chunks: readonly string completeEvents: [ ...partialEvents, LLMEvent.textEnd({ id }), - LLMEvent.stepFinish({ index: 0, reason: "stop" }), - LLMEvent.finish({ reason: "stop" }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } }), + LLMEvent.finish({ reason: { normalized: "stop" } }), ], expectedAssistant: { type: "assistant", finish: "stop", content: [expectedContent] }, expectedContent, @@ -702,8 +702,8 @@ const fragmentFixture = (kind: FragmentKind, id: string, chunks: readonly string completeEvents: [ ...partialEvents, LLMEvent.reasoningEnd({ id }), - LLMEvent.stepFinish({ index: 0, reason: "stop" }), - LLMEvent.finish({ reason: "stop" }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } }), + LLMEvent.finish({ reason: { normalized: "stop" } }), ], expectedAssistant: { type: "assistant", finish: "stop", content: [expectedContent] }, expectedContent, @@ -999,8 +999,8 @@ describe("SessionRunnerLLM", () => { [ LLMEvent.stepStart({ index: 0 }), LLMEvent.toolCall({ id: "call-reloaded", name: "reloaded", input: {} }), - LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), - LLMEvent.finish({ reason: "tool-calls" }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "tool-calls" } }), + LLMEvent.finish({ reason: { normalized: "tool-calls" } }), ], [], ] @@ -2377,7 +2377,7 @@ describe("SessionRunnerLLM", () => { }), LLMEvent.stepFinish({ index: 0, - reason: "tool-calls", + reason: { normalized: "tool-calls" }, usage: { inputTokens: 10, nonCachedInputTokens: 8, @@ -2386,7 +2386,7 @@ describe("SessionRunnerLLM", () => { cacheReadInputTokens: 2, }, }), - LLMEvent.finish({ reason: "tool-calls" }), + LLMEvent.finish({ reason: { normalized: "tool-calls" } }), ] yield* session.resume(sessionID) @@ -2535,8 +2535,8 @@ describe("SessionRunnerLLM", () => { anthropic: { ignored: true }, }, }), - LLMEvent.stepFinish({ index: 0, reason: "stop" }), - LLMEvent.finish({ reason: "stop" }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } }), + LLMEvent.finish({ reason: { normalized: "stop" } }), ] yield* session.resume(sessionID) yield* replaySessionProjection(sessionID) @@ -2600,8 +2600,8 @@ describe("SessionRunnerLLM", () => { providerExecuted: true, providerMetadata: { openai: { blockType: "web_search_tool_result" }, anthropic: { ignored: true } }, }), - LLMEvent.stepFinish({ index: 0, reason: "stop" }), - LLMEvent.finish({ reason: "stop" }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } }), + LLMEvent.finish({ reason: { normalized: "stop" } }), ] yield* session.resume(sessionID) yield* replaySessionProjection(sessionID) @@ -2648,8 +2648,8 @@ describe("SessionRunnerLLM", () => { ), ]) const final = Stream.fromIterable([ - LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), - LLMEvent.finish({ reason: "tool-calls" }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "tool-calls" } }), + LLMEvent.finish({ reason: { normalized: "tool-calls" } }), ]) responseStream = Stream.concat( initial, @@ -3605,7 +3605,7 @@ describe("SessionRunnerLLM", () => { yield* admit(session, "Reject permission") responses = [ reply.tool("call-permission", "permissionfail", {}), - [LLMEvent.stepStart({ index: 0 }), LLMEvent.stepFinish({ index: 0, reason: "stop" })], + [LLMEvent.stepStart({ index: 0 }), LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } })], ] yield* session.resume(sessionID) @@ -3954,10 +3954,10 @@ describe("SessionRunnerLLM", () => { LLMEvent.textDelta({ id: "partial", text: "Partial" }), LLMEvent.stepFinish({ index: 0, - reason: "content-filter", + reason: { normalized: "content-filter" }, usage: { nonCachedInputTokens: 8, outputTokens: 3, reasoningTokens: 1 }, }), - LLMEvent.finish({ reason: "content-filter" }), + LLMEvent.finish({ reason: { normalized: "content-filter" } }), ] expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("Provider blocked the response") @@ -3990,8 +3990,8 @@ describe("SessionRunnerLLM", () => { response = [ LLMEvent.stepStart({ index: 0 }), LLMEvent.toolCall({ id: "call-before-content-filter", name: "echo", input: { text: "settled" } }), - LLMEvent.stepFinish({ index: 0, reason: "content-filter" }), - LLMEvent.finish({ reason: "content-filter" }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "content-filter" } }), + LLMEvent.finish({ reason: { normalized: "content-filter" } }), ] const run = yield* session.resume(sessionID).pipe(Effect.forkChild) @@ -4282,8 +4282,8 @@ describe("SessionRunnerLLM", () => { name: "echo", raw, }), - LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), - LLMEvent.finish({ reason: "tool-calls" }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "tool-calls" } }), + LLMEvent.finish({ reason: { normalized: "tool-calls" } }), ], reply.stop(), ] @@ -4379,8 +4379,8 @@ describe("SessionRunnerLLM", () => { name: "echo", raw: '{"text":"partial', }), - LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), - LLMEvent.finish({ reason: "tool-calls" }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "tool-calls" } }), + LLMEvent.finish({ reason: { normalized: "tool-calls" } }), ], reply.stop(), ] @@ -4421,8 +4421,8 @@ describe("SessionRunnerLLM", () => { name: "echo", raw: '{"text":"partial', }), - LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), - LLMEvent.finish({ reason: "tool-calls" }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "tool-calls" } }), + LLMEvent.finish({ reason: { normalized: "tool-calls" } }), ] const run = yield* session.resume(sessionID).pipe(Effect.forkChild) @@ -4530,8 +4530,8 @@ describe("SessionRunnerLLM", () => { name: "echo", raw: '{"text":"partial', }), - LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), - LLMEvent.finish({ reason: "tool-calls" }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "tool-calls" } }), + LLMEvent.finish({ reason: { normalized: "tool-calls" } }), ] responses = [ malformed("call-first"), @@ -4565,8 +4565,8 @@ describe("SessionRunnerLLM", () => { name: "echo", raw: '{"text":"partial', }), - LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), - LLMEvent.finish({ reason: "tool-calls" }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "tool-calls" } }), + LLMEvent.finish({ reason: { normalized: "tool-calls" } }), ] responses = [malformed("call-first"), malformed("call-at-limit")] @@ -4727,8 +4727,8 @@ describe("SessionRunnerLLM", () => { response = [ LLMEvent.stepStart({ index: 0 }), hostedCall("call-hosted-clean-end", "effect"), - LLMEvent.stepFinish({ index: 0, reason: "stop" }), - LLMEvent.finish({ reason: "stop" }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } }), + LLMEvent.finish({ reason: { normalized: "stop" } }), ] yield* session.resume(sessionID) @@ -4852,8 +4852,8 @@ describe("SessionRunnerLLM", () => { LLMEvent.textStart({ id: "text-2" }), LLMEvent.textDelta({ id: "text-2", text: "Second" }), LLMEvent.textEnd({ id: "text-2" }), - LLMEvent.stepFinish({ index: 0, reason: "stop" }), - LLMEvent.finish({ reason: "stop" }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } }), + LLMEvent.finish({ reason: { normalized: "stop" } }), ] yield* session.resume(sessionID) @@ -4906,8 +4906,8 @@ describe("SessionRunnerLLM", () => { LLMEvent.toolInputDelta({ id: "call-parsed", name: "web_search", text: '{"query":"hello"}' }), LLMEvent.toolInputEnd({ id: "call-parsed", name: "web_search" }), hostedCall("call-parsed", "hello"), - LLMEvent.stepFinish({ index: 0, reason: "stop" }), - LLMEvent.finish({ reason: "stop" }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } }), + LLMEvent.finish({ reason: { normalized: "stop" } }), ] yield* session.resume(sessionID) diff --git a/packages/core/test/session-title.test.ts b/packages/core/test/session-title.test.ts index 3673509ae55f..ce7f7f89bd35 100644 --- a/packages/core/test/session-title.test.ts +++ b/packages/core/test/session-title.test.ts @@ -47,7 +47,7 @@ const client = Layer.mock(LLMClient.Service)({ LLMEvent.textDelta({ id: "title", text: "Generated Title\n" }), LLMEvent.stepFinish({ index: 0, - reason: "stop", + reason: { normalized: "stop" }, usage: { inputTokens: 15, outputTokens: 6, @@ -58,7 +58,7 @@ const client = Layer.mock(LLMClient.Service)({ }, }), LLMEvent.finish({ - reason: "stop", + reason: { normalized: "stop" }, }), ) }, From 8d9727be9f825c17fd40de66c7e13089f7cac37c Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:59:29 -0500 Subject: [PATCH 066/150] fix(core): improve patch errors (#38369) --- packages/core/src/tool/patch.ts | 88 +++++++++----- packages/core/test/patch.test.ts | 44 ++++++- packages/core/test/tool-patch.test.ts | 166 ++++++++++++++++++++++++-- packages/util/src/patch.ts | 38 +++++- 4 files changed, 294 insertions(+), 42 deletions(-) diff --git a/packages/core/src/tool/patch.ts b/packages/core/src/tool/patch.ts index 5e90c3559667..c7307c069ef2 100644 --- a/packages/core/src/tool/patch.ts +++ b/packages/core/src/tool/patch.ts @@ -5,6 +5,7 @@ import { ToolFailure } from "@opencode-ai/ai" import { FileDiff } from "@opencode-ai/schema/file-diff" import { createTwoFilesPatch, diffLines } from "diff" import { Effect, Schema } from "effect" +import { PlatformError } from "effect/PlatformError" import path from "path" import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "../location" @@ -81,12 +82,11 @@ export const Plugin = { output: Output, execute: (input, context) => { const applied: Array = [] - const fail = (path: string, error?: unknown) => { - const prefix = - applied.length === 0 - ? `Unable to apply patch at ${path}` - : `Patch partially applied before failing at ${path}. Applied: ${applied.map((item) => item.resource).join(", ")}` - return new ToolFailure({ message: prefix, error }) + const fail = (operation: string, error: unknown) => { + const completed = applied.map((item) => item.resource).join(", ") + return new ToolFailure({ + message: `${operation}: ${errorMessage(error)}${completed ? `. Completed before failure: ${completed}` : ""}`, + }) } return Effect.gen(function* () { const source = { @@ -101,11 +101,7 @@ export const Plugin = { ), ) if (hunks.length === 0) { - const normalized = input.patchText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim() - if (normalized === "*** Begin Patch\n*** End Patch") { - return yield* new ToolFailure({ message: "patch rejected: empty patch" }) - } - return yield* new ToolFailure({ message: "patch verification failed: no hunks found" }) + return yield* new ToolFailure({ message: "patch rejected: empty patch" }) } const prepared: Prepared[] = [] const targets: Target[] = [] @@ -145,7 +141,7 @@ export const Plugin = { Effect.mapError( (error) => new ToolFailure({ - message: `patch verification failed: ${error instanceof Error ? error.message : String(error)}`, + message: `patch verification failed: Failed to delete ${target.resource}: ${errorMessage(error)}`, }), ), ) @@ -161,7 +157,7 @@ export const Plugin = { Effect.mapError( (error) => new ToolFailure({ - message: `patch verification failed: Failed to read file to update ${target.canonical}: ${error instanceof Error ? error.message : String(error)}`, + message: `patch verification failed: Failed to read file to update ${target.canonical}: ${errorMessage(error)}`, }), ), ) @@ -175,7 +171,7 @@ export const Plugin = { Effect.mapError( (error) => new ToolFailure({ - message: `patch verification failed: Failed to read file to update ${target.canonical}: ${error instanceof Error ? error.message : String(error)}`, + message: `patch verification failed: Failed to read file to update ${target.canonical}: ${errorMessage(error)}`, }), ), ), @@ -184,7 +180,8 @@ export const Plugin = { const before = original.replace(/^\uFEFF/, "") const update = yield* Effect.try({ try: () => Patch.derive(hunk.path, hunk.chunks, original), - catch: (error) => new ToolFailure({ message: `patch verification failed: ${String(error)}` }), + catch: (error) => + new ToolFailure({ message: `patch verification failed: ${errorMessage(error)}` }), }) const moveTarget = hunk.movePath ? resolveTarget(location, hunk.movePath) : undefined if (moveTarget) targets.push(moveTarget) @@ -211,7 +208,13 @@ export const Plugin = { moveTarget, }) if (!moveTarget) updates.set(target.canonical, Patch.joinBom(update.content, update.bom)) - }).pipe(Effect.mapError((error) => (error instanceof ToolFailure ? error : fail(hunk.path, error)))) + }).pipe( + Effect.mapError((error) => + error instanceof ToolFailure + ? error + : new ToolFailure({ message: `Unable to prepare patch at ${hunk.path}`, error }), + ), + ) } const patchFiles = prepared.map(patchFile) @@ -234,12 +237,16 @@ export const Plugin = { (change) => Effect.gen(function* () { if (change.type === "add") { - yield* fs.writeWithDirs( - change.target.canonical, - change.contents.endsWith("\n") || change.contents === "" - ? change.contents - : `${change.contents}\n`, - ) + yield* fs + .writeWithDirs( + change.target.canonical, + change.contents.endsWith("\n") || change.contents === "" + ? change.contents + : `${change.contents}\n`, + ) + .pipe( + Effect.mapError((error) => fail(`Failed to write ${change.target.resource}`, error)), + ) applied.push({ type: change.type, resource: change.target.resource, @@ -248,7 +255,11 @@ export const Plugin = { return } if (change.type === "delete") { - yield* fs.remove(change.target.canonical) + yield* fs + .remove(change.target.canonical) + .pipe( + Effect.mapError((error) => fail(`Failed to delete ${change.target.resource}`, error)), + ) applied.push({ type: change.type, resource: change.target.resource, @@ -257,8 +268,15 @@ export const Plugin = { return } if (change.moveTarget) { - yield* fs.writeWithDirs(change.moveTarget.canonical, change.content) - yield* fs.remove(change.target.canonical) + const moveTarget = change.moveTarget + yield* fs + .writeWithDirs(moveTarget.canonical, change.content) + .pipe(Effect.mapError((error) => fail(`Failed to write ${moveTarget.resource}`, error))) + yield* fs.remove(change.target.canonical).pipe( + Effect.mapError((error) => + fail(`Wrote ${moveTarget.resource} but failed to remove ${change.target.resource}`, error), + ), + ) applied.push({ type: change.type, resource: change.moveTarget.resource, @@ -266,13 +284,15 @@ export const Plugin = { }) return } - yield* fs.writeWithDirs(change.target.canonical, change.content) + yield* fs + .writeWithDirs(change.target.canonical, change.content) + .pipe(Effect.mapError((error) => fail(`Failed to write ${change.target.resource}`, error))) applied.push({ type: change.type, resource: change.target.resource, target: change.target.canonical, }) - }).pipe(Effect.mapError((error) => fail(change.path, error))), + }), { discard: true }, ) return { applied, files: patchFiles } @@ -282,7 +302,11 @@ export const Plugin = { content: toModelOutput(output), metadata: { files: output.files }, })), - Effect.mapError((error) => (error instanceof ToolFailure ? error : fail("patch", error))), + Effect.mapError((error) => + error instanceof ToolFailure + ? error + : new ToolFailure({ message: "Unable to apply patch", error }), + ), ) }, }), @@ -306,6 +330,14 @@ export const Plugin = { }), } +function errorMessage(error: unknown) { + if (error instanceof PlatformError) { + if (error.reason._tag === "NotFound") return "file does not exist" + return error.reason.description ?? error.reason.message + } + return error instanceof Error ? error.message : String(error) +} + function patchFile(change: Prepared): typeof FileDiff.Info.Type { const target = (change.type === "update" ? change.moveTarget : undefined)?.resource ?? change.target.resource const patch = trimDiff( diff --git a/packages/core/test/patch.test.ts b/packages/core/test/patch.test.ts index 18dd3695ce0b..bd75b2664790 100644 --- a/packages/core/test/patch.test.ts +++ b/packages/core/test/patch.test.ts @@ -246,6 +246,35 @@ describe("Patch", () => { ).toBe("line 1\nLINE 2\nline 3\nLINE 4\n") }) + test("appends a pure-addition chunk to a nonempty file", () => { + expect(Patch.derive("update.txt", [{ oldLines: [], newLines: ["added 1", "added 2"] }], "line 1\nline 2\n").content).toBe( + "line 1\nline 2\nadded 1\nadded 2\n", + ) + }) + + test("applies a pure-addition chunk after an earlier replacement", () => { + expect( + Patch.derive( + "update.txt", + [ + { oldLines: [], newLines: ["after-context", "second-line"] }, + { oldLines: ["line1", "line2", "line3"], newLines: ["line1", "line2-replacement"] }, + ], + "line1\nline2\nline3\n", + ).content, + ).toBe("line1\nline2-replacement\nafter-context\nsecond-line\n") + }) + + test("applies a deletion-only update chunk", () => { + expect( + Patch.derive( + "update.txt", + [{ oldLines: ["line1", "line2", "line3"], newLines: ["line1", "line3"] }], + "line1\nline2\nline3\n", + ).content, + ).toBe("line1\nline3\n") + }) + test("updates empty files and adds a trailing newline", () => { expect(Patch.derive("empty.txt", [{ oldLines: [], newLines: ["First line"] }], "").content).toBe("First line\n") expect(Patch.derive("no-newline.txt", [{ oldLines: ["old"], newLines: ["new"] }], "old").content).toBe("new\n") @@ -327,6 +356,12 @@ describe("Patch", () => { ).toThrow("Failed to find expected lines") }) + test("identifies a missing blank line", () => { + expect(() => + Patch.derive("update.txt", [{ oldLines: [""], newLines: ["added"] }], "content\n"), + ).toThrow("Failed to find an expected blank line in update.txt") + }) + test("parses an update without an explicit first chunk header", () => { expect(parse("*** Begin Patch\n*** Update File: file.txt\n import foo\n+bar\n*** End Patch")).toEqual([ { @@ -413,11 +448,14 @@ describe("Patch", () => { test("rejects invalid add and delete lines", () => { expect(() => parse("*** Begin Patch\n*** Add File: file.txt\nbad\n*** End Patch")).toThrow( - "Invalid hunk at line 3: 'bad' is not a valid hunk header", + "Invalid hunk at line 3: Invalid Add File line for 'file.txt': expected a line starting with '+', got 'bad'", ) expect(() => parse("*** Begin Patch\n*** Delete File: file.txt\nbad\n*** End Patch")).toThrow( - "Invalid hunk at line 3: 'bad' is not a valid hunk header", + "Invalid hunk at line 3: Unexpected line after Delete File 'file.txt': 'bad'. Delete hunks do not contain body lines", ) + expect(() => + parse("*** Begin Patch\n*** Delete File: file.txt\n*** Frobnicate File: next.txt\n*** End Patch"), + ).toThrow("Invalid hunk at line 3: '*** Frobnicate File: next.txt' is not a valid hunk header") }) test("rejects an empty update hunk", () => { @@ -478,6 +516,6 @@ describe("Patch", () => { } expect(() => parse("*** Begin Patch\n*** Update File: old.txt\n*** Move to: \n@@\n-old\n+new\n*** End Patch"), - ).toThrow("Invalid hunk at line 3: '*** Move to:' is not a valid hunk header") + ).toThrow("Invalid hunk at line 3: Move destination for 'old.txt' must not be empty") }) }) diff --git a/packages/core/test/tool-patch.test.ts b/packages/core/test/tool-patch.test.ts index cfae81eef86c..64616e7d2ab8 100644 --- a/packages/core/test/tool-patch.test.ts +++ b/packages/core/test/tool-patch.test.ts @@ -2,6 +2,7 @@ import fs from "fs/promises" import path from "path" import { describe, expect } from "bun:test" import { Effect, Exit, Layer, Schema } from "effect" +import { systemError } from "effect/PlatformError" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { FSUtil } from "@opencode-ai/util/fs-util" @@ -28,6 +29,8 @@ const sessionID = SessionV2.ID.make("ses_patch_tool_test") const assertions: PermissionV2.AssertInput[] = [] let denyAction: string | undefined let failRemoveTarget: string | undefined +let failRemoveErrorTarget: string | undefined +let failWriteTarget: string | undefined let readsBeforeEditApproval = 0 let editApproved = false let afterEditApproval = (): Effect.Effect => Effect.void @@ -65,6 +68,8 @@ const reset = () => { assertions.length = 0 denyAction = undefined failRemoveTarget = undefined + failRemoveErrorTarget = undefined + failWriteTarget = undefined readsBeforeEditApproval = 0 editApproved = false afterEditApproval = () => Effect.void @@ -82,8 +87,33 @@ const filesystem = Layer.effect( }).pipe(Effect.andThen(fs.readFile(target))), remove: (target, options) => { if (failRemoveTarget && path.basename(target) === failRemoveTarget) return Effect.die("forced remove failure") + if (failRemoveErrorTarget && path.basename(target) === failRemoveErrorTarget) { + return Effect.fail( + systemError({ + _tag: "Unknown", + module: "FileSystem", + method: "remove", + description: "forced remove failure", + pathOrDescriptor: target, + }), + ) + } return fs.remove(target, options) }, + writeWithDirs: (target, content, mode) => { + if (failWriteTarget && path.basename(target) === failWriteTarget) { + return Effect.fail( + systemError({ + _tag: "Unknown", + module: "FileSystem", + method: "writeWithDirs", + description: "forced write failure", + pathOrDescriptor: target, + }), + ) + } + return fs.writeWithDirs(target, content, mode) + }, }) }), ).pipe(Layer.provide(LayerNode.compile(FSUtil.node))) @@ -302,6 +332,27 @@ describe("PatchTool", () => { ), ) + it.live("moves a file without changing its contents", () => + withTempTool((directory, registry) => + Effect.gen(function* () { + const source = path.join(directory, "old.txt") + const destination = path.join(directory, "moved.txt") + yield* Effect.promise(() => fs.writeFile(source, "same\n")) + expect( + yield* executeTool( + registry, + call("*** Begin Patch\n*** Update File: old.txt\n*** Move to: moved.txt\n@@\n same\n*** End Patch"), + ), + ).toMatchObject({ + status: "completed", + content: [{ type: "text", text: "Success. Updated the following files:\nM moved.txt" }], + }) + expect(yield* exists(source)).toBe(false) + expect(yield* Effect.promise(() => fs.readFile(destination, "utf8"))).toBe("same\n") + }), + ), + ) + it.live("moves a symlink without deleting its target", () => withTempTool((directory, registry) => Effect.gen(function* () { @@ -451,10 +502,17 @@ describe("PatchTool", () => { it.live("rejects an empty patch", () => withTempTool((_directory, registry) => Effect.gen(function* () { - expect(yield* executeTool(registry, call("*** Begin Patch\n*** End Patch"))).toEqual({ - status: "error", - error: { type: "tool.execution", message: "patch rejected: empty patch" }, - }) + for (const patchText of [ + "*** Begin Patch\n*** End Patch", + " *** Begin Patch \n *** End Patch ", + "< { ), ).toMatchObject({ status: "error", - error: { message: expect.stringContaining("Failed to find expected lines") }, + error: { + type: "tool.execution", + message: "patch verification failed: Failed to find expected lines in unchanged.txt:\nmissing", + }, }) expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("line1\nline2\n") }), @@ -569,12 +630,83 @@ describe("PatchTool", () => { ), ) - it.live("rejects a delete when the target file is missing", () => + it.live("identifies a missing delete target", () => withTempTool((_directory, registry) => Effect.gen(function* () { expect( yield* executeTool(registry, call("*** Begin Patch\n*** Delete File: missing.txt\n*** End Patch")), - ).toMatchObject({ status: "error", error: { message: expect.stringContaining("patch verification failed") } }) + ).toEqual({ + status: "error", + error: { + type: "tool.execution", + message: "patch verification failed: Failed to delete missing.txt: file does not exist", + }, + }) + }), + ), + ) + + it.live("reports the failing destination and filesystem error", () => + withTempTool((directory, registry) => + Effect.gen(function* () { + yield* Effect.promise(() => fs.writeFile(path.join(directory, "old.txt"), "before\n")) + failWriteTarget = "new.txt" + expect( + yield* executeTool( + registry, + call("*** Begin Patch\n*** Update File: old.txt\n*** Move to: new.txt\n@@\n-before\n+after\n*** End Patch"), + ), + ).toEqual({ + status: "error", + error: { type: "tool.execution", message: "Failed to write new.txt: forced write failure" }, + }) + expect(yield* Effect.promise(() => fs.readFile(path.join(directory, "old.txt"), "utf8"))).toBe("before\n") + expect(yield* exists(path.join(directory, "new.txt"))).toBe(false) + }), + ), + ) + + it.live("reports the successful prefix and filesystem error", () => + withTempTool((directory, registry) => + Effect.gen(function* () { + failWriteTarget = "second.txt" + expect( + yield* executeTool( + registry, + call("*** Begin Patch\n*** Add File: first.txt\n+first\n*** Add File: second.txt\n+second\n*** End Patch"), + ), + ).toEqual({ + status: "error", + error: { + type: "tool.execution", + message: "Failed to write second.txt: forced write failure. Completed before failure: first.txt", + }, + }) + expect(yield* Effect.promise(() => fs.readFile(path.join(directory, "first.txt"), "utf8"))).toBe("first\n") + expect(yield* exists(path.join(directory, "second.txt"))).toBe(false) + }), + ), + ) + + it.live("reports a destination written before move removal fails", () => + withTempTool((directory, registry) => + Effect.gen(function* () { + yield* Effect.promise(() => fs.writeFile(path.join(directory, "old.txt"), "before\n")) + failRemoveErrorTarget = "old.txt" + expect( + yield* executeTool( + registry, + call("*** Begin Patch\n*** Update File: old.txt\n*** Move to: new.txt\n@@\n-before\n+after\n*** End Patch"), + ), + ).toEqual({ + status: "error", + error: { + type: "tool.execution", + message: "Wrote new.txt but failed to remove old.txt: forced remove failure", + }, + }) + expect(yield* Effect.promise(() => fs.readFile(path.join(directory, "old.txt"), "utf8"))).toBe("before\n") + expect(yield* Effect.promise(() => fs.readFile(path.join(directory, "new.txt"), "utf8"))).toBe("after\n") }), ), ) @@ -628,7 +760,7 @@ describe("PatchTool", () => { registry, call(`*** Begin Patch\n*** Update File: ${target}\n@@\n-before\n+after\n*** End Patch`), ), - ).toMatchObject({ status: "error" }) + ).toMatchObject({ status: "error", error: { type: "permission.rejected" } }) expect(assertions.map((input) => input.action)).toEqual(["external_directory"]) expect(readsBeforeEditApproval).toBe(0) expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("before\n") @@ -645,6 +777,24 @@ describe("PatchTool", () => { ), ) + it.live("preserves edit permission rejection", () => + withTempTool((directory, registry) => + Effect.gen(function* () { + const target = path.join(directory, "target.txt") + yield* Effect.promise(() => fs.writeFile(target, "before\n")) + denyAction = "edit" + expect( + yield* executeTool( + registry, + call("*** Begin Patch\n*** Update File: target.txt\n@@\n-before\n+after\n*** End Patch"), + ), + ).toMatchObject({ status: "error", error: { type: "permission.rejected" } }) + expect(assertions.map((input) => input.action)).toEqual(["edit"]) + expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("before\n") + }), + ), + ) + it.live("treats a sibling path inside the project worktree as internal", () => Effect.acquireUseRelease( Effect.promise(() => tmpdir()), diff --git a/packages/util/src/patch.ts b/packages/util/src/patch.ts index 843e14efa14f..54766c5a61e3 100644 --- a/packages/util/src/patch.ts +++ b/packages/util/src/patch.ts @@ -69,7 +69,7 @@ export function parse(patchText: string): Result.Result, Par } if (header.startsWith("*** Add File: ")) { const path = header.slice("*** Add File: ".length).trim() - const parsed = parseAdd(lines, index + 1, end) + const parsed = parseAdd(lines, index + 1, end, path) if ("error" in parsed) return Result.fail(parsed.error) hunks.push({ type: "add", path, contents: parsed.content }) index = parsed.next @@ -77,6 +77,19 @@ export function parse(patchText: string): Result.Result, Par } if (header.startsWith("*** Delete File: ")) { const path = header.slice("*** Delete File: ".length).trim() + const next = lines[index + 1]?.trim() + if (index + 1 < end && next !== undefined && !isBoundary(next)) { + if (next.startsWith("*** ")) { + return Result.fail(new InvalidHunkError({ line: next, lineNumber: index + 2 })) + } + return Result.fail( + new InvalidHunkError({ + line: next, + lineNumber: index + 2, + reason: `Unexpected line after Delete File '${path}': '${next}'. Delete hunks do not contain body lines`, + }), + ) + } hunks.push({ type: "delete", path }) index++ continue @@ -90,7 +103,13 @@ export function parse(patchText: string): Result.Result, Par if (move === "*** Move to:" || move?.startsWith("*** Move to: ")) { movePath = move.slice("*** Move to: ".length).trim() if (!movePath) { - return Result.fail(new InvalidHunkError({ line: lines[next]!.trim(), lineNumber: next + 1 })) + return Result.fail( + new InvalidHunkError({ + line: lines[next]!.trim(), + lineNumber: next + 1, + reason: `Move destination for '${path}' must not be empty`, + }), + ) } next++ } @@ -126,12 +145,20 @@ function parseAdd( lines: ReadonlyArray, start: number, end: number, + path: string, ): { content: string; next: number } | { error: InvalidHunkError } { const content: string[] = [] let index = start while (index < end && !isBoundary(lines[index]!.trim())) { if (!lines[index]!.startsWith("+")) { - return { error: new InvalidHunkError({ line: lines[index]!.trim(), lineNumber: index + 1 }) } + const line = lines[index]!.trim() + return { + error: new InvalidHunkError({ + line, + lineNumber: index + 1, + reason: `Invalid Add File line for '${path}': expected a line starting with '+', got '${line}'`, + }), + } } content.push(lines[index]!.slice(1)) index++ @@ -303,6 +330,11 @@ function computeReplacements(lines: ReadonlyArray, path: string, chunks: if (newLines.at(-1) === "") newLines = newLines.slice(0, -1) found = seek(lines, oldLines, lineIndex, chunk.endOfFile) } + if (found === -1 && chunk.oldLines.every((line) => line === "")) { + const expected = + chunk.oldLines.length === 1 ? "an expected blank line" : `${chunk.oldLines.length} consecutive blank lines` + throw new Error(`Failed to find ${expected} in ${path}`) + } if (found === -1) throw new Error(`Failed to find expected lines in ${path}:\n${chunk.oldLines.join("\n")}`) replacements.push([found, oldLines.length, newLines]) lineIndex = found + oldLines.length From bb3f4cc3c7ff17edbaf3c808c0546872090a69bd Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 23 Jul 2026 20:56:18 -0400 Subject: [PATCH 067/150] fix(codemode): stabilize catalog ordering (#38588) --- packages/codemode/src/tool-runtime.ts | 20 ++++---- packages/codemode/test/codemode.test.ts | 35 ++++++++++++++ packages/codemode/test/tool-paths.test.ts | 5 +- .../core/test/codemode/instructions.test.ts | 46 ++++++++++++++++++- 4 files changed, 94 insertions(+), 12 deletions(-) diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index 389d3af2760a..053bbee62251 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -21,6 +21,7 @@ import { } from "./values.js" const estimateTokens = (input: string) => Math.max(0, Math.round(input.length / 4)) +const compareText = (left: string, right: string) => (left < right ? -1 : left > right ? 1 : 0) export type Services = ServicesOf @@ -326,12 +327,15 @@ const describeTool = (path: string, tool: Tool): ToolDescription => ({ signature: `${toolExpression(path)}(input: ${inputTypeScript(tool, true)}): Promise<${outputTypeScript(tool, true)}>`, }) +// Discovery bytes are durable instructions, so order only after canonical-path collisions settle. const visibleTools = (tools: Tools) => - flattenTools(toolTrie(tools)).map(({ path, tool }) => ({ - path, - tool, - description: describeTool(path, tool), - })) + flattenTools(toolTrie(tools)) + .sort((left, right) => compareText(left.path, right.path)) + .map(({ path, tool }) => ({ + path, + tool, + description: describeTool(path, tool), + })) export type DiscoveryPlan = { readonly catalog: ReadonlyArray @@ -403,7 +407,7 @@ const makeSearchTool = (searchIndex: ReadonlyArray): Tool => ({ .filter(({ score }) => terms.length === 0 || score > 0) .sort( (left, right) => - right.score - left.score || left.entry.description.path.localeCompare(right.entry.description.path), + right.score - left.score || compareText(left.entry.description.path, right.entry.description.path), ) .map(({ entry }) => entry) const items = ranked.slice(offset, offset + (request.limit ?? defaultSearchLimit)).map(({ description }) => ({ @@ -462,14 +466,14 @@ export const prepare = (tools: Tools, catalogBudget = defaultCatalogBudget group.push(tool) namespaces.set(namespace, group) } - const ordered = [...namespaces].sort(([left], [right]) => left.localeCompare(right)) + const ordered = [...namespaces].sort(([left], [right]) => compareText(left, right)) const selections = ordered.map(([namespace, group]) => ({ namespace, picked: new Set(), queue: [...group].sort( (left, right) => - estimateTokens(catalogLine(left)) - estimateTokens(catalogLine(right)) || left.path.localeCompare(right.path), + estimateTokens(catalogLine(left)) - estimateTokens(catalogLine(right)) || compareText(left.path, right.path), ), })) let used = 0 diff --git a/packages/codemode/test/codemode.test.ts b/packages/codemode/test/codemode.test.ts index 9d5db0ba5837..2d83f4bcb4c8 100644 --- a/packages/codemode/test/codemode.test.ts +++ b/packages/codemode/test/codemode.test.ts @@ -629,6 +629,41 @@ describe("CodeMode public contract", () => { } }) + test("renders equivalent catalogs identically regardless of tool insertion order", () => { + const alpha = Tool.make({ + description: "Alpha tool", + input: Schema.Struct({}), + output: Schema.Void, + execute: () => Effect.void, + }) + const zeta = Tool.make({ + description: "Zeta tool", + input: Schema.Struct({}), + output: Schema.Void, + execute: () => Effect.void, + }) + const first = CodeMode.make({ tools: { zeta: { zeta, alpha }, alpha: { zeta, alpha } } }) + const second = CodeMode.make({ tools: { alpha: { alpha, zeta }, zeta: { alpha, zeta } } }) + + expect(first.catalog()).toStrictEqual(second.catalog()) + expect(first.instructions()).toBe(second.instructions()) + expect(first.catalog().map((tool) => tool.path)).toEqual(["alpha.alpha", "alpha.zeta", "zeta.alpha", "zeta.zeta"]) + + for (const catalogBudget of [0, 10, 20, 40]) { + expect( + CodeMode.make({ + tools: { zeta: { zeta, alpha }, alpha: { zeta, alpha } }, + discovery: { catalogBudget }, + }).instructions(), + ).toBe( + CodeMode.make({ + tools: { alpha: { alpha, zeta }, zeta: { alpha, zeta } }, + discovery: { catalogBudget }, + }).instructions(), + ) + } + }) + test("renders bracket notation for tool names that are not JavaScript identifiers", async () => { const resolveLibrary = Tool.make({ description: "Resolve a library ID", diff --git a/packages/codemode/test/tool-paths.test.ts b/packages/codemode/test/tool-paths.test.ts index 91d23cdee71e..359cb7b9c6b6 100644 --- a/packages/codemode/test/tool-paths.test.ts +++ b/packages/codemode/test/tool-paths.test.ts @@ -106,7 +106,7 @@ describe("blocked member names on tool paths", () => { }) test("tools may use blocked member names because path segments never touch real properties", async () => { - expect(runtime.catalog().map((tool) => tool.path)).toEqual(["prototype", "issues.constructor", "nested.__proto__"]) + expect(runtime.catalog().map((tool) => tool.path)).toEqual(["issues.constructor", "nested.__proto__", "prototype"]) expect(await value(runtime, `return await tools.prototype({})`)).toBe("proto") expect(await value(runtime, `return await tools.issues.constructor({})`)).toBe("ctor") expect(await value(runtime, `return await tools["issues.constructor"]({})`)).toBe("ctor") @@ -155,8 +155,7 @@ describe("canonical path collisions", () => { "issues.close": echo("Close issue", "closed"), }, }) - // Catalog order follows first appearance of each canonical path. - expect(runtime.catalog().map((tool) => tool.path)).toEqual(["issues.list", "issues.get", "issues.close"]) + expect(runtime.catalog().map((tool) => tool.path)).toEqual(["issues.close", "issues.get", "issues.list"]) expect(await value(runtime, `return await tools.issues.list({})`)).toBe("second") expect(await value(runtime, `return await tools.issues.get({})`)).toBe("got") expect(await value(runtime, `return await tools.issues.close({})`)).toBe("closed") diff --git a/packages/core/test/codemode/instructions.test.ts b/packages/core/test/codemode/instructions.test.ts index 172041a11d1e..c47f35766c42 100644 --- a/packages/core/test/codemode/instructions.test.ts +++ b/packages/core/test/codemode/instructions.test.ts @@ -3,13 +3,57 @@ import { AgentV2 } from "@opencode-ai/core/agent" import { CodeMode } from "@opencode-ai/core/codemode" import { CodeModeInstructions } from "@opencode-ai/core/codemode/instructions" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { Effect, Layer } from "effect" +import { Tool } from "@opencode-ai/core/tool/tool" +import { Effect, Layer, Schema } from "effect" import { it } from "../lib/effect" import { readInitial, readUpdate } from "../lib/instructions" const agent = AgentV2.Info.make(AgentV2.Info.empty(AgentV2.ID.make("build"))) describe("CodeModeInstructions", () => { + it.effect("treats equivalent registration orders as an instruction no-op", () => { + const alpha = Tool.make({ + description: "Alpha tool", + input: Schema.Struct({}), + output: Schema.String, + execute: () => Effect.succeed({ output: "alpha" }), + }) + const zeta = Tool.make({ + description: "Zeta tool", + input: Schema.Struct({}), + output: Schema.String, + execute: () => Effect.succeed({ output: "zeta" }), + }) + + const codeModeLayer = AppNodeBuilder.build(CodeMode.node) + const layer = Layer.merge( + codeModeLayer, + AppNodeBuilder.build(CodeModeInstructions.node, [[CodeMode.node, codeModeLayer]]), + ) + + return Effect.gen(function* () { + const codeMode = yield* CodeMode.Service + const instructions = yield* CodeModeInstructions.Service + const initialized = yield* Effect.scoped( + Effect.gen(function* () { + yield* codeMode.register(Tool.registrationEntries({ zeta, alpha }, { namespace: "tools" })) + return yield* instructions.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(readInitial)) + }), + ) + const reordered = yield* Effect.scoped( + Effect.gen(function* () { + yield* codeMode.register(Tool.registrationEntries({ alpha, zeta }, { namespace: "tools" })) + return yield* instructions + .load({ id: agent.id, info: agent }) + .pipe(Effect.flatMap((context) => readUpdate(context, initialized))) + }), + ) + + expect(reordered.changed).toBe(false) + expect(reordered.text).toBe("") + }).pipe(Effect.provide(layer)) + }) + it.effect("renders catalog changes and removal", () => { let catalog: string | undefined = "Initial Code Mode catalog" const layer = AppNodeBuilder.build(CodeModeInstructions.node, [ From c228fc48866f06af74d6c7f579181d03fa74b325 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 23 Jul 2026 21:30:06 -0400 Subject: [PATCH 068/150] fix(core): stabilize tool definition ordering (#38590) --- packages/core/src/tool/registry.ts | 5 +++- packages/core/test/plugin.test.ts | 2 +- .../test/session-runner-tool-registry.test.ts | 29 ++++++++++++++++++- packages/core/test/session-runner.test.ts | 4 +-- 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/packages/core/src/tool/registry.ts b/packages/core/src/tool/registry.ts index 9316f6f8f6ee..6688c7ef01dd 100644 --- a/packages/core/src/tool/registry.ts +++ b/packages/core/src/tool/registry.ts @@ -323,7 +323,10 @@ const registryLayer = Layer.effect( const codemodeTool = (yield* codeMode.materialize(permissions)).tool return { definitions: [ - ...Array.from(direct, ([name, registration]) => toLLMDefinition(name, registration.tool)), + // Definitions are prompt-cache prefix bytes, so order only after effective registrations settle. + ...Array.from(direct) + .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)) + .map(([name, registration]) => toLLMDefinition(name, registration.tool)), ...(codemodeTool ? [toLLMDefinition("execute", codemodeTool)] : []), ], execute: (input: ExecuteInput) => { diff --git a/packages/core/test/plugin.test.ts b/packages/core/test/plugin.test.ts index f643e2a36ed9..ca191a8b56da 100644 --- a/packages/core/test/plugin.test.ts +++ b/packages/core/test/plugin.test.ts @@ -300,8 +300,8 @@ describe("PluginV2", () => { yield* plugins.activate([versioned(plugin)]) expect((yield* registry.snapshot()).definitions.map((tool) => tool.name)).toEqual([ - "plain", "context7_look_up", + "plain", "execute", ]) }), diff --git a/packages/core/test/session-runner-tool-registry.test.ts b/packages/core/test/session-runner-tool-registry.test.ts index 29e1e17c145e..3109509581ad 100644 --- a/packages/core/test/session-runner-tool-registry.test.ts +++ b/packages/core/test/session-runner-tool-registry.test.ts @@ -121,6 +121,33 @@ describe("ToolRegistry", () => { }), ) + it.effect("canonicalizes effective definitions and keeps Code Mode last", () => + Effect.gen(function* () { + const service = yield* ToolRegistry.Service + const tool = make() + const capture = (registrations: Parameters[0]) => + Effect.scoped( + Effect.gen(function* () { + yield* service.registerBatch(registrations) + return (yield* service.snapshot()).definitions + }), + ) + const first = yield* capture([ + { tools: { zeta: tool, alpha: tool }, options: { codemode: false } }, + { tools: { beta: tool }, options: { namespace: "alpha", codemode: false } }, + { tools: { echo: tool } }, + ]) + const second = yield* capture([ + { tools: { echo: tool } }, + { tools: { beta: tool }, options: { namespace: "alpha", codemode: false } }, + { tools: { alpha: tool, zeta: tool }, options: { codemode: false } }, + ]) + + expect(first).toEqual(second) + expect(first.map((definition) => definition.name)).toEqual(["alpha", "alpha_beta", "zeta", "execute"]) + }), + ) + it.effect("filters disabled tools with edit aliases and ordered wildcard precedence", () => Effect.gen(function* () { const service = yield* ToolRegistry.Service @@ -142,7 +169,7 @@ describe("ToolRegistry", () => { { action: "*", resource: "*", effect: "deny" }, ]), ).toEqual([]) - expect(yield* names([{ action: "edit", resource: "*", effect: "deny" }])).toEqual(["question", "bash"]) + expect(yield* names([{ action: "edit", resource: "*", effect: "deny" }])).toEqual(["bash", "question"]) }), ) diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 2b1c386cd4e2..04f5ba0d5f77 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -1099,7 +1099,7 @@ describe("SessionRunnerLLM", () => { expect(requests).toHaveLength(1) expect(requests[0]?.model).toBe(model) - expect(requests[0]?.tools.map((tool) => tool.name)).toEqual(["echo", "defect", "storefail"]) + expect(requests[0]?.tools.map((tool) => tool.name)).toEqual(["defect", "echo", "storefail"]) expect(requests[0]?.messages.map((message) => ({ role: message.role, content: message.content }))).toEqual([ { role: "user", content: [{ type: "text", text: "First" }] }, { role: "user", content: [{ type: "text", text: "Second" }] }, @@ -2392,7 +2392,7 @@ describe("SessionRunnerLLM", () => { yield* session.resume(sessionID) expect(requests).toHaveLength(1) - expect(requests[0]?.tools.map((tool) => tool.name)).toEqual(["echo", "defect", "storefail"]) + expect(requests[0]?.tools.map((tool) => tool.name)).toEqual(["defect", "echo", "storefail"]) expect(yield* session.context(sessionID)).toMatchObject([ { type: "user", text: "Use tools" }, { From 7456598cded90531fbf7268e02ddb4c6ad658a1f Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 23 Jul 2026 22:54:22 -0400 Subject: [PATCH 069/150] fix(core): share one tool snapshot per request (#38596) --- packages/core/src/codemode/instructions.ts | 57 ++++++------------- packages/core/src/location-services.ts | 2 - packages/core/src/session/context.ts | 54 ++++++++++++------ packages/core/src/session/generate-node.ts | 14 +---- packages/core/src/session/model-request.ts | 5 +- packages/core/src/tool/registry.ts | 13 +++-- .../core/test/codemode/instructions.test.ts | 47 ++++----------- packages/core/test/location-layer.test.ts | 7 ++- packages/core/test/session-generate.test.ts | 16 +++--- .../test/session-runner-tool-registry.test.ts | 1 + packages/core/test/session-runner.test.ts | 50 ++++++++++++++++ 11 files changed, 142 insertions(+), 124 deletions(-) diff --git a/packages/core/src/codemode/instructions.ts b/packages/core/src/codemode/instructions.ts index 95e9f5338dcb..0934a4a953a8 100644 --- a/packages/core/src/codemode/instructions.ts +++ b/packages/core/src/codemode/instructions.ts @@ -1,45 +1,24 @@ export * as CodeModeInstructions from "./instructions" -import { Context, Effect, Layer, Schema } from "effect" -import { makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { AgentV2 } from "../agent" -import { CodeMode } from "../codemode" +import { Effect, Schema } from "effect" import { Instructions } from "../instructions/index" -export interface Interface { - readonly load: (agent: AgentV2.Selection) => Effect.Effect +const key = Instructions.Key.make("core/codemode") +const codec = Schema.toCodecJson(Schema.String) +const render = { + initial: (current: string) => current, + changed: (_previous: string, current: string) => + [ + "The Code Mode tool catalog has changed. This catalog supersedes the previous Code Mode tool catalog.", + current, + ].join("\n\n"), + removed: () => "Code Mode tools are no longer available. Do not use any previously listed Code Mode tools.", } -export class Service extends Context.Service()("@opencode/v2/CodeModeInstructions") {} - -const layer = Layer.effect( - Service, - Effect.gen(function* () { - const codeMode = yield* CodeMode.Service - - return Service.of({ - load: Effect.fn("CodeModeInstructions.load")(function* (selection) { - const instructions = selection.info - ? (yield* codeMode.materialize(selection.info.permissions)).instructions - : undefined - return Instructions.make({ - key: Instructions.Key.make("core/codemode"), - codec: Schema.toCodecJson(Schema.String), - read: Effect.succeed(instructions ?? Instructions.removed), - render: { - initial: (current) => current, - changed: (_previous, current) => - [ - "The Code Mode tool catalog has changed. This catalog supersedes the previous Code Mode tool catalog.", - current, - ].join("\n\n"), - removed: () => - "Code Mode tools are no longer available. Do not use any previously listed Code Mode tools.", - }, - }) - }), - }) - }), -) - -export const node = makeLocationNode({ service: Service, layer, deps: [CodeMode.node] }) +export const make = (content?: string): Instructions.Instructions => + Instructions.make({ + key, + codec, + read: Effect.succeed(content ?? Instructions.removed), + render, + }) diff --git a/packages/core/src/location-services.ts b/packages/core/src/location-services.ts index adf1afe62bea..5208f6a7cc51 100644 --- a/packages/core/src/location-services.ts +++ b/packages/core/src/location-services.ts @@ -3,7 +3,6 @@ import { AgentV2 } from "./agent" import { AISDK } from "./aisdk" import { Catalog } from "./catalog" import { CodeMode } from "./codemode" -import { CodeModeInstructions } from "./codemode/instructions" import { CommandV2 } from "./command" import { Config } from "./config" import { LayerNode } from "@opencode-ai/util/effect/layer-node" @@ -82,7 +81,6 @@ const locationServiceNodes = [ ToolRegistry.toolsNode, Image.node, SkillInstructions.node, - CodeModeInstructions.node, ReferenceInstructions.node, InstructionEntry.node, Form.node, diff --git a/packages/core/src/session/context.ts b/packages/core/src/session/context.ts index d025ca8dce28..be3d9bb08f86 100644 --- a/packages/core/src/session/context.ts +++ b/packages/core/src/session/context.ts @@ -2,6 +2,7 @@ export * as SessionContext from "./context" import { Context, Effect, Layer } from "effect" import { AgentV2 } from "../agent" +import { CodeModeInstructions } from "../codemode/instructions" import { Database } from "../database/database" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { InstructionDiscovery } from "../instruction-discovery" @@ -12,7 +13,7 @@ import { McpInstructions } from "../mcp/instructions" import { PluginSupervisor } from "../plugin/supervisor" import { ReferenceInstructions } from "../reference/instructions" import { SkillInstructions } from "../skill/instructions" -import { CodeModeInstructions } from "../codemode/instructions" +import { ToolRegistry } from "../tool/registry" import { AgentNotFoundError } from "./error" import { SessionHistory } from "./history" import { InstructionEntry } from "./instruction-entry" @@ -25,6 +26,7 @@ export interface Selection { readonly session: SessionSchema.Info readonly agent: AgentV2.Selection & { readonly info: AgentV2.Info } readonly instructions: Instructions.Instructions + readonly toolSet: ToolRegistry.ToolSet } export interface Loaded { @@ -33,15 +35,17 @@ export interface Loaded { readonly model: SessionRunnerModel.Resolved readonly initial: string readonly messages: ReadonlyArray + readonly toolSet: ToolRegistry.ToolSet } /** * Resolves model-request state in two phases: `select` fixes the Session, - * agent, and instruction sources; `load` adds the model and active history for - * that selection. This module does not build or execute the model request. + * agent, instruction sources, and tool snapshot; `load` adds the model and + * active history for that selection. This module does not build or execute the + * model request. */ export interface Interface { - /** Selects the Session, agent, and instruction sources used by subsequent work. */ + /** Selects the Session, agent, instructions, and tools used by subsequent work. */ readonly select: (sessionID: SessionSchema.ID) => Effect.Effect /** Resolves the model and active history for that selection. */ readonly load: (selection: Selection) => Effect.Effect @@ -55,7 +59,6 @@ const layer = Layer.effect( Effect.gen(function* () { const agents = yield* AgentV2.Service const builtins = yield* InstructionBuiltIns.Service - const codeModeInstructions = yield* CodeModeInstructions.Service const db = (yield* Database.Service).db const discovery = yield* InstructionDiscovery.Service const entries = yield* InstructionEntry.Service @@ -66,6 +69,7 @@ const layer = Layer.effect( const referenceInstructions = yield* ReferenceInstructions.Service const skillInstructions = yield* SkillInstructions.Service const store = yield* SessionStore.Service + const registry = yield* ToolRegistry.Service const select = Effect.fn("SessionContext.select")(function* (sessionID: SessionSchema.ID) { const session = yield* store.get(sessionID) @@ -76,19 +80,32 @@ const layer = Layer.effect( yield* plugins.flush const agent = yield* agents.select(session.agent) if (!agent.info) return yield* new AgentNotFoundError({ sessionID: session.id, agent: session.agent ?? agent.id }) - const instructions = yield* Effect.all( - [ - builtins.load(sessionID), - codeModeInstructions.load(agent), - discovery.load(), - skillInstructions.load(agent), - referenceInstructions.load(), - mcpInstructions.load(agent), - entries.load(sessionID), - ], + const loaded = yield* Effect.all( + { + toolSet: registry.snapshot(agent.info.permissions), + builtins: builtins.load(sessionID), + discovery: discovery.load(), + skills: skillInstructions.load(agent), + references: referenceInstructions.load(), + mcp: mcpInstructions.load(agent), + entries: entries.load(sessionID), + }, { concurrency: "unbounded" }, - ).pipe(Effect.map(Instructions.combine)) - return { session, agent: { ...agent, info: agent.info }, instructions } + ) + return { + session, + agent: { ...agent, info: agent.info }, + instructions: Instructions.combine([ + loaded.builtins, + CodeModeInstructions.make(loaded.toolSet.codeModeInstructions), + loaded.discovery, + loaded.skills, + loaded.references, + loaded.mcp, + loaded.entries, + ]), + toolSet: loaded.toolSet, + } }) const load = Effect.fn("SessionContext.load")(function* (selection: Selection) { @@ -100,6 +117,7 @@ const layer = Layer.effect( model, initial: history.initial, messages: history.entries.map((entry) => entry.message), + toolSet: selection.toolSet, } }) @@ -112,7 +130,6 @@ export const node = makeLocationNode({ layer, deps: [ AgentV2.node, - CodeModeInstructions.node, Database.node, InstructionBuiltIns.node, InstructionDiscovery.node, @@ -124,5 +141,6 @@ export const node = makeLocationNode({ SessionRunnerModel.node, SessionStore.node, SkillInstructions.node, + ToolRegistry.node, ], }) diff --git a/packages/core/src/session/generate-node.ts b/packages/core/src/session/generate-node.ts index 0158760c3bf1..e1b81c22b914 100644 --- a/packages/core/src/session/generate-node.ts +++ b/packages/core/src/session/generate-node.ts @@ -12,7 +12,6 @@ import { SessionGenerate } from "./generate" import { SessionHistory } from "./history" import { SessionModelHeaders } from "./model-headers" import { SessionRunnerModel } from "./runner/model" -import { ToolRegistry } from "../tool/registry" import PROMPT_DEFAULT from "./runner/prompt/base.txt" import { toLLMMessages } from "./runner/to-llm-message" @@ -24,7 +23,6 @@ export const layer = Layer.effect( const hooks = yield* PluginHooks.Service const llm = yield* LLMClient.Service const models = yield* SessionRunnerModel.Service - const registry = yield* ToolRegistry.Service const app = yield* App.Metadata return SessionGenerate.Service.of({ @@ -36,7 +34,7 @@ export const layer = Layer.effect( const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(selection.session.id) ? selection.session.id.slice(4) : selection.session.id - const toolSet = yield* registry.snapshot(selection.agent.info.permissions) + const toolSet = selection.toolSet const toolDefinitions = toolSet.definitions const toolsByName = new Map(toolDefinitions.map((tool) => [tool.name, tool])) const contextEvent = yield* hooks.trigger("session", "context", { @@ -89,13 +87,5 @@ export const layer = Layer.effect( export const node = makeLocationNode({ service: SessionGenerate.Service, layer, - deps: [ - SessionContext.node, - Database.node, - PluginHooks.node, - SessionRunnerModel.node, - ToolRegistry.node, - App.node, - llmClient, - ], + deps: [SessionContext.node, Database.node, PluginHooks.node, SessionRunnerModel.node, App.node, llmClient], }) diff --git a/packages/core/src/session/model-request.ts b/packages/core/src/session/model-request.ts index f7fcb95adb00..af9ac71a8646 100644 --- a/packages/core/src/session/model-request.ts +++ b/packages/core/src/session/model-request.ts @@ -86,7 +86,6 @@ export const layer = Layer.effect( Service, Effect.gen(function* () { const hooks = yield* PluginHooks.Service - const registry = yield* ToolRegistry.Service const app = yield* App.Metadata const prepare = Effect.fn("SessionModelRequest.prepare")(function* (input: PrepareInput) { @@ -98,7 +97,7 @@ export const layer = Layer.effect( const stepLimitReached = agent.info.steps !== undefined && input.step >= agent.info.steps // The final Step keeps definitions available to protocols with native "none", // preserving their prompt cache prefix. Calls are still rejected at execution. - const toolSet = yield* registry.snapshot(agent.info.permissions) + const toolSet = input.context.toolSet const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id const system = [agent.info.system ? agent.info.system : PROMPT_DEFAULT, input.context.initial] .filter((part) => part.length > 0) @@ -162,5 +161,5 @@ export const layer = Layer.effect( export const node = makeLocationNode({ service: Service, layer, - deps: [PluginHooks.node, ToolRegistry.node, App.node], + deps: [PluginHooks.node, App.node], }) diff --git a/packages/core/src/tool/registry.ts b/packages/core/src/tool/registry.ts index 6688c7ef01dd..c572541a948e 100644 --- a/packages/core/src/tool/registry.ts +++ b/packages/core/src/tool/registry.ts @@ -44,12 +44,13 @@ export interface Interface { } /** - * One request-scoped snapshot pairing advertised definitions with captured - * tools. A model request executes exactly the tool values it advertised - * even if registration changes while the request is in flight. + * One request-scoped snapshot pairing Code Mode instructions and advertised + * definitions with captured tools. A model request executes exactly the tool + * values it advertised even if registration changes while it is in flight. */ export interface ToolSet { readonly definitions: ReadonlyArray + readonly codeModeInstructions?: string readonly execute: (input: ExecuteInput) => Effect.Effect } @@ -320,8 +321,12 @@ const registryLayer = Layer.effect( if (whollyDisabled(registration.permission, rules)) continue direct.set(name, registration) } - const codemodeTool = (yield* codeMode.materialize(permissions)).tool + const codeModeMaterialization = yield* codeMode.materialize(permissions) + const codemodeTool = codeModeMaterialization.tool return { + ...(codeModeMaterialization.instructions === undefined + ? {} + : { codeModeInstructions: codeModeMaterialization.instructions }), definitions: [ // Definitions are prompt-cache prefix bytes, so order only after effective registrations settle. ...Array.from(direct) diff --git a/packages/core/test/codemode/instructions.test.ts b/packages/core/test/codemode/instructions.test.ts index c47f35766c42..6c8bc7907691 100644 --- a/packages/core/test/codemode/instructions.test.ts +++ b/packages/core/test/codemode/instructions.test.ts @@ -1,15 +1,12 @@ import { describe, expect } from "bun:test" -import { AgentV2 } from "@opencode-ai/core/agent" import { CodeMode } from "@opencode-ai/core/codemode" import { CodeModeInstructions } from "@opencode-ai/core/codemode/instructions" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Tool } from "@opencode-ai/core/tool/tool" -import { Effect, Layer, Schema } from "effect" +import { Effect, Schema } from "effect" import { it } from "../lib/effect" import { readInitial, readUpdate } from "../lib/instructions" -const agent = AgentV2.Info.make(AgentV2.Info.empty(AgentV2.ID.make("build"))) - describe("CodeModeInstructions", () => { it.effect("treats equivalent registration orders as an instruction no-op", () => { const alpha = Tool.make({ @@ -24,70 +21,46 @@ describe("CodeModeInstructions", () => { output: Schema.String, execute: () => Effect.succeed({ output: "zeta" }), }) - const codeModeLayer = AppNodeBuilder.build(CodeMode.node) - const layer = Layer.merge( - codeModeLayer, - AppNodeBuilder.build(CodeModeInstructions.node, [[CodeMode.node, codeModeLayer]]), - ) return Effect.gen(function* () { const codeMode = yield* CodeMode.Service - const instructions = yield* CodeModeInstructions.Service const initialized = yield* Effect.scoped( Effect.gen(function* () { yield* codeMode.register(Tool.registrationEntries({ zeta, alpha }, { namespace: "tools" })) - return yield* instructions.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(readInitial)) + const materialization = yield* codeMode.materialize() + return yield* readInitial(CodeModeInstructions.make(materialization.instructions)) }), ) const reordered = yield* Effect.scoped( Effect.gen(function* () { yield* codeMode.register(Tool.registrationEntries({ alpha, zeta }, { namespace: "tools" })) - return yield* instructions - .load({ id: agent.id, info: agent }) - .pipe(Effect.flatMap((context) => readUpdate(context, initialized))) + const materialization = yield* codeMode.materialize() + return yield* readUpdate(CodeModeInstructions.make(materialization.instructions), initialized) }), ) expect(reordered.changed).toBe(false) expect(reordered.text).toBe("") - }).pipe(Effect.provide(layer)) + }).pipe(Effect.provide(codeModeLayer)) }) it.effect("renders catalog changes and removal", () => { let catalog: string | undefined = "Initial Code Mode catalog" - const layer = AppNodeBuilder.build(CodeModeInstructions.node, [ - [ - CodeMode.node, - Layer.mock(CodeMode.Service, { - materialize: () => Effect.succeed({ ...(catalog === undefined ? {} : { instructions: catalog }) }), - register: () => Effect.void, - }), - ], - ]) return Effect.gen(function* () { - const instructions = yield* CodeModeInstructions.Service - const initialized = yield* instructions.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(readInitial)) + const initialized = yield* readInitial(CodeModeInstructions.make(catalog)) expect(initialized.text).toBe("Initial Code Mode catalog") catalog = "Updated Code Mode catalog" - expect( - yield* instructions - .load({ id: agent.id, info: agent }) - .pipe(Effect.flatMap((context) => readUpdate(context, initialized))), - ).toMatchObject({ + expect(yield* readUpdate(CodeModeInstructions.make(catalog), initialized)).toMatchObject({ text: "The Code Mode tool catalog has changed. This catalog supersedes the previous Code Mode tool catalog.\n\nUpdated Code Mode catalog", }) catalog = undefined - expect( - yield* instructions - .load({ id: agent.id, info: agent }) - .pipe(Effect.flatMap((context) => readUpdate(context, initialized))), - ).toMatchObject({ + expect(yield* readUpdate(CodeModeInstructions.make(catalog), initialized)).toMatchObject({ text: "Code Mode tools are no longer available. Do not use any previously listed Code Mode tools.", }) - }).pipe(Effect.provide(layer)) + }) }) }) diff --git a/packages/core/test/location-layer.test.ts b/packages/core/test/location-layer.test.ts index 44a572f138d5..1a198371ece1 100644 --- a/packages/core/test/location-layer.test.ts +++ b/packages/core/test/location-layer.test.ts @@ -578,7 +578,8 @@ describe("LocationServiceMap", () => { const blockedState = yield* update(blocked.path, blockedID) expect(blockedState.providers.some((provider) => provider.id === blockedID)).toBe(true) expect(blockedState.providers.some((provider) => provider.id === allowedID)).toBe(false) - expect(blockedState.tools.map((tool) => tool.name).sort()).toEqual([ + const blockedTools = blockedState.tools.map((tool) => tool.name) + expect(blockedTools.filter((name) => name !== "execute").sort()).toEqual([ "edit", "glob", "grep", @@ -595,7 +596,9 @@ describe("LocationServiceMap", () => { const allowedState = yield* update(allowed.path, allowedID) expect(allowedState.providers.some((provider) => provider.id === allowedID)).toBe(true) expect(allowedState.providers.some((provider) => provider.id === blockedID)).toBe(false) - expect(allowedState.tools.map((tool) => tool.name).sort()).toEqual([ + const allowedTools = allowedState.tools.map((tool) => tool.name) + expect(blockedTools.includes("execute")).toBe(allowedTools.includes("execute")) + expect(allowedTools.filter((name) => name !== "execute").sort()).toEqual([ "edit", "glob", "grep", diff --git a/packages/core/test/session-generate.test.ts b/packages/core/test/session-generate.test.ts index b53dcb826dc9..20b0ca07fe36 100644 --- a/packages/core/test/session-generate.test.ts +++ b/packages/core/test/session-generate.test.ts @@ -97,6 +97,7 @@ const plugins = Layer.mock(PluginSupervisor.Service, { flush: Effect.void }) const tools = Layer.mock(ToolRegistry.Service, { snapshot: () => Effect.succeed({ + codeModeInstructions: "Captured Code Mode catalog", definitions: [ToolDefinition.make({ name: "lookup", description: "Lookup", inputSchema: { type: "object" } })], execute: () => Effect.die(new Error("unused")), }), @@ -285,13 +286,14 @@ it.effect("generates from fresh settled Session context without durable mutation expect(requests[0]?.system.map((part) => part.text)).toContain("Initial context") expect(requests[0]?.http?.headers).toMatchObject({ "X-Session-Id": sessionID }) expect(requests[0]?.providerOptions).toMatchObject({ openai: { promptCacheKey: sessionID } }) - expect( - requests[0]?.messages.flatMap((message) => - message.role === "system" - ? message.content.flatMap((content) => (content.type === "text" ? [content.text] : [])) - : [], - ), - ).toEqual(["Changed context"]) + const instructionUpdates = requests[0]?.messages.flatMap((message) => + message.role === "system" + ? message.content.flatMap((content) => (content.type === "text" ? [content.text] : [])) + : [], + ) + expect(instructionUpdates).toHaveLength(1) + expect(instructionUpdates?.[0]).toContain("Changed context") + expect(instructionUpdates?.[0]).toContain("Captured Code Mode catalog") expect(userTexts(requests[0])).toEqual(["Existing durable context", "Summarize privately"]) expect( requests[0]?.messages.flatMap((message) => diff --git a/packages/core/test/session-runner-tool-registry.test.ts b/packages/core/test/session-runner-tool-registry.test.ts index 3109509581ad..7ba1322002d3 100644 --- a/packages/core/test/session-runner-tool-registry.test.ts +++ b/packages/core/test/session-runner-tool-registry.test.ts @@ -533,6 +533,7 @@ describe("ToolRegistry", () => { .pipe(Scope.provide(scope)) const toolSet = yield* service.snapshot() const execute = toolSet.definitions.find((tool) => tool.name === "execute") + expect(toolSet.codeModeInstructions).toContain("tools.echo") expect(execute?.description).toContain("confined Code Mode runtime") expect(execute?.description).not.toContain("Echo text") yield* Scope.close(scope, Exit.void) diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 04f5ba0d5f77..a96e08eb5452 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -43,6 +43,7 @@ import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm" import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" import { SessionUsage } from "@opencode-ai/core/session/usage" import { ToolRegistry } from "@opencode-ai/core/tool/registry" +import { CodeMode } from "@opencode-ai/core/codemode" import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" import { PluginHooks } from "@opencode-ai/core/plugin/hooks" import { SystemPromptPlugin } from "@opencode-ai/core/plugin/system-prompt" @@ -368,6 +369,12 @@ const pluginSupervisor = Layer.succeed( flush: Effect.suspend(() => pluginFlushHook), }), ) +let codeModeMaterializations: ReadonlyArray = [] +let codeModeMaterializationCount = 0 +const codeMode = Layer.mock(CodeMode.Service, { + register: () => Effect.void, + materialize: () => Effect.sync(() => codeModeMaterializations[codeModeMaterializationCount++] ?? {}), +}) const promptCatalog = Layer.mock(Catalog.Service, { provider: { get: () => Effect.succeed(undefined), @@ -405,6 +412,7 @@ const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [ [McpInstructions.node, mcpInstructions], [ToolOutputStore.node, toolOutputStore], [PluginSupervisor.node, pluginSupervisor], + [CodeMode.node, codeMode], ]) const execution = Layer.effect( SessionExecution.Service, @@ -464,6 +472,7 @@ const it = testEffect( [Config.node, config], [ToolOutputStore.node, toolOutputStore], [PluginSupervisor.node, pluginSupervisor], + [CodeMode.node, codeMode], ], ), ) @@ -512,6 +521,8 @@ const setup = Effect.gen(function* () { systemLoadHook = Effect.void modelResolveHook = Effect.void pluginFlushHook = Effect.void + codeModeMaterializations = [] + codeModeMaterializationCount = 0 currentModel = model skillBaselines.clear() responses = undefined @@ -823,6 +834,45 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) => }) describe("SessionRunnerLLM", () => { + it.effect("uses one Code Mode materialization per request for instructions and execution", () => + Effect.gen(function* () { + const executed: string[] = [] + const execute = (name: string) => + Tool.make({ + description: `Execute ${name}`, + input: Schema.Struct({}), + output: Schema.String, + execute: () => Effect.sync(() => executed.push(name)).pipe(Effect.as({ output: name })), + }) + const session = yield* setup + codeModeMaterializations = [ + { instructions: "Code Mode catalog A", tool: execute("A") }, + { instructions: "Code Mode catalog B", tool: execute("B") }, + { instructions: "Code Mode catalog C", tool: execute("C") }, + { instructions: "Code Mode catalog D", tool: execute("D") }, + ] + yield* admit(session, "Use Code Mode") + responses = [reply.tool("call-execute", "execute", {}), reply.stop()] + + yield* session.resume(sessionID) + + expect(requests).toHaveLength(2) + expect(codeModeMaterializationCount).toBe(2) + expect(requests[0]?.system.some((part) => part.text.includes("Code Mode catalog A"))).toBe(true) + expect(requests[0]?.system.some((part) => part.text.includes("Code Mode catalog B"))).toBe(false) + expect(requests[0]?.tools.find((tool) => tool.name === "execute")?.description).toBe("Execute A") + expect(executed).toEqual(["A"]) + expect(requests[1]?.tools.find((tool) => tool.name === "execute")?.description).toBe("Execute B") + expect( + requests[1]?.messages.some( + (message) => + message.role === "system" && + message.content.some((part) => part.type === "text" && part.text.includes("Code Mode catalog B")), + ), + ).toBe(true) + }), + ) + it.effect("applies session context hooks without exposing unavailable tools", () => Effect.gen(function* () { const session = yield* setup From ee69a91f2633b6da419d73f97565a85ac09ba4af Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 23:02:44 -0500 Subject: [PATCH 070/150] docs: add ideal pseudocode skill (#38611) Co-authored-by: Aiden Cline --- .opencode/skills/ideal-pseudocode/SKILL.md | 68 ++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .opencode/skills/ideal-pseudocode/SKILL.md diff --git a/.opencode/skills/ideal-pseudocode/SKILL.md b/.opencode/skills/ideal-pseudocode/SKILL.md new file mode 100644 index 000000000000..d75ae08a05c1 --- /dev/null +++ b/.opencode/skills/ideal-pseudocode/SKILL.md @@ -0,0 +1,68 @@ +--- +name: ideal-pseudocode +description: Function-by-function refactoring loop driven by ideal pseudocode. Use when the user says "ideal pseudocode", asks to make a function read like its pseudocode, or wants a dense module cleaned up one function at a time. +--- + +# Ideal Pseudocode + +Clean up one function at a time by writing the pseudocode it _should_ read as, naming every delta between that and the real code, and closing only the gaps the user approves. + +## Loop + +One function per round. Never touch code before the user picks a direction. + +1. **Pick the target** with the user — usually the next function up or down the call chain from the last round. +2. **Read the current code** fresh from disk. It may have unsaved or parallel edits; ask before overwriting anything unexpected. +3. **Distill.** Write the function's ideal pseudocode in a `ts`-fenced code block — TypeScript-flavored for syntax highlighting, but pseudocode: comments over mechanics, one line per idea, every arm of a loop visible as an arm. For a dense or unfamiliar function, first show the _current_ structure as pseudocode, then the ideal. +4. **Name the deltas.** A numbered list; each delta is one concrete gap: control flow smuggled through combinators, a flag-typed parameter, colliding names, side-effect plumbing drowning a loop body, twin functions differing only by a filter. For each, give an honest cost/benefit and a decisive recommendation — including "leave it" when the density has reasons (state the reasons). +5. **Verify before proposing.** Any API the proposal builds on (a combinator, schedule, matcher) gets checked against the installed version's source, not memory. +6. **Get buy-in, then apply.** Minimal edits. Quarantine irreducible ugliness inside a small helper whose name is the pseudocode line it implements. Collapse shallow sibling functions into one deep function only when the distinguishing argument is a domain noun, not a behavior switch. +7. **Validate.** Typecheck and the focused test suites from the package directory; rerun timing-sensitive tests repeatedly. Flag any semantic change the restructure introduced, even when tests pass. +8. **Offer the next target** and stop. + +## Style + +- Keep each round tight: pseudocode block, deltas, recommendation, one question. +- State mutation belongs in one visible place (usually the loop tail), never spread across error channels or callback closures. +- Prefer early return and `if` chains for control flow; matchers are for producing values. +- Compare names against the question each caller is actually asking; rename toward the domain noun. + +## Example round + +Every round should read like this (abridged from a real one, on an Effect step-runner): + +> `runStep` it is. The ideal pseudocode: +> +> ```ts +> // One logical step: one settled model call, however many attempts it takes. +> // An attempt can end without settling in two ways: +> // transient provider failure -> retry the same call, same assistant message +> // compaction rewrote history -> rebuild the request and call again +> function runStep(session, promotable, step) { +> while (true) { +> const result = callModel(session, promotable, step) +> +> if (result.completed) return result +> +> if (result.retryable) { +> wait(retrySchedule) // exhausted -> publish Step.Failed, fail drain +> promotable = none // never re-promote on a second attempt +> continue +> } +> +> // compaction restarted the step: fresh request from rewritten history +> promotable = none +> step = result.step +> } +> } +> ``` +> +> Comparing against the real thing, three deltas: +> +> 1. **The retry arm doesn't live in the loop.** In the pseudocode, retry is just another loop arm. In the real code it's smuggled through the error channel: `Effect.tapError` mutating loop variables via `Effect.sync` closures, then `Effect.retryOrElse` re-running a suspended call. Four mutable variables mutated from three different places — the state flow is invisible unless you already know the trick. +> 2. **`recoverOverflow: typeof compaction.compact | undefined`** — a function-typed parameter used as a one-shot armed/disarmed flag. The callee already closes over `compaction`; a boolean says what it is. +> 3. **`assistantMessageID`** exists solely to resume the same durable message across retries — fine, but only legible once the retry arm is visible in the loop. +> +> The unifying move for (1): a schedule driver, so a retryable failure becomes a caught value handled in the loop body exactly like the compaction arm. One honest trade: it's more manual than the blessed combinator — but the manual version is the loop the combinator was hiding, and the loop already exists for restarts. Two restart mechanisms, one control structure. +> +> Want me to apply it — unified loop, simplified schedule input, boolean `recoverOverflow`? From 65c5c7e3f64759566e9d58e0ac067c41c96cefe5 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 24 Jul 2026 00:42:44 -0500 Subject: [PATCH 071/150] feat(ai): round-trip Anthropic redacted thinking blocks (#38614) --- .../ai/src/protocols/anthropic-messages.ts | 53 +++++++- .../test/provider/anthropic-messages.test.ts | 127 ++++++++++++++++++ 2 files changed, 175 insertions(+), 5 deletions(-) diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index 4f5ad902a034..d8fe2f446818 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -75,6 +75,15 @@ const AnthropicThinkingBlock = Schema.Struct({ cache_control: Schema.optional(AnthropicCacheControl), }) +// Safety-filtered thinking arrives as an opaque encrypted `data` payload with +// no visible text. It must round-trip verbatim so multi-turn thinking + tool +// use conversations keep their reasoning continuity. +const AnthropicRedactedThinkingBlock = Schema.Struct({ + type: Schema.tag("redacted_thinking"), + data: Schema.String, + cache_control: Schema.optional(AnthropicCacheControl), +}) + const AnthropicToolUseBlock = Schema.Struct({ type: Schema.tag("tool_use"), id: Schema.String, @@ -136,6 +145,7 @@ type AnthropicUserBlock = Schema.Schema.Type const AnthropicAssistantBlock = Schema.Union([ AnthropicTextBlock, AnthropicThinkingBlock, + AnthropicRedactedThinkingBlock, AnthropicToolUseBlock, AnthropicServerToolUseBlock, AnthropicServerToolResultBlock, @@ -214,6 +224,9 @@ const AnthropicStreamBlock = Schema.Struct({ text: Schema.optional(Schema.String), thinking: Schema.optional(Schema.String), signature: Schema.optional(Schema.String), + // redacted_thinking blocks arrive whole in content_block_start with the + // encrypted payload in `data`; there is no streaming delta sequence. + data: Schema.optional(Schema.String), input: Schema.optional(Schema.Unknown), // *_tool_result blocks arrive whole as content_block_start (no streaming // delta) with the structured payload in `content` and the originating @@ -287,6 +300,12 @@ const signatureFromMetadata = (metadata: ProviderMetadata | undefined): string | return typeof anthropic.signature === "string" ? anthropic.signature : undefined } +const redactedDataFromMetadata = (metadata: ProviderMetadata | undefined): string | undefined => { + const anthropic = metadata?.anthropic + if (!ProviderShared.isRecord(anthropic)) return undefined + return typeof anthropic.redactedData === "string" ? anthropic.redactedData : undefined +} + const lowerTool = (breakpoints: Cache.Breakpoints, tool: ToolDefinition, inputSchema: JsonSchema): AnthropicTool => ({ name: tool.name, description: tool.description, @@ -472,11 +491,16 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* ( continue } if (part.type === "reasoning") { - content.push({ - type: "thinking", - thinking: part.text, - signature: part.encrypted ?? signatureFromMetadata(part.providerMetadata), - }) + // Mirrors Vercel's @ai-sdk/anthropic: a signature marks visible + // thinking; only signature-less parts carrying redactedData + // round-trip as opaque redacted_thinking blocks. + const signature = part.encrypted ?? signatureFromMetadata(part.providerMetadata) + const redactedData = redactedDataFromMetadata(part.providerMetadata) + if (signature === undefined && redactedData !== undefined) { + content.push({ type: "redacted_thinking", data: redactedData }) + continue + } + content.push({ type: "thinking", thinking: part.text, signature }) continue } if (part.type === "tool-call") { @@ -747,6 +771,25 @@ const onContentBlockStart = (state: ParserState, event: AnthropicEvent): StepRes ] } + // Redacted thinking surfaces as an empty reasoning part carrying the opaque + // payload as `redactedData` metadata (same model as Vercel's + // @ai-sdk/anthropic). The existing content_block_stop closes the part. + if (block.type === "redacted_thinking" && block.data) { + const events: LLMEvent[] = [] + return [ + { + ...state, + lifecycle: Lifecycle.reasoningStart( + state.lifecycle, + events, + `reasoning-${event.index ?? 0}`, + anthropicMetadata({ redactedData: block.data }), + ), + }, + events, + ] + } + const result = serverToolResultEvent(block) if (!result) return [state, NO_EVENTS] const events: LLMEvent[] = [] diff --git a/packages/ai/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts index a6fcd628a288..021738d75f28 100644 --- a/packages/ai/test/provider/anthropic-messages.test.ts +++ b/packages/ai/test/provider/anthropic-messages.test.ts @@ -409,6 +409,34 @@ describe("Anthropic Messages route", () => { }), ) + it.effect("round-trips redacted thinking as redacted_thinking blocks", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model, + messages: [ + Message.assistant([ + { type: "reasoning", text: "", providerMetadata: { anthropic: { redactedData: "opaque_1" } } }, + { type: "reasoning", text: "visible", providerMetadata: { anthropic: { signature: "sig_1" } } }, + ]), + ], + }), + ) + + expect(prepared.body).toMatchObject({ + messages: [ + { + role: "assistant", + content: [ + { type: "redacted_thinking", data: "opaque_1" }, + { type: "thinking", thinking: "visible", signature: "sig_1" }, + ], + }, + ], + }) + }), + ) + it.effect("parses text, reasoning, and usage stream fixtures", () => Effect.gen(function* () { const body = sseEvents( @@ -454,6 +482,105 @@ describe("Anthropic Messages route", () => { }), ) + it.effect("parses redacted thinking into empty reasoning with redactedData metadata", () => + Effect.gen(function* () { + const body = sseEvents( + { type: "message_start", message: { usage: { input_tokens: 5 } } }, + { type: "content_block_start", index: 0, content_block: { type: "redacted_thinking", data: "opaque_1" } }, + { type: "content_block_stop", index: 0 }, + { type: "content_block_start", index: 1, content_block: { type: "text", text: "" } }, + { type: "content_block_delta", index: 1, delta: { type: "text_delta", text: "Hello" } }, + { type: "content_block_stop", index: 1 }, + { type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 2 } }, + { type: "message_stop" }, + ) + const response = yield* LLMClient.generate(request).pipe(Effect.provide(fixedResponse(body))) + + expect(response.events.find((event) => event.type === "reasoning-start")).toMatchObject({ + providerMetadata: { anthropic: { redactedData: "opaque_1" } }, + }) + expect(response.message.content).toEqual([ + { type: "reasoning", text: "", providerMetadata: { anthropic: { redactedData: "opaque_1" } } }, + { type: "text", text: "Hello" }, + ]) + }), + ) + + it.effect("round-trips streamed redacted thinking with tool use into a continuation request", () => + Effect.gen(function* () { + // Anthropic types `redacted_thinking.data` as an opaque string. Its + // contents are provider-owned and must be replayed without inspection. + const redactedData = "cmVkYWN0ZWQtdGhpbmtpbmc=" + const response = yield* LLMClient.generate( + LLM.updateRequest(request, { + tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + }), + ).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { type: "message_start", message: { usage: { input_tokens: 5 } } }, + { + type: "content_block_start", + index: 0, + content_block: { type: "redacted_thinking", data: redactedData }, + }, + { type: "content_block_stop", index: 0 }, + { + type: "content_block_start", + index: 1, + content_block: { type: "tool_use", id: "call_1", name: "lookup" }, + }, + { + type: "content_block_delta", + index: 1, + delta: { type: "input_json_delta", partial_json: '{"query":"weather"}' }, + }, + { type: "content_block_stop", index: 1 }, + { type: "message_delta", delta: { stop_reason: "tool_use" }, usage: { output_tokens: 1 } }, + { type: "message_stop" }, + ), + ), + ), + ) + const prepared = yield* LLMClient.prepare( + LLM.request({ + model, + messages: [ + Message.user("Say hello."), + response.message, + Message.tool({ id: "call_1", name: "lookup", result: "sunny", resultType: "text" }), + ], + tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + cache: "none", + }), + ) + + expect(prepared.body.messages).toEqual([ + { role: "user", content: [{ type: "text", text: "Say hello." }] }, + { + role: "assistant", + content: [ + { type: "redacted_thinking", data: redactedData }, + { type: "tool_use", id: "call_1", name: "lookup", input: { query: "weather" } }, + ], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "call_1", + content: "sunny", + is_error: undefined, + cache_control: undefined, + }, + ], + }, + ]) + }), + ) + it.effect("maps context-window truncation to length", () => Effect.gen(function* () { const response = yield* LLMClient.generate(request).pipe( From ea010ab3a434981aecff1556aee6cb9c8a6a139d Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 24 Jul 2026 01:06:58 -0500 Subject: [PATCH 072/150] feat(ai): round-trip Bedrock redacted reasoning (#38623) --- packages/ai/src/protocols/bedrock-converse.ts | 46 +++++++++---- .../ai/test/provider/bedrock-converse.test.ts | 66 +++++++++++++++++++ 2 files changed, 99 insertions(+), 13 deletions(-) diff --git a/packages/ai/src/protocols/bedrock-converse.ts b/packages/ai/src/protocols/bedrock-converse.ts index 0f0316c51792..18de26420594 100644 --- a/packages/ai/src/protocols/bedrock-converse.ts +++ b/packages/ai/src/protocols/bedrock-converse.ts @@ -66,14 +66,15 @@ const BedrockToolResultBlock = Schema.Struct({ type BedrockToolResultBlock = Schema.Schema.Type const BedrockReasoningBlock = Schema.Struct({ - reasoningContent: Schema.Struct({ - reasoningText: Schema.optional( - Schema.Struct({ + reasoningContent: Schema.Union([ + Schema.Struct({ + reasoningText: Schema.Struct({ text: Schema.String, signature: Schema.optional(Schema.String), }), - ), - }), + }), + Schema.Struct({ redactedContent: Schema.String }), + ]), }) const BedrockUserBlock = Schema.Union([ @@ -181,6 +182,8 @@ const BedrockEvent = Schema.Struct({ Schema.Struct({ text: Schema.optional(Schema.String), signature: Schema.optional(Schema.String), + // Blob fields in Bedrock's JSON event stream are base64 strings. + redactedContent: Schema.optional(Schema.String), }), ), }), @@ -260,6 +263,13 @@ const reasoningSignature = (part: ReasoningPart) => { ) } +const reasoningRedactedData = (part: ReasoningPart) => { + const bedrock = part.providerMetadata?.bedrock + return ProviderShared.isRecord(bedrock) && typeof bedrock.redactedData === "string" + ? bedrock.redactedData + : undefined +} + const lowerToolCall = (part: ToolCallPart): BedrockToolUseBlock => ({ toolUse: { toolUseId: part.id, @@ -349,11 +359,13 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* ( continue } if (part.type === "reasoning") { - content.push({ - reasoningContent: { - reasoningText: { text: part.text, signature: reasoningSignature(part) }, - }, - }) + const signature = reasoningSignature(part) + const redactedData = reasoningRedactedData(part) + if (signature === undefined && redactedData !== undefined) { + content.push({ reasoningContent: { redactedContent: redactedData } }) + continue + } + content.push({ reasoningContent: { reasoningText: { text: part.text, signature } } }) continue } if (part.type === "tool-call") { @@ -519,12 +531,20 @@ const step = (state: ParserState, event: BedrockEvent) => const index = event.contentBlockDelta.contentBlockIndex const reasoning = event.contentBlockDelta.delta.reasoningContent const events: LLMEvent[] = [] + const lifecycle = reasoning.text + ? Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${index}`, reasoning.text) + : reasoning.redactedContent !== undefined + ? Lifecycle.reasoningStart( + state.lifecycle, + events, + `reasoning-${index}`, + bedrockMetadata({ redactedData: reasoning.redactedContent }), + ) + : state.lifecycle return [ { ...state, - lifecycle: reasoning.text - ? Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${index}`, reasoning.text) - : state.lifecycle, + lifecycle, reasoningSignatures: reasoning.signature ? { ...state.reasoningSignatures, [index]: reasoning.signature } : state.reasoningSignatures, diff --git a/packages/ai/test/provider/bedrock-converse.test.ts b/packages/ai/test/provider/bedrock-converse.test.ts index 809e74a0cb3f..2aa75057f63b 100644 --- a/packages/ai/test/provider/bedrock-converse.test.ts +++ b/packages/ai/test/provider/bedrock-converse.test.ts @@ -467,6 +467,72 @@ describe("Bedrock Converse route", () => { }), ) + it.effect("round-trips streamed redacted reasoning with tool use into a continuation request", () => + Effect.gen(function* () { + // Bedrock represents redactedContent blobs as base64 strings on its JSON + // wire. The provider owns the payload and requires byte-exact replay. + const redactedData = "cmVkYWN0ZWQtdGhpbmtpbmc=" + const response = yield* LLMClient.generate( + LLM.updateRequest(baseRequest, { + tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + }), + ).pipe( + Effect.provide( + fixedBytes( + eventStreamBody( + ["messageStart", { role: "assistant" }], + [ + "contentBlockDelta", + { contentBlockIndex: 0, delta: { reasoningContent: { redactedContent: redactedData } } }, + ], + ["contentBlockStop", { contentBlockIndex: 0 }], + [ + "contentBlockStart", + { + contentBlockIndex: 1, + start: { toolUse: { toolUseId: "tool_1", name: "lookup" } }, + }, + ], + [ + "contentBlockDelta", + { contentBlockIndex: 1, delta: { toolUse: { input: '{"query":"weather"}' } } }, + ], + ["contentBlockStop", { contentBlockIndex: 1 }], + ["messageStop", { stopReason: "tool_use" }], + ), + ), + ), + ) + const prepared = yield* LLMClient.prepare( + LLM.request({ + model, + messages: [ + Message.user("Say hello."), + response.message, + Message.tool({ id: "tool_1", name: "lookup", result: "sunny", resultType: "text" }), + ], + tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + cache: "none", + }), + ) + + expect(prepared.body.messages).toEqual([ + { role: "user", content: [{ text: "Say hello." }] }, + { + role: "assistant", + content: [ + { reasoningContent: { redactedContent: redactedData } }, + { toolUse: { toolUseId: "tool_1", name: "lookup", input: { query: "weather" } } }, + ], + }, + { + role: "user", + content: [{ toolResult: { toolUseId: "tool_1", content: [{ text: "sunny" }], status: "success" } }], + }, + ]) + }), + ) + it.effect("classifies throttlingException as a rate limit", () => Effect.gen(function* () { const body = eventStreamBody( From 00f063b3811dae66a512b67246b899389485a554 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Fri, 24 Jul 2026 10:54:30 +0200 Subject: [PATCH 073/150] mini: pack statusline by content width (#38646) --- packages/tui/src/mini/footer.ts | 4 +- packages/tui/src/mini/footer.view.tsx | 163 ++++++++++++------ packages/tui/src/mini/footer.width.ts | 71 +++++--- packages/tui/test/mini/footer-keymap.test.tsx | 1 + packages/tui/test/mini/footer.test.ts | 4 +- packages/tui/test/mini/footer.view.test.tsx | 75 +++++++- packages/tui/test/mini/footer.width.test.ts | 27 +-- 7 files changed, 230 insertions(+), 115 deletions(-) diff --git a/packages/tui/src/mini/footer.ts b/packages/tui/src/mini/footer.ts index 002cc626168a..99c86dd769e6 100644 --- a/packages/tui/src/mini/footer.ts +++ b/packages/tui/src/mini/footer.ts @@ -104,7 +104,8 @@ type RunFooterOptions = { export function resolveRunAgent(agents: RunAgent[], current: string | undefined) { const selectable = agents.filter((agent) => agent.mode !== "subagent" && !agent.hidden) - return selectable.find((agent) => agent.id === current) ?? selectable.at(0) + if (current === undefined) return selectable.at(0) + return selectable.find((agent) => agent.id === current) } const PERMISSION_ROWS = 12 @@ -327,6 +328,7 @@ export class RunFooter implements FooterApi { providers: footer.providers, currentAgent: footer.currentAgent, currentAgentID: footer.currentAgentID, + currentAgentExplicit: () => selectedAgentID() !== undefined, currentModel: footer.currentModel, variants: footer.variants, currentVariant: footer.currentVariant, diff --git a/packages/tui/src/mini/footer.view.tsx b/packages/tui/src/mini/footer.view.tsx index 956168565cf5..0656a83eacc8 100644 --- a/packages/tui/src/mini/footer.view.tsx +++ b/packages/tui/src/mini/footer.view.tsx @@ -29,10 +29,11 @@ import { RunPromptBody, createPromptState } from "./footer.prompt" import { RunPermissionBody } from "./footer.permission" import { RunFormBody } from "./footer.form" import { createFormBodyState, type FormBodyState } from "./form.shared" -import { footerWidthPolicy } from "./footer.width" +import { footerStatuslinePolicy } from "./footer.width" import { Keymap } from "../context/keymap" import { modelInfo } from "./variant.shared" import { monoShortcut } from "./mono" +import { stringWidth } from "../util/string-width" import type { FooterPromptRoute, @@ -79,6 +80,7 @@ type RunFooterViewProps = { providers: () => RunProvider[] | undefined currentAgent: () => string currentAgentID: () => string | undefined + currentAgentExplicit: () => boolean currentModel: () => RunInput["model"] variants: () => string[] currentVariant: () => string | undefined @@ -116,7 +118,6 @@ type RunFooterViewProps = { export function RunFooterView(props: RunFooterViewProps) { const term = useTerminalDimensions() const width = createMemo(() => term().width) - const responsive = createMemo(() => footerWidthPolicy(width())) const active = createMemo(() => props.view?.() ?? { type: "prompt" }) const subagent = createMemo(() => { return ( @@ -410,19 +411,19 @@ export function RunFooterView(props: RunFooterViewProps) { return shell() ? "Shell mode" : "" }) const activityMeta = createMemo(() => { - if (!footerDetails() || !responsive().statusline.showActivityMeta || usage().length === 0) { - return "" - } - + if (!footerDetails()) return "" return props.mono ? usage().replaceAll(" · ", " - ") : usage() }) + const agentStatus = createMemo(() => { + if (!footerDetails() || !prompt() || shell() || !props.currentAgentExplicit()) return undefined + return props.currentAgent() + }) const modelStatus = createMemo(() => { const current = model() ?? props.state().model.trim() - if (!footerDetails() || !prompt() || shell() || !responsive().statusline.showModel || !current) return + if (!footerDetails() || !prompt() || shell() || !current) return return { - agent: props.currentAgent(), model: current, - variant: responsive().statusline.showModelVariant ? props.currentVariant() : undefined, + variant: props.currentVariant(), } }) const statusColor = createMemo(() => { @@ -441,32 +442,26 @@ export function RunFooterView(props: RunFooterViewProps) { return theme().muted }) const statuslineBackground = createMemo(() => theme().status) - const hasActivityMeta = createMemo(() => activityMeta().length > 0) - const hasModelStatus = createMemo(() => Boolean(modelStatus())) - const contextHints = createMemo(() => { - if (!footerDetails() || !prompt() || shell() || !responsive().statusline.showContextHints) { + const contextHintCandidates = createMemo(() => { + if (!footerDetails() || !prompt() || shell()) { return [] } - const items: Array<{ kind: string; key: string; label: string }> = [] + const items: Array<{ key: string; label: string }> = [] if (foregroundSubagents() && backgroundShortcut()) { - items.push({ kind: "background", key: backgroundShortcut(), label: "background" }) + items.push({ key: backgroundShortcut(), label: "background" }) } if (queuedPrompts().length > 0 && queuedShortcut()) { - items.push({ kind: "queued", key: queuedShortcut(), label: `${queuedPrompts().length} pending` }) + items.push({ key: queuedShortcut(), label: `${queuedPrompts().length} pending` }) } if (activeTabs().length > 0 && subagentShortcut()) { - items.push({ kind: "subagents", key: subagentShortcut(), label: "subagents" }) + items.push({ key: subagentShortcut(), label: "subagents" }) } - const limit = responsive().statusline.contextHintLimit - return limit === undefined ? items : items.slice(0, limit) + return items }) - const hasContextHints = createMemo(() => contextHints().length > 0) const commandHint = createMemo(() => { - if (!prompt() || !responsive().statusline.showCommandHint) { - return - } + if (!prompt()) return if (shell()) { return { key: "esc", label: "normal" } @@ -476,6 +471,49 @@ export function RunFooterView(props: RunFooterViewProps) { return { key: command(), label: "cmd" } } }) + const commandHintWidth = createMemo(() => { + const hint = commandHint() + return hint ? stringWidth(`${hint.key} ${hint.label}`) : 0 + }) + const statuslineText = createMemo(() => + busy() && !exiting() && (footerDetails() || armed()) + ? `${interruptLabel() ? `${interruptLabel()} ` : ""}${statusText()}` + : statusText(), + ) + const statuslineMainWidth = createMemo(() => { + const mode = modeLabel() + const modeWidth = mode ? stringWidth(mode) + (props.mono ? 1 : 2) : 0 + const spinnerWidth = footerDetails() && busy() && !exiting() ? stringWidth(spin().frames[0] ?? "") + 1 : 0 + return modeWidth + Math.max(12, (props.mono ? 1 : 2) + spinnerWidth + stringWidth(statuslineText())) + }) + const visibleModeLabel = createMemo(() => { + const mode = modeLabel() + if (!mode || width() - commandHintWidth() < stringWidth(mode) + (props.mono ? 1 : 2)) return undefined + return mode + }) + const statuslineMainAvailable = createMemo(() => { + const mode = visibleModeLabel() + return width() - commandHintWidth() - (mode ? stringWidth(mode) + (props.mono ? 1 : 2) : 0) + }) + const statuslineLayout = createMemo(() => { + const agent = agentStatus() + const info = modelStatus() + return footerStatuslinePolicy({ + width: width(), + mainWidth: statuslineMainWidth(), + commandWidth: commandHint() ? commandHintWidth() : undefined, + agentWidth: agent ? stringWidth(agent) : undefined, + contextWidths: contextHintCandidates().map((item) => stringWidth(`${item.key} ${item.label}`)), + modelWidth: info ? stringWidth(info.model) : undefined, + variantWidth: info?.variant ? stringWidth(` ${info.variant}`) : undefined, + usageWidth: activityMeta() ? stringWidth(activityMeta()) : undefined, + }) + }) + const contextHints = createMemo(() => contextHintCandidates().slice(0, statuslineLayout().contextCount)) + const hasStatuslineInfo = createMemo(() => { + const layout = statuslineLayout() + return layout.showUsage || layout.showAgent || layout.showModel + }) const sectionSeparator = () => {props.mono ? "- " : "· "} createEffect(() => { @@ -876,7 +914,7 @@ export function RunFooterView(props: RunFooterViewProps) { flexShrink={0} backgroundColor={statuslineBackground()} > - + {(label) => ( = 2 && !props.mono ? 1 : 0} + paddingRight={statuslineMainAvailable() >= (props.mono ? 1 : 2) ? 1 : 0} backgroundColor="transparent" + overflow="hidden" > - + = + (props.mono ? 1 : 2) + stringWidth(spin().frames[0] ?? "") + 1 + stringWidth(statuslineText()) + } + > @@ -917,29 +964,36 @@ export function RunFooterView(props: RunFooterViewProps) { - 0}> - - - {activityMeta()} - - + + {(usage) => ( + + + {usage()} + + + )} + + + + {(agent) => ( + + + {sectionSeparator()} + {agent()} + + + )} - + {(info) => ( - - - - {info().agent} - {props.mono ? " - " : " · "} + + + + {sectionSeparator()} {info().model} - + {(variant) => {variant()}} @@ -949,25 +1003,20 @@ export function RunFooterView(props: RunFooterViewProps) { {(hint, index) => ( - - - 0 || ((hasActivityMeta() || hasModelStatus()) && index() === 0)}> - {sectionSeparator()} - + + + 0 || (hasStatuslineInfo() && index() === 0)}>{sectionSeparator()} {hint.key}{" "} {hint.label} )} - {(hint) => ( - - - - {sectionSeparator()} - + + + 0}>{sectionSeparator()} {hint().key}{" "} {hint().label} diff --git a/packages/tui/src/mini/footer.width.ts b/packages/tui/src/mini/footer.width.ts index e7fd07b5bf91..c48f7eeb15ae 100644 --- a/packages/tui/src/mini/footer.width.ts +++ b/packages/tui/src/mini/footer.width.ts @@ -1,31 +1,52 @@ -// Shared responsive width policy - -const FOOTER_WIDTH_BREAKPOINTS = { - commandHint: 24, - model: 32, - modelVariant: 40, - compact: 80, - context: 120, - spacious: 150, -} as const - export function footerWidthPolicy(width: number) { - const compact = width >= FOOTER_WIDTH_BREAKPOINTS.compact - const context = width >= FOOTER_WIDTH_BREAKPOINTS.context - const spacious = width >= FOOTER_WIDTH_BREAKPOINTS.spacious - return { dialog: { - narrow: !compact, - }, - statusline: { - showActivityMeta: compact, - showAgent: compact, - showCommandHint: width >= FOOTER_WIDTH_BREAKPOINTS.commandHint, - showModel: width >= FOOTER_WIDTH_BREAKPOINTS.model, - showModelVariant: width >= FOOTER_WIDTH_BREAKPOINTS.modelVariant, - showContextHints: compact, - contextHintLimit: !compact ? 0 : spacious ? undefined : context ? 2 : 1, + narrow: width < 80, }, } } + +export function footerStatuslinePolicy(input: { + width: number + mainWidth: number + commandWidth?: number + agentWidth?: number + contextWidths: number[] + modelWidth?: number + variantWidth?: number + usageWidth?: number +}) { + let remaining = input.width - input.mainWidth - (input.commandWidth ?? 0) + let hasSection = input.commandWidth !== undefined + const include = (width: number | undefined) => { + if (width === undefined) return false + const required = width + (hasSection ? 3 : 1) + if (remaining < required) return false + remaining -= required + hasSection = true + return true + } + + const showModel = include(input.modelWidth) + const showAgent = include(input.agentWidth) + const hiddenContext = input.contextWidths.findIndex((width) => !include(width)) + const contextCount = hiddenContext === -1 ? input.contextWidths.length : hiddenContext + const contextComplete = contextCount === input.contextWidths.length + const variantWidth = input.variantWidth + const showVariant = showModel && contextComplete && variantWidth !== undefined && remaining >= variantWidth + if (showVariant) remaining -= variantWidth + const showUsage = + (showModel || input.modelWidth === undefined) && + (showAgent || input.agentWidth === undefined) && + contextComplete && + (showVariant || input.variantWidth === undefined) && + include(input.usageWidth) + + return { + showAgent, + contextCount, + showModel, + showVariant, + showUsage, + } +} diff --git a/packages/tui/test/mini/footer-keymap.test.tsx b/packages/tui/test/mini/footer-keymap.test.tsx index cca3526576ef..40d7e09813d6 100644 --- a/packages/tui/test/mini/footer-keymap.test.tsx +++ b/packages/tui/test/mini/footer-keymap.test.tsx @@ -49,6 +49,7 @@ test("down opens subagents from an empty prompt", async () => { providers={() => undefined} currentAgent={() => "Build"} currentAgentID={() => "build"} + currentAgentExplicit={() => false} currentModel={() => undefined} variants={() => []} currentVariant={() => undefined} diff --git a/packages/tui/test/mini/footer.test.ts b/packages/tui/test/mini/footer.test.ts index 8fbf7fc686b4..caa84ee35740 100644 --- a/packages/tui/test/mini/footer.test.ts +++ b/packages/tui/test/mini/footer.test.ts @@ -24,7 +24,7 @@ test("coalesces progress only within the same message and tool state", () => { ) }) -test("resolves the first selectable agent when none is selected", () => { +test("falls back only when no agent is selected", () => { const agents: RunAgent[] = [ { id: "task", name: "Task", mode: "subagent", hidden: false }, { id: "secret", name: "Secret", mode: "primary", hidden: true }, @@ -34,5 +34,5 @@ test("resolves the first selectable agent when none is selected", () => { expect(resolveRunAgent(agents, undefined)?.id).toBe("build") expect(resolveRunAgent(agents, "plan")?.id).toBe("plan") - expect(resolveRunAgent(agents, "missing")?.id).toBe("build") + expect(resolveRunAgent(agents, "missing")).toBeUndefined() }) diff --git a/packages/tui/test/mini/footer.view.test.tsx b/packages/tui/test/mini/footer.view.test.tsx index f86287124e08..881cf0e62572 100644 --- a/packages/tui/test/mini/footer.view.test.tsx +++ b/packages/tui/test/mini/footer.view.test.tsx @@ -157,6 +157,7 @@ async function renderFooter( providers={() => input.providers} currentAgent={() => input.currentAgent ?? "Build"} currentAgentID={() => input.currentAgent?.toLowerCase() ?? "build"} + currentAgentExplicit={() => input.currentAgent !== undefined} currentModel={() => input.currentModel} variants={() => []} currentVariant={() => input.currentVariant} @@ -208,11 +209,13 @@ async function renderFooter( } } -test("direct footer shows the generic default model before resolution", async () => { +test("direct footer shows the default model without the fallback agent", async () => { const app = await renderFooter({ state: { model: "Default model" } }) try { await app.renderOnce() - expect(app.captureCharFrame()).toContain("Default model") + const frame = app.captureCharFrame() + expect(frame).toContain("Default model") + expect(frame).not.toContain("Build") } finally { app.cleanup() } @@ -1179,6 +1182,7 @@ test("direct footer shows authoritative pending work while running", async () => providers={() => undefined} currentAgent={() => "Build"} currentAgentID={() => "build"} + currentAgentExplicit={() => false} currentModel={() => ({ providerID: "opencode", modelID: "a-model-name-long-enough-to-force-responsive-truncation", @@ -1276,14 +1280,13 @@ test("direct footer progressively adds model details after the command hint", as for (const expected of [ { width: 24, agent: false, model: false, variant: false }, { width: 32, agent: false, model: true, variant: false }, - { width: 40, agent: false, model: true, variant: true }, - { width: 80, agent: true, model: true, variant: true }, + { width: 40, agent: true, model: true, variant: false }, + { width: 48, agent: true, model: true, variant: true }, ]) { const app = await renderFooter({ - providers: [provider()], currentAgent: "Plan", - currentModel: { providerID: "opencode", modelID: "gpt-5" }, currentVariant: "xhigh", + state: { model: "GPT-5" }, width: expected.width, }) @@ -1303,6 +1306,66 @@ test("direct footer progressively adds model details after the command hint", as } }) +test("direct footer keeps commands and active work ahead of usage under width pressure", async () => { + const app = await renderFooter({ + currentAgent: "Plan", + subagents: { + tabs: [subagent({ sessionID: "s-1", label: "Explore", description: "Inspect auth flow" })], + details: {}, + permissions: [], + forms: [], + }, + state: { + phase: "running", + model: "a-model-name-long-enough-to-force-responsive-truncation", + usage: "159.6K (16%) · $4.23", + }, + width: 80, + }) + + try { + await app.renderOnce() + const frame = app.captureCharFrame() + + expect(frame).toContain("Plan") + expect(frame).toContain("ctrl+b background") + expect(frame).toContain("↓ subagents") + expect(frame).toContain("ctrl+p cmd") + expect(frame).not.toContain("a-model-name") + expect(frame).not.toContain("159.6K") + expect(frame).not.toContain("$4.23") + } finally { + app.cleanup() + } +}) + +test("direct footer keeps the command hint at its minimum width", async () => { + const app = await renderFooter({ state: { phase: "running" }, width: 10 }) + + try { + await app.renderOnce() + expect(app.captureCharFrame()).toContain("ctrl+p cmd") + } finally { + app.cleanup() + } +}) + +test("direct footer keeps complete status text ahead of the spinner", async () => { + const app = await renderFooter({ + tuiConfig: createTuiResolvedConfig({ keybinds: { session_interrupt: "none" } }), + state: { phase: "running" }, + width: 22, + }) + + try { + await app.renderOnce() + expect(app.captureCharFrame()).toContain("interrupt") + expect(boxPath(footerStatusline(app.renderer.root), "SpinnerRenderable")).toBeUndefined() + } finally { + app.cleanup() + } +}) + test("direct footer always offers backgrounding for a foreground subagent", async () => { const app = await renderFooter({ subagents: { diff --git a/packages/tui/test/mini/footer.width.test.ts b/packages/tui/test/mini/footer.width.test.ts index 244ec83cb329..8e7fb229d920 100644 --- a/packages/tui/test/mini/footer.width.test.ts +++ b/packages/tui/test/mini/footer.width.test.ts @@ -2,29 +2,8 @@ import { describe, expect, test } from "bun:test" import { footerWidthPolicy } from "../../src/mini/footer.width" describe("run footer width", () => { - test("preserves shared dialog and statusline breakpoints", () => { - expect([23, 24].map((width) => footerWidthPolicy(width).statusline.showCommandHint)).toEqual([false, true]) - expect([31, 32].map((width) => footerWidthPolicy(width).statusline.showModel)).toEqual([false, true]) - expect([39, 40].map((width) => footerWidthPolicy(width).statusline.showModelVariant)).toEqual([false, true]) - - const narrow = footerWidthPolicy(79) - expect(narrow.dialog.narrow).toBe(true) - expect(narrow.statusline.showActivityMeta).toBe(false) - expect(narrow.statusline.showAgent).toBe(false) - expect(narrow.statusline.showContextHints).toBe(false) - expect(narrow.statusline.contextHintLimit).toBe(0) - - const compact = footerWidthPolicy(80) - expect(compact.dialog.narrow).toBe(false) - expect(compact.statusline.showActivityMeta).toBe(true) - expect(compact.statusline.showAgent).toBe(true) - expect(compact.statusline.showContextHints).toBe(true) - expect(compact.statusline.contextHintLimit).toBe(1) - - const context = footerWidthPolicy(120) - expect(context.statusline.contextHintLimit).toBe(2) - - const spacious = footerWidthPolicy(150) - expect(spacious.statusline.contextHintLimit).toBeUndefined() + test("preserves the dialog breakpoint", () => { + expect(footerWidthPolicy(79).dialog.narrow).toBe(true) + expect(footerWidthPolicy(80).dialog.narrow).toBe(false) }) }) From 4184149b907b7ad2035a95281ea71be4cd54b287 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Fri, 24 Jul 2026 11:14:48 +0200 Subject: [PATCH 074/150] mini: monochrome rendered markdown only (#38656) --- packages/tui/src/mini/entry.body.ts | 4 +- packages/tui/src/mini/mono.ts | 135 ++++++++++++++++-- packages/tui/src/mini/scrollback.surface.ts | 8 +- packages/tui/src/mini/scrollback.writer.tsx | 16 ++- packages/tui/test/mini/entry.body.test.ts | 41 +++--- packages/tui/test/mini/footer.view.test.tsx | 77 ++++++++++ .../tui/test/mini/scrollback.surface.test.ts | 86 ++++++++++- 7 files changed, 322 insertions(+), 45 deletions(-) diff --git a/packages/tui/src/mini/entry.body.ts b/packages/tui/src/mini/entry.body.ts index 499ed091da6f..6cd23187499c 100644 --- a/packages/tui/src/mini/entry.body.ts +++ b/packages/tui/src/mini/entry.body.ts @@ -81,8 +81,8 @@ function systemBody(raw: string, phase: StreamCommit["phase"]): RunEntryBody { } function monoBody(body: RunEntryBody): RunEntryBody { - if (body.type === "none" || body.type === "text") return body - if (body.type === "code" || body.type === "markdown") return textBody(body.content) + if (body.type === "none" || body.type === "text" || body.type === "markdown") return body + if (body.type === "code") return textBody(body.content) const snapshot = body.snapshot if (snapshot.kind === "code") return textBody(`${snapshot.title}\n${snapshot.content}`) if (snapshot.kind === "diff") { diff --git a/packages/tui/src/mini/mono.ts b/packages/tui/src/mini/mono.ts index 109daa0a4bd5..62205eba6626 100644 --- a/packages/tui/src/mini/mono.ts +++ b/packages/tui/src/mini/mono.ts @@ -1,10 +1,17 @@ import { BoxRenderable, + CodeRenderable, + MarkdownRenderable, RGBA, + Renderable, + StyledText, + TextRenderable, + TextTableRenderable, + isStyledText, + stringToStyledText, type BorderCharacters, type CliRendererExternalOutputEvent, - type MarkdownOptions, - type Renderable, + type TreeSitterClient, } from "@opentui/core" const prefixes: Record = { @@ -52,27 +59,129 @@ const asciiBorder: BorderCharacters = { cross: "+", } +const hooked = new WeakSet() + export const monoMarkdownTableOptions = { style: "columns" as const, widthMode: "content" as const, borders: false, } -export const monoMarkdownRenderNode: NonNullable = (token, context) => { - if (token.type !== "blockquote" && token.type !== "hr" && token.type !== "list") return - const renderable = context.defaultRender() - if (!renderable) return renderable - monoBorders(renderable) - return renderable +export function monoMarkdownRenderable(renderable: MarkdownRenderable): void { + monoRenderable(renderable) } -function monoBorders(renderable: Renderable): void { +function monoRenderable(renderable: Renderable): void { + if (hooked.has(renderable)) return + hooked.add(renderable) + // Markdown reconciles nested lists and tables without calling renderNode. + // Hook the actual tree so future descendants are transformed before layout. + const add = renderable.add.bind(renderable) + renderable.add = (child, index) => { + if (child instanceof Renderable) monoRenderable(child) + return add(child, index) + } + if (renderable instanceof BoxRenderable) renderable.customBorderChars = asciiBorder - renderable.getChildren().forEach(monoBorders) + if (renderable instanceof CodeRenderable) monoCode(renderable) + if (renderable instanceof TextRenderable) renderable.content = monoStyledText(renderable.content) + if (renderable instanceof TextTableRenderable) monoTable(renderable) + renderable.getChildren().forEach(monoRenderable) } -export function monoMarkdown(value: string, mono: boolean): string { - if (!mono) return value +function monoCode(renderable: CodeRenderable): void { + const onChunks = renderable.onChunks + const prose = renderable.filetype === "markdown" && onChunks !== undefined + renderable.onChunks = async (chunks, context) => monoChunks((await onChunks?.(chunks, context)) ?? chunks) + renderable.treeSitterClient = monoTreeSitter(renderable.treeSitterClient) + + const initialDescriptor = Object.getOwnPropertyDescriptor(CodeRenderable.prototype, "initialStyledText") + const contentDescriptor = Object.getOwnPropertyDescriptor(CodeRenderable.prototype, "content") + if (!initialDescriptor?.set || !contentDescriptor?.get || !contentDescriptor.set) return + const initialSetter = initialDescriptor.set.bind(renderable) + const contentGetter = contentDescriptor.get.bind(renderable) + const contentSetter = contentDescriptor.set.bind(renderable) + const initial = Reflect.get(renderable, "_initialStyledText") + Object.defineProperty(renderable, "initialStyledText", { + configurable: true, + set(value: StyledText | undefined) { + initialSetter(value ? monoStyledText(value) : value) + }, + }) + Object.defineProperty(renderable, "content", { + configurable: true, + get: contentGetter, + set(value: string) { + if (!prose || !isStyledText(Reflect.get(renderable, "_initialStyledText"))) { + renderable.drawUnstyledText = true + renderable.initialStyledText = stringToStyledText(value) + } + contentSetter(value) + }, + }) + + if (isStyledText(initial)) { + renderable.initialStyledText = initial + } else { + renderable.drawUnstyledText = true + renderable.initialStyledText = stringToStyledText(renderable.content) + } + if (!renderable.drawUnstyledText) return + + // Refresh the eager buffer with the transformed initial text. Highlighted + // chunks continue through onChunks without changing the Markdown source. + const content = renderable.content + renderable.content = "" + renderable.content = content +} + +function monoTreeSitter(client: TreeSitterClient): TreeSitterClient { + return new Proxy(client, { + get(target, property) { + if (property !== "highlightOnce") return Reflect.get(target, property, target) + // Keep parser failures on the chunk path instead of OpenTUI's raw-text fallback. + return (...args: Parameters) => + target.highlightOnce(...args).catch(() => ({ highlights: [] })) + }, + }) +} + +function monoTable(renderable: TextTableRenderable): void { + const descriptor = Object.getOwnPropertyDescriptor(TextTableRenderable.prototype, "content") + if (!descriptor?.get || !descriptor.set) return + const cells = new WeakMap() + const content = renderable.content + Object.defineProperty(renderable, "content", { + configurable: true, + get: () => descriptor.get!.call(renderable), + set: (value: TextTableRenderable["content"]) => { + descriptor.set!.call( + renderable, + value.map((row) => + row.map((cell) => { + if (!cell) return cell + const cached = cells.get(cell) + if (cached) return cached + const next = monoChunks(cell) + cells.set(cell, next) + return next + }), + ), + ) + }, + }) + renderable.content = content +} + +function monoStyledText(value: StyledText): StyledText { + return new StyledText(monoChunks(value.chunks)) +} + +function monoChunks(value: StyledText["chunks"]): StyledText["chunks"] { + return value.map((chunk) => ({ ...chunk, text: monoText(chunk.text) })) +} + +function monoText(value: string): string { return value.replace(/[^\t\n\x20-\x7e]/gu, (char) => markdown[char.codePointAt(0)!] ?? "?") } @@ -80,7 +189,7 @@ export function monoSnapshot(event: CliRendererExternalOutputEvent): void { const buffers = event.snapshot.buffers const chars = buffers.char for (let index = 0; index < chars.length; index += 1) { - const point = chars[index]! + const point = chars[index] if (point <= 0x7f) continue const offset = index * 4 event.snapshot.setCell( diff --git a/packages/tui/src/mini/scrollback.surface.ts b/packages/tui/src/mini/scrollback.surface.ts index 9d81c2bb987b..e5d4d01b1294 100644 --- a/packages/tui/src/mini/scrollback.surface.ts +++ b/packages/tui/src/mini/scrollback.surface.ts @@ -14,7 +14,7 @@ import { type ScrollbackSurface, } from "@opentui/core" import { entryBody, entryCanStream, entryDone, entryFlags } from "./entry.body" -import { monoMarkdown, monoMarkdownRenderNode, monoMarkdownTableOptions } from "./mono" +import { monoMarkdownRenderable, monoMarkdownTableOptions } from "./mono" import { entryColor, entryLook, entrySyntax } from "./scrollback.shared" import { turnSummaryCommit } from "./turn-summary" import { entryWriter, sameEntryGroup, separatorRows, spacerWriter, turnSummaryWriter } from "./scrollback.writer" @@ -181,11 +181,11 @@ export class RunScrollbackStream { streaming: true, internalBlockMode: "top-level", tableOptions: this.mono ? monoMarkdownTableOptions : { widthMode: "content" }, - renderNode: this.mono ? monoMarkdownRenderNode : undefined, fg: entryColor(commit, this.theme), treeSitterClient, }) + if (this.mono && renderable instanceof MarkdownRenderable) monoMarkdownRenderable(renderable) surface.root.add(renderable) const rows = separatorRows(this.rendered, commit, body) @@ -283,7 +283,7 @@ export class RunScrollbackStream { } const renderable = active.renderable - renderable.content = monoMarkdown(active.content, this.mono) + renderable.content = active.content renderable.streaming = !done await active.surface.settle() this.releasePendingThemes() @@ -378,7 +378,7 @@ export class RunScrollbackStream { ) { await this.writeStreaming(commit, body) if (entryDone(commit)) { - this.markRendered(await this.finishActive(false)) + this.markRendered(await this.finishActive(entryFlags(commit).trailingNewline)) } this.tail = commit return diff --git a/packages/tui/src/mini/scrollback.writer.tsx b/packages/tui/src/mini/scrollback.writer.tsx index c74f09168b62..907abe0b74c7 100644 --- a/packages/tui/src/mini/scrollback.writer.tsx +++ b/packages/tui/src/mini/scrollback.writer.tsx @@ -1,8 +1,14 @@ import { createScrollbackWriter } from "@opentui/solid" -import { TextRenderable, type ColorInput, type ScrollbackRenderContext, type ScrollbackWriter } from "@opentui/core" +import { + MarkdownRenderable, + TextRenderable, + type ColorInput, + type ScrollbackRenderContext, + type ScrollbackWriter, +} from "@opentui/core" import { Match, Switch, createMemo } from "solid-js" import { entryBody, entryFlags } from "./entry.body" -import { monoMarkdown, monoMarkdownRenderNode, monoMarkdownTableOptions } from "./mono" +import { monoMarkdownRenderable, monoMarkdownTableOptions } from "./mono" import { entryColor, entryLook, entrySyntax } from "./scrollback.shared" import { toolFiletype, toolStructuredFinal } from "./tool" import { RUN_THEME_FALLBACK, transparent, type RunTheme } from "./theme" @@ -237,13 +243,15 @@ export function RunEntryContent(props: { { + if (props.opts?.mono) monoMarkdownRenderable(renderable) + }} width="100%" syntaxStyle={syntax()} streaming={streaming()} - content={monoMarkdown(markdown()!.content, props.opts?.mono === true)} + content={markdown()!.content} fg={color()} tableOptions={props.opts?.mono ? monoMarkdownTableOptions : { widthMode: "content" }} - renderNode={props.opts?.mono ? monoMarkdownRenderNode : undefined} /> diff --git a/packages/tui/test/mini/entry.body.test.ts b/packages/tui/test/mini/entry.body.test.ts index 14b1a035b91b..717466de3c6e 100644 --- a/packages/tui/test/mini/entry.body.test.ts +++ b/packages/tui/test/mini/entry.body.test.ts @@ -231,29 +231,28 @@ describe("run entry body", () => { }) test("promotes subagent results to markdown and falls back to structured summaries", () => { - expect( - entryBody( - toolCommit({ - tool: "subagent", - state: { - status: "completed", - input: { - description: "Inspect reducer", - agent: "explore", - }, - content: [{ type: "text", text: "# Findings\n\n- Footer stays live" }], - metadata: { - sessionID: "ses-child-1", - status: "completed", - output: "# Findings\n\n- Footer stays live", - }, - }, - }), - ), - ).toEqual({ + const result = toolCommit({ + tool: "subagent", + state: { + status: "completed", + input: { + description: "Inspect reducer", + agent: "explore", + }, + content: [{ type: "text", text: "# Findings\n\n- Footer stays live" }], + metadata: { + sessionID: "ses-child-1", + status: "completed", + output: "# Findings\n\n- Footer stays live", + }, + }, + }) + const markdown = { type: "markdown", content: "# Findings\n\n- Footer stays live", - }) + } as const + expect(entryBody(result)).toEqual(markdown) + expect(entryBody(result, { mono: true })).toEqual(markdown) expect( structured( diff --git a/packages/tui/test/mini/footer.view.test.tsx b/packages/tui/test/mini/footer.view.test.tsx index 881cf0e62572..ae1c8322baa8 100644 --- a/packages/tui/test/mini/footer.view.test.tsx +++ b/packages/tui/test/mini/footer.view.test.tsx @@ -381,6 +381,83 @@ test("run entry content updates when live commit text changes", async () => { } }) +test("run entry content preserves monochrome markdown grammar", async () => { + const [commit, setCommit] = createSignal({ + kind: "assistant", + text: "• literal\n\n———\n\narrow →", + phase: "progress", + source: "assistant", + messageID: "msg-1", + partID: "part-1", + }) + const app = await testRender( + () => ( + + + + ), + { width: 60, height: 8 }, + ) + + try { + await app.renderOnce() + const rows = app + .captureCharFrame() + .split("\n") + .map((row) => row.trimEnd()) + expect(rows).toContain("* literal") + expect(rows).toContain("------") + expect(rows).toContain("arrow ->") + expect(rows.join("\n")).not.toMatch(/[^\x00-\x7f]/) + + setCommit({ ...commit(), text: "- Café\n- arrow →\n- third …" }) + await app.renderOnce() + expect(app.captureCharFrame()).toContain("- arrow ->") + expect(app.captureCharFrame()).toContain("- third ...") + + setCommit({ ...commit(), text: "| A | B |\n| - | - |\n| Café | → |" }) + await app.renderOnce() + expect(app.captureCharFrame()).toContain("Caf?") + expect(app.captureCharFrame()).toContain("->") + setCommit({ ...commit(), text: "| A | B |\n| - | - |\n| Café | … |" }) + await app.renderOnce() + expect(app.captureCharFrame()).toContain("...") + + setCommit({ ...commit(), text: "```\nCafé → …\n```" }) + await app.renderOnce() + expect(app.captureCharFrame()).toContain("Caf? -> ...") + expect(app.captureCharFrame()).not.toMatch(/[^\x00-\x7f]/) + } finally { + app.renderer.destroy() + } +}) + +test("run entry content eagerly renders final monochrome markdown", async () => { + const app = await testRender( + () => ( + + + + ), + { width: 60, height: 6 }, + ) + + try { + await app.renderOnce() + const frame = app.captureCharFrame() + expect(frame).toContain("# Caf? ->") + expect(frame).toContain("Caf? ->") + expect(frame).not.toMatch(/[^\x00-\x7f]/) + } finally { + app.renderer.destroy() + } +}) + test("direct command panel renders grouped actions without catalog commands", async () => { const [commands] = createSignal([ command({ name: "review", description: "Review code" }), diff --git a/packages/tui/test/mini/scrollback.surface.test.ts b/packages/tui/test/mini/scrollback.surface.test.ts index e2fe8226819f..88507be52fc7 100644 --- a/packages/tui/test/mini/scrollback.surface.test.ts +++ b/packages/tui/test/mini/scrollback.surface.test.ts @@ -69,6 +69,7 @@ async function setup( theme?: RunTheme onThemeRelease?: (theme: RunTheme) => void mono?: boolean + failHighlight?: boolean } = {}, ) { const out = await createTestRenderer({ @@ -83,6 +84,11 @@ async function setup( const treeSitterClient = new MockTreeSitterClient({ autoResolveTimeout: 0 }) treeSitterClient.setMockResult({ highlights: [] }) + if (input.failHighlight) { + treeSitterClient.highlightOnce = async () => { + throw new Error("highlight failed") + } + } return { renderer: out.renderer, @@ -217,7 +223,25 @@ test("renders monochrome scrollback as ASCII markdown", async () => { try { await out.scrollback.append(assistant("# H")) expect(Reflect.get(out.scrollback, "active")?.renderable).toBeInstanceOf(MarkdownRenderable) - await out.scrollback.append(assistant("éading →\n\n> “quote”\n\n---\n\n| A | B |\n| - | - |\n| α | β |")) + await out.scrollback.append( + assistant( + "éading →\n\n> “quote”\n\n---\n\n| A | B |\n| - | - |\n| α | β |\n\n• literal\n\n———\n\n[café](https://example.com/café)", + ), + ) + const active: unknown = Reflect.get(out.scrollback, "active") + const renderable = + active && typeof active === "object" && "renderable" in active && active.renderable instanceof MarkdownRenderable + ? active.renderable + : undefined + expect(renderable?._blockStates.slice(-3).map((state) => state.token.type)).toEqual([ + "paragraph", + "paragraph", + "paragraph", + ]) + const link = renderable?._blockStates.at(-1)?.token + const tokens = link && "tokens" in link && Array.isArray(link.tokens) ? link.tokens : [] + const href = tokens.find((token) => "href" in token) + expect(href && "href" in href ? href.href : undefined).toBe("https://example.com/café") await out.scrollback.complete() out.renderer.writeToScrollback((ctx) => ({ root: new TextRenderable(ctx.renderContext, { @@ -235,6 +259,8 @@ test("renders monochrome scrollback as ASCII markdown", async () => { expect(rendered).toContain('| "quote"') expect(rendered).toContain("------------------------------------------------------------") expect(rendered).toContain("? ?") + expect(rendered).toContain("* literal") + expect(rendered).toContain("------") expect(rendered).toContain("plain ? emoji ?") expect(rendered).not.toMatch(/[^\x00-\x7f]/) } finally { @@ -243,6 +269,64 @@ test("renders monochrome scrollback as ASCII markdown", async () => { } }) +test("renders completed subagent markdown in monochrome mode", async () => { + const out = await setup({ mono: true, width: 60 }) + + try { + await out.scrollback.append( + toolCommit({ + tool: "subagent", + phase: "final", + toolState: "completed", + state: { + status: "completed", + input: { description: "Inspect reducer", agent: "explore" }, + content: [{ type: "text", text: "# Findings\n\n- Café → stable" }], + metadata: { + sessionID: "ses-child-1", + status: "completed", + output: "# Findings\n\n- Café → stable", + }, + }, + }), + ) + + const commits = claim(out.renderer) + try { + expect(commits).toHaveLength(1) + expect(commits[0]?.trailingNewline).toBe(true) + const output = render(commits) + expect(output).toContain("# Findings") + expect(output).toContain("- Caf? -> stable") + expect(output).not.toMatch(/[^\x00-\x7f]/) + } finally { + destroy(commits) + } + } finally { + out.scrollback.destroy() + } +}) + +test("keeps fenced code monochrome when highlighting fails", async () => { + const out = await setup({ mono: true, failHighlight: true }) + + try { + await out.scrollback.append(assistant("```ts\nCafé → …\n```")) + await out.scrollback.complete() + + const commits = claim(out.renderer) + try { + const output = render(commits) + expect(output).toContain("Caf? -> ...") + expect(output).not.toMatch(/[^\x00-\x7f]/) + } finally { + destroy(commits) + } + } finally { + out.scrollback.destroy() + } +}) + function user(text: string): StreamCommit { return { kind: "user", From edaee143d9c31463bb4117645d77a481778cb135 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Fri, 24 Jul 2026 11:23:53 +0200 Subject: [PATCH 075/150] mini: reserve headroom before showing usage (#38659) --- packages/tui/src/mini/footer.width.ts | 8 +++++--- packages/tui/test/mini/footer.view.test.tsx | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/tui/src/mini/footer.width.ts b/packages/tui/src/mini/footer.width.ts index c48f7eeb15ae..f6ee290fe4f9 100644 --- a/packages/tui/src/mini/footer.width.ts +++ b/packages/tui/src/mini/footer.width.ts @@ -6,6 +6,8 @@ export function footerWidthPolicy(width: number) { } } +const USAGE_HEADROOM = 8 + export function footerStatuslinePolicy(input: { width: number mainWidth: number @@ -18,10 +20,10 @@ export function footerStatuslinePolicy(input: { }) { let remaining = input.width - input.mainWidth - (input.commandWidth ?? 0) let hasSection = input.commandWidth !== undefined - const include = (width: number | undefined) => { + const include = (width: number | undefined, headroom = 0) => { if (width === undefined) return false const required = width + (hasSection ? 3 : 1) - if (remaining < required) return false + if (remaining < required + headroom) return false remaining -= required hasSection = true return true @@ -40,7 +42,7 @@ export function footerStatuslinePolicy(input: { (showAgent || input.agentWidth === undefined) && contextComplete && (showVariant || input.variantWidth === undefined) && - include(input.usageWidth) + include(input.usageWidth, USAGE_HEADROOM) return { showAgent, diff --git a/packages/tui/test/mini/footer.view.test.tsx b/packages/tui/test/mini/footer.view.test.tsx index ae1c8322baa8..022e90623ba6 100644 --- a/packages/tui/test/mini/footer.view.test.tsx +++ b/packages/tui/test/mini/footer.view.test.tsx @@ -1522,6 +1522,27 @@ test("direct footer shows full usage metadata when room is available", async () } }) +test("direct footer omits usage when it would fill the statusline", async () => { + const app = await renderFooter({ + state: { phase: "running", model: "GPT-5.6 SoL", usage: "8.4K (1%) · $0.01" }, + currentVariant: "high", + mono: true, + width: 66, + }) + + try { + await app.renderOnce() + const frame = app.captureCharFrame() + + expect(frame).toContain("esc interrupt") + expect(frame).toContain("GPT-5.6 SoL high") + expect(frame).toContain("ctrl+p cmd") + expect(frame).not.toContain("8.4K") + } finally { + app.cleanup() + } +}) + test("direct footer hides routine activity and shows explicit notices", async () => { let status = "" const app = await renderFooter({ From c5680a206e4ef0211fee17469684ffa3d9c5a0c9 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Fri, 24 Jul 2026 18:36:35 +0530 Subject: [PATCH 076/150] refactor(ai): make OpenAI Responses extend Open Responses (#38681) --- packages/ai/AGENTS.md | 9 +- packages/ai/README.md | 2 +- packages/ai/STATUS.md | 48 +- packages/ai/src/protocols/index.ts | 1 + packages/ai/src/protocols/open-responses.ts | 949 +++++++++++++++++ packages/ai/src/protocols/openai-chat.ts | 13 +- .../protocols/openai-compatible-responses.ts | 17 +- packages/ai/src/protocols/openai-responses.ts | 993 ++---------------- .../protocols/utils/open-responses-options.ts | 65 ++ .../ai/src/protocols/utils/openai-options.ts | 84 +- .../ai/src/protocols/utils/tool-schema.ts | 3 + .../src/providers/google-vertex-responses.ts | 1 + .../src/providers/open-responses-options.ts | 20 + .../providers/openai-compatible-responses.ts | 7 +- packages/ai/src/providers/openai-options.ts | 18 +- packages/ai/src/route/protocol.ts | 3 +- packages/ai/test/exports.test.ts | 10 +- packages/ai/test/provider-package.test.ts | 8 +- .../openai-compatible-responses.test.ts | 77 +- 19 files changed, 1261 insertions(+), 1067 deletions(-) create mode 100644 packages/ai/src/protocols/open-responses.ts create mode 100644 packages/ai/src/protocols/utils/open-responses-options.ts create mode 100644 packages/ai/src/providers/open-responses-options.ts diff --git a/packages/ai/AGENTS.md b/packages/ai/AGENTS.md index 997615ce0df9..f02b5616b220 100644 --- a/packages/ai/AGENTS.md +++ b/packages/ai/AGENTS.md @@ -54,7 +54,7 @@ Filter or narrow `LLMEvent` streams with `LLMEvent.is.*` (camelCase guards, e.g. A route is the registered, runnable composition of four orthogonal pieces: -- **`Protocol`** (`src/route/protocol.ts`) — semantic API contract. Owns request body construction (`body.from`), the body schema (`body.schema`), the streaming-event schema (`stream.event`), and the event-to-`LLMEvent` state machine (`stream.step`). `Route.make(...)` validates and JSON-encodes the body from `body.schema` and decodes frames with `stream.event`. Examples: `OpenAIChat.protocol`, `OpenAIResponses.protocol`, `AnthropicMessages.protocol`, `Gemini.protocol`, `BedrockConverse.protocol`. +- **`Protocol`** (`src/route/protocol.ts`) — semantic API contract. Owns request body construction (`body.from`), the body schema (`body.schema`), the streaming-event schema (`stream.event`), and the event-to-`LLMEvent` state machine (`stream.step`). `Route.make(...)` validates and JSON-encodes the body from `body.schema` and decodes frames with `stream.event`. Examples: `OpenAIChat.protocol`, `OpenResponses.protocol`, `OpenAIResponses.protocol`, `AnthropicMessages.protocol`, `Gemini.protocol`, `BedrockConverse.protocol`. - **`Endpoint`** (`src/route/endpoint.ts`) — URL construction. The host, path, and route query live on the endpoint. `Endpoint.path("/chat/completions", { baseURL })` is the common case; pass a function for paths that embed the model id or a body field (e.g. `Endpoint.path(({ body }) => `/model/${body.modelId}/converse-stream`)`). - **`Auth`** (`src/route/auth.ts`) — per-request transport authentication. Provider facades configure credentials onto the route before model selection, usually via `Auth.bearer(apiKey)` or `Auth.header(name, apiKey)`. Routes that need per-request signing (Bedrock SigV4, future Vertex IAM, Azure AAD) implement `Auth` as a function that signs the body and merges signed headers into the result. - **`Framing`** (`src/route/framing.ts`) — bytes → frames. SSE (`Framing.sse`) is shared; Bedrock keeps its AWS event-stream framing as a typed `Framing` value alongside its protocol. @@ -158,13 +158,14 @@ packages/ai/src/ protocols/ shared.ts ProviderShared toolkit used inside protocol impls openai-chat.ts protocol + route (compose OpenAIChat.protocol) - openai-responses.ts + open-responses.ts provider-neutral Responses protocol baseline + openai-responses.ts OpenAI tools/events/transports composed over OpenResponses anthropic-messages.ts gemini.ts bedrock-converse.ts bedrock-event-stream.ts framing for AWS event-stream binary frames openai-compatible-chat.ts route that reuses OpenAIChat.protocol, no canonical URL - openai-compatible-responses.ts route that reuses OpenAIResponses.protocol, no canonical URL + openai-compatible-responses.ts deployment adapter that reuses OpenResponses.protocol, no canonical URL utils/ per-protocol helpers (auth, cache, media, tool-stream, ...) providers/ openai-compatible.ts generic Chat helper + family model helpers @@ -175,7 +176,7 @@ packages/ai/src/ tool-runtime.ts narrow one-call typed tool dispatcher ``` -The dependency arrow points down: `providers/*.ts` files import protocol routes and auth-option utilities; protocol modules import `endpoint`, `auth`, `framing`, and transport pieces. Protocols do not import provider facades. Lower-level modules know nothing about provider catalog metadata. +The dependency arrow points down: `providers/*.ts` files import protocol routes and auth-option utilities; protocol modules import `endpoint`, `auth`, `framing`, and transport pieces. Protocols do not import provider facades. Lower-level modules know nothing about provider catalog metadata. `OpenAIResponses` composes the provider-neutral `OpenResponses` protocol; the baseline never imports the OpenAI extension. ### Shared protocol helpers diff --git a/packages/ai/README.md b/packages/ai/README.md index 6e4d87611cab..597ff8eaab5b 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -300,7 +300,7 @@ OpenAI Chat and OpenAI Responses are separate semantic entrypoints: - `@opencode-ai/ai/providers/google-vertex/responses` - `@opencode-ai/ai/providers/google-vertex/messages` -Responses HTTP versus WebSocket is a scoped `transport` setting on the OpenAI Responses entrypoint, not another entrypoint. Azure follows the same Chat/Responses split at `providers/azure/chat` and `providers/azure/responses`. Generic OpenAI-compatible Chat remains at `providers/openai-compatible`; compatible Responses is separate at `providers/openai-compatible/responses`. Generic Anthropic Messages-compatible providers use `providers/anthropic-compatible`, which the named Anthropic provider composes. Google Gemini and Amazon Bedrock expose their single native API through their existing provider paths. +Responses HTTP versus WebSocket is a scoped `transport` setting on the OpenAI Responses entrypoint, not another entrypoint. Azure follows the same Chat/Responses split at `providers/azure/chat` and `providers/azure/responses`. Generic OpenAI-compatible Chat remains at `providers/openai-compatible`; the Responses adapter at `providers/openai-compatible/responses` uses the provider-neutral Open Responses protocol. OpenAI Responses extends that baseline with OpenAI tools, event variants, metadata, defaults, and transports. Generic Anthropic Messages-compatible providers use `providers/anthropic-compatible`, which the named Anthropic provider composes. Google Gemini and Amazon Bedrock expose their single native API through their existing provider paths. Vertex Gemini, Vertex Chat, Vertex Responses, and Vertex Messages are separate API entrypoints. All accept `project`, `location`, and an optional `accessToken`; when no explicit token or auth override is supplied they lazily use Google Application Default Credentials. Vertex Gemini instead selects express mode when `apiKey` or `GOOGLE_VERTEX_API_KEY` is present. Vertex Chat targets MaaS models through the OpenAI-compatible Chat Completions endpoint, while Vertex Responses targets Grok models and defaults `store` to `false` as required by Vertex. `providers/google-vertex` remains the default alias for `providers/google-vertex/gemini`. diff --git a/packages/ai/STATUS.md b/packages/ai/STATUS.md index 24c98a426ac8..0e25ca2e3fb9 100644 --- a/packages/ai/STATUS.md +++ b/packages/ai/STATUS.md @@ -1,6 +1,6 @@ # LLM Provider Parity Status -Last reviewed: 2026-07-17 +Last reviewed: 2026-07-24 This file tracks the gap between the native `@opencode-ai/ai` package and the AI SDK provider packages that opencode still depends on for many catalog/runtime paths. @@ -13,26 +13,26 @@ This file tracks the gap between the native `@opencode-ai/ai` package and the AI ## Current Implementation Snapshot -| Native slice | Source | Current state | Main gaps | -| ---------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| OpenAI Chat | `src/protocols/openai-chat.ts`, `src/providers/openai.ts` | Usable. Streams text, reasoning deltas, tool calls, usage, images, and common generation controls. | No typed structured-output / `response_format` path. Limited typed OpenAI option surface compared with SDK escape hatches. | -| OpenAI Responses HTTP | `src/protocols/openai-responses.ts`, `src/providers/openai.ts` | Usable. Supports hosted-tool event surfacing, reasoning replay metadata, GPT-5 defaults, and cache usage. | No explicit `previous_response_id` path. Typed options cover only a subset of Responses fields. Structured output is still mostly synthetic-tool based. | -| OpenAI Responses WebSocket | `src/protocols/openai-responses.ts`, `src/route/transport/websocket.ts` | Present as `OpenAI.responsesWebSocket(...)`. | Runner/catalog support explicitly must not downgrade WebSocket routes; broader runtime selection is not complete. | -| OpenAI-compatible Chat | `src/protocols/openai-compatible-chat.ts`, `src/providers/openai-compatible.ts` | Usable for generic Chat and several profiles: Baseten, Cerebras, DeepInfra, DeepSeek, Fireworks, Groq, TogetherAI. | Family quirks are mostly endpoint defaults, not full typed behavior. | -| OpenAI-compatible Responses | `src/protocols/openai-compatible-responses.ts`, `src/providers/openai-compatible-responses.ts` | Usable for deployments that implement the OpenAI Responses wire protocol. | No named family profiles or recorded deployment coverage yet. | -| Anthropic-compatible Messages | `src/protocols/anthropic-messages.ts`, `src/providers/anthropic-compatible.ts` | Usable for deployments that implement the Anthropic Messages wire protocol. Named Anthropic composes this base; MiniMax M3 has recorded text and tool-loop coverage. | No named compatible family profiles yet. | -| Anthropic Messages | `src/protocols/anthropic-messages.ts`, `src/providers/anthropic.ts` | Usable. Supports tools, thinking, cache control, images, server-hosted tool events, and usage. | Provider option surface is small. Beta/header handling, metadata, and newer Messages fields need a typed parity pass. | -| Gemini Developer API | `src/protocols/gemini.ts`, `src/providers/google.ts` | Usable for Google API key flow. Supports text, images, tools, thinking signatures, and cache usage. | This is not Vertex. Typed provider options are narrow; many Gemini request fields currently require raw `http.body` overlays. | -| Vertex Gemini | `src/protocols/gemini.ts`, `src/providers/google-vertex.ts` | Usable through API-key express mode, explicit OAuth tokens, or ADC with project/location endpoint derivation, including tuned `endpoints/...` deployments. | Core runner/catalog mapping and recorded provider coverage are missing. | -| Vertex Chat | `src/protocols/openai-chat.ts`, `src/providers/google-vertex-chat.ts` | Usable for MaaS models through OpenAI-compatible Chat Completions with explicit OAuth tokens or ADC and project/location endpoint derivation. | Core runner/catalog mapping and recorded provider coverage are missing; MaaS family-specific request parity needs review. | -| Vertex Responses | `src/protocols/openai-responses.ts`, `src/providers/google-vertex-responses.ts` | Usable for Grok models through OpenAI-compatible Responses with explicit OAuth tokens or ADC, project/location endpoint derivation, and storage disabled by default. | Core runner/catalog mapping and recorded provider coverage are missing; stateful continuation is not supported by Vertex. | -| Vertex Messages | `src/protocols/anthropic-messages.ts`, `src/providers/google-vertex-messages.ts` | Usable through explicit OAuth tokens or ADC, including global, regional, and `eu`/`us` multi-region endpoints. | Core runner/catalog mapping and recorded provider coverage are missing; Vertex-specific hosted-tool parity needs review. | -| Bedrock Converse | `src/protocols/bedrock-converse.ts`, `src/providers/amazon-bedrock.ts` | Partial but real. Supports AWS event-stream framing, SigV4 with supplied credentials, bearer auth, tools, reasoning signatures, media, cache points, and recorded tests. | Native facade does not mirror the AI SDK plugin's default AWS credential chain/profile behavior. Runner/catalog mapping is missing. Guardrails, inference profiles, region-specific model ID fixes, and model-specific request fields need a parity pass. | -| Azure OpenAI | `src/providers/azure.ts` using OpenAI Chat/Responses protocols | Partial. Supports resource/base URL setup, API key auth, API version query, Chat, and Responses selectors. | Core runner does not map `@ai-sdk/azure` to this native facade. AAD/token auth and Azure-specific endpoint variants need review. | -| Cloudflare AI Gateway / Workers AI | `src/providers/cloudflare.ts` | Present via OpenAI-compatible Chat routes. | Useful but not part of the critical AI SDK replacement set yet. Needs per-product recorded coverage before relying on it broadly. | -| OpenRouter | `src/providers/openrouter.ts` | Present with OpenRouter-specific usage/reasoning/prompt-cache options over Chat. | Responses-style OpenRouter support is absent. | -| xAI | `src/providers/xai.ts` | Present with Responses and Chat selectors. | Needs package-parity review against the AI SDK xAI provider. | -| GitHub Copilot | `src/providers/github-copilot.ts` | Present as explicit-base-URL OpenAI Chat/Responses facade. | Runtime/catalog integration remains specialized and should stay separate from public OpenAI-compatible defaults. | +| Native slice | Source | Current state | Main gaps | +| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| OpenAI Chat | `src/protocols/openai-chat.ts`, `src/providers/openai.ts` | Usable. Streams text, reasoning deltas, tool calls, usage, images, and common generation controls. | No typed structured-output / `response_format` path. Limited typed OpenAI option surface compared with SDK escape hatches. | +| OpenAI Responses HTTP | `src/protocols/open-responses.ts`, `src/protocols/openai-responses.ts`, `src/providers/openai.ts` | Usable. Extends the Open Responses baseline with hosted-tool event surfacing, reasoning replay metadata, GPT-5 defaults, and cache usage. | No explicit `previous_response_id` path. Typed options cover only a subset of Responses fields. Structured output is still mostly synthetic-tool based. | +| OpenAI Responses WebSocket | `src/protocols/openai-responses.ts`, `src/route/transport/websocket.ts` | Present as `OpenAI.responsesWebSocket(...)`. | Runner/catalog support explicitly must not downgrade WebSocket routes; broader runtime selection is not complete. | +| OpenAI-compatible Chat | `src/protocols/openai-compatible-chat.ts`, `src/providers/openai-compatible.ts` | Usable for generic Chat and several profiles: Baseten, Cerebras, DeepInfra, DeepSeek, Fireworks, Groq, TogetherAI. | Family quirks are mostly endpoint defaults, not full typed behavior. | +| Open Responses-compatible | `src/protocols/open-responses.ts`, `src/protocols/openai-compatible-responses.ts`, `src/providers/openai-compatible-responses.ts` | Usable for deployments that implement the provider-neutral Open Responses protocol. The deployment adapter does not inherit OpenAI tools, events, metadata, or defaults. | No named family profiles or recorded deployment coverage yet. | +| Anthropic-compatible Messages | `src/protocols/anthropic-messages.ts`, `src/providers/anthropic-compatible.ts` | Usable for deployments that implement the Anthropic Messages wire protocol. Named Anthropic composes this base; MiniMax M3 has recorded text and tool-loop coverage. | No named compatible family profiles yet. | +| Anthropic Messages | `src/protocols/anthropic-messages.ts`, `src/providers/anthropic.ts` | Usable. Supports tools, thinking, cache control, images, server-hosted tool events, and usage. | Provider option surface is small. Beta/header handling, metadata, and newer Messages fields need a typed parity pass. | +| Gemini Developer API | `src/protocols/gemini.ts`, `src/providers/google.ts` | Usable for Google API key flow. Supports text, images, tools, thinking signatures, and cache usage. | This is not Vertex. Typed provider options are narrow; many Gemini request fields currently require raw `http.body` overlays. | +| Vertex Gemini | `src/protocols/gemini.ts`, `src/providers/google-vertex.ts` | Usable through API-key express mode, explicit OAuth tokens, or ADC with project/location endpoint derivation, including tuned `endpoints/...` deployments. | Core runner/catalog mapping and recorded provider coverage are missing. | +| Vertex Chat | `src/protocols/openai-chat.ts`, `src/providers/google-vertex-chat.ts` | Usable for MaaS models through OpenAI-compatible Chat Completions with explicit OAuth tokens or ADC and project/location endpoint derivation. | Core runner/catalog mapping and recorded provider coverage are missing; MaaS family-specific request parity needs review. | +| Vertex Responses | `src/protocols/open-responses.ts`, `src/providers/google-vertex-responses.ts` | Usable for Grok models through Open Responses with explicit OAuth tokens or ADC, project/location endpoint derivation, and an explicit `store: false` Vertex default. | Core runner/catalog mapping and recorded provider coverage are missing; stateful continuation is not supported by Vertex. | +| Vertex Messages | `src/protocols/anthropic-messages.ts`, `src/providers/google-vertex-messages.ts` | Usable through explicit OAuth tokens or ADC, including global, regional, and `eu`/`us` multi-region endpoints. | Core runner/catalog mapping and recorded provider coverage are missing; Vertex-specific hosted-tool parity needs review. | +| Bedrock Converse | `src/protocols/bedrock-converse.ts`, `src/providers/amazon-bedrock.ts` | Partial but real. Supports AWS event-stream framing, SigV4 with supplied credentials, bearer auth, tools, reasoning signatures, media, cache points, and recorded tests. | Native facade does not mirror the AI SDK plugin's default AWS credential chain/profile behavior. Runner/catalog mapping is missing. Guardrails, inference profiles, region-specific model ID fixes, and model-specific request fields need a parity pass. | +| Azure OpenAI | `src/providers/azure.ts` using OpenAI Chat/Responses protocols | Partial. Supports resource/base URL setup, API key auth, API version query, Chat, and Responses selectors. | Core runner does not map `@ai-sdk/azure` to this native facade. AAD/token auth and Azure-specific endpoint variants need review. | +| Cloudflare AI Gateway / Workers AI | `src/providers/cloudflare.ts` | Present via OpenAI-compatible Chat routes. | Useful but not part of the critical AI SDK replacement set yet. Needs per-product recorded coverage before relying on it broadly. | +| OpenRouter | `src/providers/openrouter.ts` | Present with OpenRouter-specific usage/reasoning/prompt-cache options over Chat. | Responses-style OpenRouter support is absent. | +| xAI | `src/providers/xai.ts` | Present with Responses and Chat selectors. | Needs package-parity review against the AI SDK xAI provider. | +| GitHub Copilot | `src/providers/github-copilot.ts` | Present as explicit-base-URL OpenAI Chat/Responses facade. | Runtime/catalog integration remains specialized and should stay separate from public OpenAI-compatible defaults. | ## V2 Runner Status @@ -65,7 +65,7 @@ Other `aisdk:` packages, including Google Vertex, Azure, and Bedrock, currently ## Highest-Risk Gaps 1. Runner support is narrower than the LLM package. The package has native provider facades for Google, Azure, and Bedrock, but the V2 Session runner only maps OpenAI, Anthropic, and explicit OpenAI-compatible Chat from `aisdk` catalog metadata. -2. OpenAI-compatible Responses is available as a separate package entrypoint, but the V2 runner still maps `@ai-sdk/openai-compatible` to Chat only. Catalog selection must become API-aware before Responses deployments can use it. +2. The Open Responses adapter is available through a separate package entrypoint, but the V2 runner still maps `@ai-sdk/openai-compatible` to Chat only. Catalog selection must become API-aware before Responses deployments can use it. 3. Bedrock native auth is not AI SDK parity. The AI SDK plugin uses the default AWS provider chain, profile, container credentials, and Bedrock bearer token env behavior. Native Bedrock currently expects explicit credentials or bearer auth on the facade. 4. Vertex Gemini, Vertex Chat, Vertex Responses, and Vertex Messages now have native package entrypoints, but the core runner does not map catalog metadata to them yet and recorded provider coverage is still missing. 5. Azure is only a provider facade, not a full runtime replacement. Native Azure exists, but the catalog runner does not select it, and token auth/resource variants need review. @@ -83,13 +83,13 @@ These are implementation/API slices, not separate npm packages. | OpenAI Chat | `@opencode-ai/ai/providers/openai/chat` | OpenAI `/chat/completions` semantics. | | OpenAI Responses | `@opencode-ai/ai/providers/openai/responses` | OpenAI `/responses` semantics with HTTP/WebSocket selected through settings. | | OpenAI-compatible Chat | `@opencode-ai/ai/providers/openai-compatible` | Generic OpenAI-compatible `/chat/completions`. | -| OpenAI-compatible Responses | `@opencode-ai/ai/providers/openai-compatible/responses` | Generic OpenAI-compatible `/responses`. | +| Open Responses-compatible | `@opencode-ai/ai/providers/openai-compatible/responses` | Generic provider-neutral `/responses`. | | Anthropic-compatible Messages | `@opencode-ai/ai/providers/anthropic-compatible` | Generic Anthropic-compatible `/messages`. | | Anthropic Messages | `@opencode-ai/ai/providers/anthropic` | Anthropic Messages API. | | Gemini Developer API | `@opencode-ai/ai/providers/google` | Google AI Studio Gemini API. | | Vertex Gemini | `@opencode-ai/ai/providers/google-vertex/gemini` | Vertex Gemini API; `providers/google-vertex` is the default alias. | | Vertex Chat | `@opencode-ai/ai/providers/google-vertex/chat` | Vertex OpenAI-compatible Chat Completions for MaaS models. | -| Vertex Responses | `@opencode-ai/ai/providers/google-vertex/responses` | Vertex OpenAI-compatible Responses for Grok models. | +| Vertex Responses | `@opencode-ai/ai/providers/google-vertex/responses` | Vertex Open Responses for Grok models. | | Vertex Messages | `@opencode-ai/ai/providers/google-vertex/messages` | Vertex-hosted Anthropic Messages API. | | Bedrock Converse | `@opencode-ai/ai/providers/amazon-bedrock` | AWS Bedrock Converse API. | | Bedrock Mantle | Missing | AWS Bedrock Mantle OpenAI-compatible APIs. | diff --git a/packages/ai/src/protocols/index.ts b/packages/ai/src/protocols/index.ts index bd3ee8eeec1d..b677e1a59910 100644 --- a/packages/ai/src/protocols/index.ts +++ b/packages/ai/src/protocols/index.ts @@ -6,3 +6,4 @@ export * as OpenAIImages from "./openai-images" export * as OpenAICompatibleChat from "./openai-compatible-chat" export * as OpenAICompatibleResponses from "./openai-compatible-responses" export * as OpenAIResponses from "./openai-responses" +export * as OpenResponses from "./open-responses" diff --git a/packages/ai/src/protocols/open-responses.ts b/packages/ai/src/protocols/open-responses.ts new file mode 100644 index 000000000000..4cd545731965 --- /dev/null +++ b/packages/ai/src/protocols/open-responses.ts @@ -0,0 +1,949 @@ +import { Effect, Schema } from "effect" +import { HttpTransport } from "../route/transport" +import { Protocol } from "../route/protocol" +import { + LLMError, + LLMEvent, + Usage, + type FinishReason, + type JsonSchema, + type LLMRequest, + type MediaPart, + type ProviderMetadata, + type ReasoningPart, + type TextPart, + type ToolCallPart, + type ToolDefinition, + type ToolContent, + type ToolResultPart, +} from "../schema" +import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" +import { classifyProviderFailure } from "../provider-error" +import { OpenResponsesOptions } from "./utils/open-responses-options" +import { Lifecycle } from "./utils/lifecycle" +import { ToolSchemaProjection } from "./utils/tool-schema" +import { ToolStream } from "./utils/tool-stream" + +const ADAPTER = "open-responses" +const NAME = "Open Responses" +const MEDIA_MIMES = new Set([...ProviderShared.IMAGE_MIMES, ...ProviderShared.PDF_MIMES]) +export const PATH = "/responses" + +// ============================================================================= +// Request Body Schema +// ============================================================================= +const OpenResponsesInputText = Schema.Struct({ + type: Schema.tag("input_text"), + text: Schema.String, +}) +const OpenResponsesInputImage = Schema.Struct({ + type: Schema.tag("input_image"), + image_url: Schema.String, +}) +const OpenResponsesInputFile = Schema.Struct({ + type: Schema.tag("input_file"), + filename: Schema.String, + file_data: Schema.String, + mime_type: Schema.optional(Schema.String), +}) +const MediaInput = Schema.Union([OpenResponsesInputImage, OpenResponsesInputFile]) +export type MediaInput = Schema.Schema.Type +const OpenResponsesInputContent = Schema.Union([OpenResponsesInputText, MediaInput]) + +const OpenResponsesOutputText = Schema.Struct({ + type: Schema.tag("output_text"), + text: Schema.String, +}) + +const OpenResponsesReasoningSummaryText = Schema.Struct({ + type: Schema.tag("summary_text"), + text: Schema.String, +}) + +const OpenResponsesReasoningItem = Schema.Struct({ + type: Schema.tag("reasoning"), + id: Schema.optionalKey(Schema.String), + summary: Schema.Array(OpenResponsesReasoningSummaryText), + encrypted_content: optionalNull(Schema.String), +}) + +const OpenResponsesItemReference = Schema.Struct({ + type: Schema.tag("item_reference"), + id: Schema.String, +}) + +// `function_call_output.output` accepts either a plain string or an ordered +// array of content items so tools can return images and files in addition to text. +// https://www.openresponses.org/reference +const OpenResponsesFunctionCallOutputContent = Schema.Union([ + OpenResponsesInputText, + OpenResponsesInputImage, + OpenResponsesInputFile, +]) + +const OpenResponsesFunctionCallOutput = Schema.Union([ + Schema.String, + Schema.Array(OpenResponsesFunctionCallOutputContent), +]) + +const OpenResponsesInputItem = Schema.Union([ + Schema.Struct({ role: Schema.tag("system"), content: Schema.String }), + Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenResponsesInputContent) }), + Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenResponsesOutputText) }), + OpenResponsesReasoningItem, + OpenResponsesItemReference, + Schema.Struct({ + type: Schema.tag("function_call"), + call_id: Schema.String, + name: Schema.String, + arguments: Schema.String, + }), + Schema.Struct({ + type: Schema.tag("function_call_output"), + call_id: Schema.String, + output: OpenResponsesFunctionCallOutput, + }), +]) +type OpenResponsesInputItem = Schema.Schema.Type + +// Mutable counterpart of the schema reasoning item so `lowerMessages` can fold +// multiple streamed summary parts into the same item before flushing. +type OpenResponsesReasoningInput = { + type: "reasoning" + id: string + summary: Array<{ type: "summary_text"; text: string }> + encrypted_content?: string | null +} +type OpenResponsesReasoningReplay = Omit + +export const Tool = Schema.Struct({ + type: Schema.tag("function"), + name: Schema.String, + description: Schema.String, + parameters: JsonObject, + strict: Schema.optional(Schema.Boolean), +}) + +export const ToolChoice = Schema.Union([ + Schema.Literals(["auto", "none", "required"]), + Schema.Struct({ type: Schema.tag("function"), name: Schema.String }), +]) + +// Fields shared between the HTTP body and the WebSocket `response.create` +// message. The HTTP body adds `stream: true`; the WebSocket message adds +// `type: "response.create"`. Defining the shared shape once keeps the two +// transports in sync without a destructure-and-strip dance. +export const coreFields = { + model: Schema.String, + input: Schema.Array(OpenResponsesInputItem), + instructions: Schema.optional(Schema.String), + tools: optionalArray(Tool), + tool_choice: Schema.optional(ToolChoice), + store: Schema.optional(Schema.Boolean), + service_tier: Schema.optional(OpenResponsesOptions.ServiceTierSchema), + prompt_cache_key: Schema.optional(Schema.String), + include: optionalArray(OpenResponsesOptions.ResponseIncludableSchema), + reasoning: Schema.optional( + Schema.Struct({ + effort: Schema.optional(OpenResponsesOptions.ReasoningEffort), + summary: Schema.optional(Schema.Literals(["auto", "concise", "detailed"])), + }), + ), + text: Schema.optional( + Schema.Struct({ + verbosity: Schema.optional(OpenResponsesOptions.TextVerbositySchema), + }), + ), + max_output_tokens: Schema.optional(Schema.Number), + temperature: Schema.optional(Schema.Number), + top_p: Schema.optional(Schema.Number), +} + +const OpenResponsesBody = Schema.Struct({ + ...coreFields, + stream: Schema.Literal(true), +}) +export type OpenResponsesBody = Schema.Schema.Type + +const OpenResponsesUsage = Schema.Struct({ + input_tokens: Schema.optional(Schema.Number), + input_tokens_details: optionalNull(Schema.Struct({ cached_tokens: Schema.optional(Schema.Number) })), + output_tokens: Schema.optional(Schema.Number), + output_tokens_details: optionalNull(Schema.Struct({ reasoning_tokens: Schema.optional(Schema.Number) })), + total_tokens: Schema.optional(Schema.Number), +}) +type OpenResponsesUsage = Schema.Schema.Type + +export const StreamItem = Schema.StructWithRest( + Schema.Struct({ + type: Schema.String, + id: Schema.optional(Schema.String), + call_id: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + arguments: Schema.optional(Schema.String), + encrypted_content: optionalNull(Schema.String), + }), + [Schema.Record(Schema.String, Schema.Unknown)], +) +export type StreamItem = Schema.Schema.Type + +// The Responses schema puts streaming error details at the top level and +// response failures under `response.error`. WebSocket failures use an +// event-level `error` envelope, so accept all three shapes here. +// https://www.openresponses.org/specification +const OpenResponsesErrorPayload = Schema.Struct({ + code: optionalNull(Schema.String), + message: optionalNull(Schema.String), + param: optionalNull(Schema.String), +}) + +export const Event = Schema.StructWithRest( + Schema.Struct({ + type: Schema.String, + delta: Schema.optional(Schema.String), + item_id: Schema.optional(Schema.String), + summary_index: Schema.optional(Schema.Number), + item: Schema.optional(StreamItem), + response: Schema.optional( + Schema.StructWithRest( + Schema.Struct({ + id: Schema.optional(Schema.String), + service_tier: optionalNull(Schema.String), + incomplete_details: optionalNull(Schema.Struct({ reason: Schema.optional(Schema.String) })), + usage: optionalNull(OpenResponsesUsage), + error: optionalNull(OpenResponsesErrorPayload), + }), + [Schema.Record(Schema.String, Schema.Unknown)], + ), + ), + code: optionalNull(Schema.String), + message: Schema.optional(Schema.String), + param: optionalNull(Schema.String), + error: optionalNull(OpenResponsesErrorPayload), + }), + [Schema.Record(Schema.String, Schema.Unknown)], +) +export type Event = Schema.Schema.Type + +export interface Extension { + readonly id: string + readonly name: string + readonly lowerMedia?: (input: { + readonly part: MediaPart + readonly media: ProviderShared.ValidatedMedia + readonly request: LLMRequest + }) => MediaInput | undefined +} + +const BASE: Extension = { id: ADAPTER, name: NAME } + +export interface ParserState { + readonly id: string + readonly name: string + readonly providerMetadataKey: string + readonly tools: ToolStream.State + readonly hasFunctionCall: boolean + readonly lifecycle: Lifecycle.State + readonly reasoningItems: Readonly> + readonly store: boolean | undefined +} + +type ReasoningSummaryStatus = "active" | "can-conclude" | "concluded" + +interface ReasoningStreamItem { + readonly encryptedContent: string | null | undefined + // Keyed by the wire protocol's numeric `summary_index`. JS object keys coerce to + // strings, but typing the map as `Record` documents intent + // and matches the wire field. + readonly summaryParts: Readonly> +} + +// ============================================================================= +// Request Lowering +// ============================================================================= +export const lowerTool = Effect.fn("OpenResponses.lowerTool")(function* ( + protocolName: string, + tool: ToolDefinition, + inputSchema: JsonSchema, +) { + if (tool.native !== undefined) + return yield* ProviderShared.invalidRequest(`${protocolName} does not support provider-native tool ${tool.name}`) + return { + type: "function" as const, + name: tool.name, + description: tool.description, + parameters: ToolSchemaProjection.responses(inputSchema), + // TODO: Read this from Responses tool options so direct LLM callers can opt into strict schemas. + strict: false, + } +}) + +export const lowerToolChoice = (protocolName: string, toolChoice: NonNullable) => + ProviderShared.matchToolChoice(protocolName, toolChoice, { + auto: () => "auto" as const, + none: () => "none" as const, + required: () => "required" as const, + tool: (toolName) => ({ type: "function" as const, name: toolName }), + }) + +const lowerToolCall = (part: ToolCallPart): OpenResponsesInputItem => ({ + type: "function_call", + call_id: part.id, + name: part.name, + arguments: ProviderShared.encodeJson(part.input), +}) + +const lowerReasoning = (part: ReasoningPart, providerMetadataKey: string): OpenResponsesReasoningInput | undefined => { + const metadata = part.providerMetadata?.[providerMetadataKey] + if (!ProviderShared.isRecord(metadata) || typeof metadata.itemId !== "string" || metadata.itemId.length === 0) + return undefined + const encryptedContent = + typeof metadata.reasoningEncryptedContent === "string" || metadata.reasoningEncryptedContent === null + ? metadata.reasoningEncryptedContent + : undefined + return { + type: "reasoning", + id: metadata.itemId, + summary: part.text.length > 0 ? [{ type: "summary_text", text: part.text }] : [], + encrypted_content: encryptedContent, + } +} + +const hostedToolItemID = (part: ToolResultPart, providerMetadataKey: string) => { + const metadata = part.providerMetadata?.[providerMetadataKey] + return ProviderShared.isRecord(metadata) && typeof metadata.itemId === "string" && metadata.itemId.length > 0 + ? metadata.itemId + : undefined +} + +const lowerMedia = Effect.fn("OpenResponses.lowerMedia")(function* ( + part: MediaPart, + request: LLMRequest, + extension: Extension, +) { + const media = yield* ProviderShared.validateMedia(extension.name, part, MEDIA_MIMES) + const extended = extension.lowerMedia?.({ part, media, request }) + if (extended) return extended + if (media.mime === "application/pdf") { + return { + type: "input_file" as const, + filename: part.filename ?? "document.pdf", + file_data: media.dataUrl, + } + } + return { type: "input_image" as const, image_url: media.dataUrl } +}) + +const lowerUserContent = Effect.fn("OpenResponses.lowerUserContent")(function* ( + part: LLMRequest["messages"][number]["content"][number], + request: LLMRequest, + extension: Extension, +) { + if (part.type === "text") return { type: "input_text" as const, text: part.text } + if (part.type === "media") return yield* lowerMedia(part, request, extension) + return yield* ProviderShared.unsupportedContent(extension.name, "user", ["text", "media"]) +}) + +// Tool results may carry structured text, images, and files. Keep media as provider-native +// content instead of JSON-stringifying base64 into a prompt string. +const lowerToolResultContentItem = Effect.fn("OpenResponses.lowerToolResultContentItem")(function* ( + item: ToolContent, + request: LLMRequest, + extension: Extension, +) { + if (item.type === "text") return { type: "input_text" as const, text: item.text } + return yield* lowerMedia( + { type: "media", mediaType: item.mime, data: item.uri, filename: item.name }, + request, + extension, + ) +}) + +const lowerToolResultOutput = Effect.fn("OpenResponses.lowerToolResultOutput")(function* ( + part: ToolResultPart, + request: LLMRequest, + extension: Extension, +) { + // Text/json/error results are encoded as a plain string for backward + // compatibility with existing cassettes and provider expectations. + if (part.result.type !== "content") return ProviderShared.toolResultText(part) + // Preserve the narrowed array element type when compiled through a consumer package. + const content: ReadonlyArray = part.result.value + return yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, extension)) +}) + +const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (request: LLMRequest, extension: Extension) { + const system: OpenResponsesInputItem[] = + request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }] + const input: OpenResponsesInputItem[] = [...system] + const store = OpenResponsesOptions.resolve(request).store + const providerMetadataKey = request.model.route.providerMetadataKey ?? "openresponses" + + for (const message of request.messages) { + if (message.role === "system") { + const part = yield* ProviderShared.wrappedSystemUpdate(extension.name, message) + const previous = input.at(-1) + if (previous && "role" in previous && previous.role === "user") + input[input.length - 1] = { + role: "user", + content: [...previous.content, { type: "input_text", text: part.text }], + } + else input.push({ role: "user", content: [{ type: "input_text", text: part.text }] }) + continue + } + + if (message.role === "user") { + input.push({ + role: "user", + content: yield* Effect.forEach(message.content, (part) => lowerUserContent(part, request, extension)), + }) + continue + } + + if (message.role === "assistant") { + const content: TextPart[] = [] + const reasoningItems: Record = {} + const reasoningReferences = new Set() + const hostedToolReferences = new Set() + const flushText = () => { + if (content.length === 0) return + input.push({ role: "assistant", content: content.map((part) => ({ type: "output_text", text: part.text })) }) + content.splice(0, content.length) + } + for (const part of message.content) { + if (part.type === "text") { + content.push(part) + continue + } + if (part.type === "reasoning") { + flushText() + const reasoning = lowerReasoning(part, providerMetadataKey) + if (!reasoning) continue + if (store !== false) { + if (!reasoningReferences.has(reasoning.id)) input.push({ type: "item_reference", id: reasoning.id }) + reasoningReferences.add(reasoning.id) + continue + } + const existing = reasoningItems[reasoning.id] + if (existing) { + existing.summary.push(...reasoning.summary) + if (typeof reasoning.encrypted_content === "string") + existing.encrypted_content = reasoning.encrypted_content + continue + } + const replay = { + type: reasoning.type, + summary: reasoning.summary, + encrypted_content: reasoning.encrypted_content, + } + reasoningItems[reasoning.id] = replay + input.push(replay) + continue + } + if (part.type === "tool-call") { + flushText() + if (part.providerExecuted === true) continue + input.push(lowerToolCall(part)) + continue + } + if (part.type === "tool-result" && part.providerExecuted === true) { + flushText() + const itemID = hostedToolItemID(part, providerMetadataKey) + if (store !== false && itemID && !hostedToolReferences.has(itemID)) + input.push({ type: "item_reference", id: itemID }) + if (store === false && part.result.type === "content") { + const content: ReadonlyArray = part.result.value + input.push({ + role: "user", + content: yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, extension)), + }) + } + if (itemID) hostedToolReferences.add(itemID) + continue + } + return yield* ProviderShared.unsupportedContent(extension.name, "assistant", [ + "text", + "reasoning", + "tool-call", + "tool-result", + ]) + } + flushText() + continue + } + + for (const part of message.content) { + if (!ProviderShared.supportsContent(part, ["tool-result"])) + return yield* ProviderShared.unsupportedContent(extension.name, "tool", ["tool-result"]) + input.push({ + type: "function_call_output", + call_id: part.id, + output: yield* lowerToolResultOutput(part, request, extension), + }) + } + } + + // With store:false, Responses APIs only accept previous reasoning items when the + // complete item has encrypted state. Summary blocks for one item may carry + // that state only on the last block, so filter after they have been joined. + return store === false + ? input.filter( + (item) => !("type" in item) || item.type !== "reasoning" || typeof item.encrypted_content === "string", + ) + : input +}) + +const lowerOptions = (request: LLMRequest) => { + const options = OpenResponsesOptions.resolve(request) + return { + ...(options.instructions ? { instructions: options.instructions } : {}), + ...(options.store !== undefined ? { store: options.store } : {}), + ...(options.promptCacheKey ? { prompt_cache_key: options.promptCacheKey } : {}), + ...(options.include ? { include: options.include } : {}), + ...(options.reasoningEffort || options.reasoningSummary + ? { reasoning: { effort: options.reasoningEffort, summary: options.reasoningSummary } } + : {}), + ...(options.textVerbosity ? { text: { verbosity: options.textVerbosity } } : {}), + ...(options.serviceTier ? { service_tier: options.serviceTier } : {}), + } +} + +export const fromRequest = Effect.fn("OpenResponses.fromRequest")(function* ( + request: LLMRequest, + extension: Extension = BASE, +) { + const generation = request.generation + const toolSchemaCompatibility = request.model.compatibility?.toolSchema + return { + model: request.model.id, + input: yield* lowerMessages(request, extension), + tools: + request.tools.length === 0 + ? undefined + : yield* Effect.forEach(request.tools, (tool) => + lowerTool( + extension.name, + tool, + ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility), + ), + ), + tool_choice: request.toolChoice ? yield* lowerToolChoice(extension.name, request.toolChoice) : undefined, + stream: true as const, + max_output_tokens: generation?.maxTokens, + temperature: generation?.temperature, + top_p: generation?.topP, + ...lowerOptions(request), + } +}) + +// ============================================================================= +// Stream Parsing +// ============================================================================= +// Responses APIs report `input_tokens` (inclusive total) with a +// `cached_tokens` subset, and `output_tokens` (inclusive total) with a +// `reasoning_tokens` subset. Pass the totals through and derive the +// non-cached breakdown. +const mapUsage = (usage: OpenResponsesUsage | null | undefined, providerMetadataKey: string) => { + if (!usage) return undefined + const cached = usage.input_tokens_details?.cached_tokens + const reasoning = usage.output_tokens_details?.reasoning_tokens + const nonCached = ProviderShared.subtractTokens(usage.input_tokens, cached) + return new Usage({ + inputTokens: usage.input_tokens, + outputTokens: usage.output_tokens, + nonCachedInputTokens: nonCached, + cacheReadInputTokens: cached, + reasoningTokens: reasoning, + totalTokens: ProviderShared.totalTokens(usage.input_tokens, usage.output_tokens, usage.total_tokens), + providerMetadata: { [providerMetadataKey]: usage }, + }) +} + +const mapFinishReason = (event: Event, hasFunctionCall: boolean): FinishReason => { + const reason = event.response?.incomplete_details?.reason + if (reason === undefined || reason === null) { + if (hasFunctionCall) return "tool-calls" + if (event.type === "response.incomplete") return "unknown" + return "stop" + } + if (reason === "max_output_tokens") return "length" + if (reason === "content_filter") return "content-filter" + return hasFunctionCall ? "tool-calls" : "unknown" +} + +export const providerMetadata = (state: ParserState, metadata: Record): ProviderMetadata => ({ + [state.providerMetadataKey]: metadata, +}) + +const isReasoningItem = (item: StreamItem): item is StreamItem & { type: "reasoning"; id: string } => + item.type === "reasoning" && typeof item.id === "string" && item.id.length > 0 + +export type StepResult = readonly [ParserState, ReadonlyArray] + +const NO_EVENTS: StepResult["1"] = [] + +// `response.completed` / `response.incomplete` are clean finishes that emit a +// `finish` event; `response.failed` is a hard failure. All three end the stream, +// so keep this set aligned with `step` and the protocol's terminal predicate. +const TERMINAL_TYPES = new Set(["response.completed", "response.incomplete", "response.failed"]) +export const terminal = (event: Event) => TERMINAL_TYPES.has(event.type) + +const onOutputTextDelta = (state: ParserState, event: Event): StepResult => { + if (!event.delta) return [state, NO_EVENTS] + const events: LLMEvent[] = [] + return [ + { ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, event.item_id ?? "text-0", event.delta) }, + events, + ] +} + +const onOutputTextDone = (state: ParserState, event: Event): StepResult => { + const events: LLMEvent[] = [] + return [{ ...state, lifecycle: Lifecycle.textEnd(state.lifecycle, events, event.item_id ?? "text-0") }, events] +} + +export const onReasoningDelta = (state: ParserState, event: Event): StepResult => { + if (!event.delta) return [state, NO_EVENTS] + const events: LLMEvent[] = [] + const itemID = event.item_id ?? "reasoning-0" + const id = + event.summary_index !== undefined || state.reasoningItems[itemID] ? `${itemID}:${event.summary_index ?? 0}` : itemID + return [ + { + ...state, + lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, id, event.delta), + }, + events, + ] +} + +export const onReasoningDone = (state: ParserState, _event: Event): StepResult => [state, NO_EVENTS] + +const reasoningMetadata = (state: ParserState, item: StreamItem & { id: string }) => + providerMetadata(state, { itemId: item.id, reasoningEncryptedContent: item.encrypted_content ?? null }) + +// Responses APIs stream reasoning items in a stable order: +// `output_item.added` (reasoning) → +// `reasoning_summary_part.added` (index=0) → +// `reasoning_summary_text.delta` → +// `reasoning_summary_part.done` (index=0) → +// (repeat for index>0) → +// `output_item.done` (reasoning). +// The handlers below rely on this ordering: `onOutputItemAdded` seeds the +// per-item entry, `onReasoningSummaryPartAdded` for `summary_index === 0` +// short-circuits when the entry already exists, and higher-index handlers +// fold against the same entry. Behaviour for out-of-order events is +// best-effort, not guaranteed. +const onOutputItemAdded = (state: ParserState, event: Event): StepResult => { + const item = event.item + if (item && isReasoningItem(item)) { + const events: LLMEvent[] = [] + return [ + { + ...state, + lifecycle: Lifecycle.reasoningStart(state.lifecycle, events, `${item.id}:0`, reasoningMetadata(state, item)), + reasoningItems: { + ...state.reasoningItems, + [item.id]: { encryptedContent: item.encrypted_content, summaryParts: { 0: "active" } }, + }, + }, + events, + ] + } + if (item?.type !== "function_call" || !item.id) return [state, NO_EVENTS] + const metadata = providerMetadata(state, { itemId: item.id }) + const events: LLMEvent[] = [] + const lifecycle = Lifecycle.stepStart(state.lifecycle, events) + return [ + { + ...state, + lifecycle, + tools: ToolStream.start(state.tools, item.id, { + id: item.call_id ?? item.id, + name: item.name ?? "", + input: item.arguments ?? "", + providerMetadata: metadata, + }), + }, + [ + ...events, + LLMEvent.toolInputStart({ id: item.call_id ?? item.id, name: item.name ?? "", providerMetadata: metadata }), + ], + ] +} + +const onReasoningSummaryPartAdded = (state: ParserState, event: Event): StepResult => { + if (!event.item_id || event.summary_index === undefined) return [state, NO_EVENTS] + const item = state.reasoningItems[event.item_id] ?? { encryptedContent: undefined, summaryParts: {} } + if (event.summary_index === 0) { + if (state.reasoningItems[event.item_id]) return [state, NO_EVENTS] + const events: LLMEvent[] = [] + return [ + { + ...state, + lifecycle: Lifecycle.reasoningStart( + state.lifecycle, + events, + `${event.item_id}:0`, + providerMetadata(state, { itemId: event.item_id, reasoningEncryptedContent: null }), + ), + reasoningItems: { + ...state.reasoningItems, + [event.item_id]: { ...item, summaryParts: { 0: "active" } }, + }, + }, + events, + ] + } + + const events: LLMEvent[] = [] + const closed = Object.entries(item.summaryParts) + .filter((entry) => entry[1] === "can-conclude") + .reduce( + (lifecycle, entry) => + Lifecycle.reasoningEnd( + lifecycle, + events, + `${event.item_id}:${entry[0]}`, + providerMetadata(state, { itemId: event.item_id }), + ), + state.lifecycle, + ) + return [ + { + ...state, + lifecycle: Lifecycle.reasoningStart( + closed, + events, + `${event.item_id}:${event.summary_index}`, + providerMetadata(state, { itemId: event.item_id, reasoningEncryptedContent: item.encryptedContent ?? null }), + ), + reasoningItems: { + ...state.reasoningItems, + [event.item_id]: { + ...item, + summaryParts: { + ...Object.fromEntries( + Object.entries(item.summaryParts).map((entry) => + entry[1] === "can-conclude" ? [entry[0], "concluded" as const] : entry, + ), + ), + [event.summary_index]: "active", + }, + }, + }, + }, + events, + ] +} + +const onReasoningSummaryPartDone = (state: ParserState, event: Event): StepResult => { + if (!event.item_id || event.summary_index === undefined) return [state, NO_EVENTS] + const item = state.reasoningItems[event.item_id] + if (!item) return [state, NO_EVENTS] + const events: LLMEvent[] = [] + return [ + { + ...state, + lifecycle: + state.store !== false + ? Lifecycle.reasoningEnd( + state.lifecycle, + events, + `${event.item_id}:${event.summary_index}`, + providerMetadata(state, { itemId: event.item_id }), + ) + : state.lifecycle, + reasoningItems: { + ...state.reasoningItems, + [event.item_id]: { + ...item, + summaryParts: { + ...item.summaryParts, + [event.summary_index]: state.store !== false ? "concluded" : "can-conclude", + }, + }, + }, + }, + events, + ] +} + +const onFunctionCallArgumentsDelta = Effect.fn("OpenResponses.onFunctionCallArgumentsDelta")(function* ( + state: ParserState, + event: Event, +) { + if (!event.item_id || !event.delta) return [state, NO_EVENTS] satisfies StepResult + const result = ToolStream.appendExisting( + state.id, + state.tools, + event.item_id, + event.delta, + `${state.name} tool argument delta is missing its tool call`, + ) + if (ToolStream.isError(result)) return yield* result + const events: LLMEvent[] = [] + const lifecycle = result.events.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle + events.push(...result.events) + return [{ ...state, lifecycle, tools: result.tools }, events] satisfies StepResult +}) + +const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (state: ParserState, event: Event) { + const item = event.item + if (!item) return [state, NO_EVENTS] satisfies StepResult + + if (item.type === "message" && item.id) return onOutputTextDone(state, { ...event, item_id: item.id }) + + if (item.type === "function_call") { + if (!item.id || !item.call_id || !item.name) return [state, NO_EVENTS] satisfies StepResult + const tools = state.tools[item.id] + ? state.tools + : ToolStream.start(state.tools, item.id, { id: item.call_id, name: item.name }) + const result = + item.arguments === undefined + ? yield* ToolStream.finish(state.id, tools, item.id) + : yield* ToolStream.finishWithInput(state.id, tools, item.id, item.arguments) + const events: LLMEvent[] = [] + const resultEvents = result.events ?? [] + const lifecycle = resultEvents.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle + events.push(...resultEvents) + return [ + { + ...state, + lifecycle, + hasFunctionCall: + resultEvents.some((event) => LLMEvent.is.toolCall(event) || LLMEvent.is.toolInputError(event)) || + state.hasFunctionCall, + tools: result.tools, + }, + events, + ] satisfies StepResult + } + + if (isReasoningItem(item)) { + const events: LLMEvent[] = [] + const metadata = reasoningMetadata(state, item) + const reasoningItem = state.reasoningItems[item.id] + if (reasoningItem) { + const lifecycle = Object.entries(reasoningItem.summaryParts) + .filter((entry) => entry[1] === "active" || entry[1] === "can-conclude") + .reduce( + (lifecycle, entry) => Lifecycle.reasoningEnd(lifecycle, events, `${item.id}:${entry[0]}`, metadata), + state.lifecycle, + ) + const { [item.id]: _removed, ...reasoningItems } = state.reasoningItems + return [{ ...state, lifecycle, reasoningItems }, events] satisfies StepResult + } + if (!state.lifecycle.reasoning.has(item.id)) { + const lifecycle = Lifecycle.stepStart(state.lifecycle, events) + events.push(LLMEvent.reasoningStart({ id: item.id, providerMetadata: metadata })) + events.push(LLMEvent.reasoningEnd({ id: item.id, providerMetadata: metadata })) + return [{ ...state, lifecycle }, events] satisfies StepResult + } + return [ + { ...state, lifecycle: Lifecycle.reasoningEnd(state.lifecycle, events, item.id, metadata) }, + events, + ] satisfies StepResult + } + + return [state, NO_EVENTS] satisfies StepResult +}) + +const onResponseFinish = (state: ParserState, event: Event): StepResult => { + const events: LLMEvent[] = [] + const lifecycle = Lifecycle.finish(state.lifecycle, events, { + reason: { + normalized: mapFinishReason(event, state.hasFunctionCall), + raw: event.response?.incomplete_details?.reason, + }, + usage: mapUsage(event.response?.usage, state.providerMetadataKey), + providerMetadata: + event.response?.id || event.response?.service_tier + ? providerMetadata(state, { + responseId: event.response.id, + serviceTier: event.response.service_tier, + }) + : undefined, + }) + return [{ ...state, lifecycle }, events] +} + +// Build a single human-readable message from whatever the provider supplied. +// When both code and message are present, prefix the code so consumers see +// the failure mode (e.g. `rate_limit_exceeded: Slow down`) instead of just +// the bare message — production rate limits and context-length failures used +// to be indistinguishable from generic stream drops. +const providerErrorMessage = (event: Event, fallback: string): string => { + const nested = event.error ?? event.response?.error ?? undefined + const message = event.message || nested?.message || undefined + const code = event.code || nested?.code || undefined + if (message && code) return `${code}: ${message}` + return message || code || fallback +} + +const providerError = (state: ParserState, event: Event, fallback: string) => { + const code = event.code || event.error?.code || event.response?.error?.code || undefined + const message = providerErrorMessage(event, fallback) + return new LLMError({ + module: state.id, + method: "stream", + reason: classifyProviderFailure({ message, code }), + }) +} + +export const step = (state: ParserState, event: Event) => { + if (event.type === "response.output_text.delta") return Effect.succeed(onOutputTextDelta(state, event)) + if (event.type === "response.output_text.done") return Effect.succeed(onOutputTextDone(state, event)) + if (event.type === "response.reasoning.delta" || event.type === "response.reasoning_summary_text.delta") + return Effect.succeed(onReasoningDelta(state, event)) + if (event.type === "response.reasoning.done" || event.type === "response.reasoning_summary_text.done") + return Effect.succeed(onReasoningDone(state, event)) + if (event.type === "response.reasoning_summary_part.added") + return Effect.succeed(onReasoningSummaryPartAdded(state, event)) + if (event.type === "response.reasoning_summary_part.done") + return Effect.succeed(onReasoningSummaryPartDone(state, event)) + if (event.type === "response.output_item.added") return Effect.succeed(onOutputItemAdded(state, event)) + if (event.type === "response.function_call_arguments.delta") return onFunctionCallArgumentsDelta(state, event) + if (event.type === "response.output_item.done") return onOutputItemDone(state, event) + if (event.type === "response.completed" || event.type === "response.incomplete") + return Effect.succeed(onResponseFinish(state, event)) + if (event.type === "response.failed") return providerError(state, event, `${state.name} response failed`) + if (event.type === "error") return providerError(state, event, `${state.name} stream error`) + return Effect.succeed([state, NO_EVENTS]) +} + +// ============================================================================= +// Protocol +// ============================================================================= +/** + * The provider-neutral Open Responses protocol. Provider-specific Responses + * implementations compose this baseline with their own tools and event variants. + */ +export const initial = (request: LLMRequest, extension: Extension = BASE): ParserState => ({ + id: extension.id, + name: extension.name, + providerMetadataKey: request.model.route.providerMetadataKey ?? "openresponses", + hasFunctionCall: false, + tools: ToolStream.empty(), + lifecycle: Lifecycle.initial(), + reasoningItems: {}, + store: OpenResponsesOptions.resolve(request).store, +}) + +export const protocol = Protocol.make({ + id: ADAPTER, + body: { + schema: OpenResponsesBody, + from: fromRequest, + }, + stream: { + event: Protocol.jsonEvent(Event), + initial, + step, + terminal, + }, +}) + +export const httpTransport = HttpTransport.sseJson.with() + +export * as OpenResponses from "./open-responses" diff --git a/packages/ai/src/protocols/openai-chat.ts b/packages/ai/src/protocols/openai-chat.ts index f0f5dbfb6b58..0e327f2abf20 100644 --- a/packages/ai/src/protocols/openai-chat.ts +++ b/packages/ai/src/protocols/openai-chat.ts @@ -396,14 +396,13 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request: return messages }) -const lowerOptions = Effect.fn("OpenAIChat.lowerOptions")(function* (request: LLMRequest) { - const store = OpenAIOptions.store(request) - const reasoningEffort = OpenAIOptions.reasoningEffort(request) +const lowerOptions = (request: LLMRequest) => { + const options = OpenAIOptions.resolve(request) return { - ...(store !== undefined ? { store } : {}), - ...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}), + ...(options.store !== undefined ? { store: options.store } : {}), + ...(options.reasoningEffort ? { reasoning_effort: options.reasoningEffort } : {}), } -}) +} const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (request: LLMRequest) { // `fromRequest` returns the provider body only. Endpoint, auth, framing, @@ -434,7 +433,7 @@ const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (request: LLMR presence_penalty: generation?.presencePenalty, seed: generation?.seed, stop: generation?.stop, - ...(yield* lowerOptions(request)), + ...lowerOptions(request), } }) diff --git a/packages/ai/src/protocols/openai-compatible-responses.ts b/packages/ai/src/protocols/openai-compatible-responses.ts index 2c56aadafaef..0278c7941e40 100644 --- a/packages/ai/src/protocols/openai-compatible-responses.ts +++ b/packages/ai/src/protocols/openai-compatible-responses.ts @@ -1,23 +1,22 @@ import { Route, type RouteRoutedModelInput } from "../route/client" import { Endpoint } from "../route/endpoint" -import { OpenAIResponses } from "./openai-responses" +import { OpenResponses } from "./open-responses" const ADAPTER = "openai-compatible-responses" export type OpenAICompatibleResponsesModelInput = RouteRoutedModelInput /** - * Route for providers that expose an OpenAI Responses-compatible `/responses` - * endpoint. Provider helpers configure identity, endpoint, and auth before - * model selection while this route reuses the OpenAI Responses protocol. + * Deployment adapter for providers that expose an Open Responses-compatible + * `/responses` endpoint. Provider helpers configure identity, endpoint, and + * auth while the semantic protocol remains provider-neutral. */ export const route = Route.make({ id: ADAPTER, - providerMetadataKey: "openai", - protocol: OpenAIResponses.protocol, - endpoint: Endpoint.path(OpenAIResponses.PATH), - transport: OpenAIResponses.httpTransport, - defaults: { providerOptions: { openai: { store: false } } }, + providerMetadataKey: "openresponses", + protocol: OpenResponses.protocol, + endpoint: Endpoint.path(OpenResponses.PATH), + transport: OpenResponses.httpTransport, }) export * as OpenAICompatibleResponses from "./openai-compatible-responses" diff --git a/packages/ai/src/protocols/openai-responses.ts b/packages/ai/src/protocols/openai-responses.ts index 53fc62d3a0bf..f8655c67294a 100644 --- a/packages/ai/src/protocols/openai-responses.ts +++ b/packages/ai/src/protocols/openai-responses.ts @@ -2,134 +2,20 @@ import { Effect, Encoding, Schema } from "effect" import { Route } from "../route/client" import { Auth } from "../route/auth" import { Endpoint } from "../route/endpoint" -import { HttpTransport, WebSocketTransport } from "../route/transport" import { Protocol } from "../route/protocol" -import { - LLMError, - LLMEvent, - Usage, - type FinishReason, - type JsonSchema, - type LLMRequest, - type MediaPart, - type ProviderMetadata, - type ReasoningPart, - type TextPart, - type ToolCallPart, - type ToolDefinition, - type ToolContent, - type ToolResultPart, -} from "../schema" -import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" -import { classifyProviderFailure } from "../provider-error" -import { OpenAIOptions } from "./utils/openai-options" +import { HttpTransport, WebSocketTransport } from "../route/transport" +import { LLMEvent, LLMRequest, type JsonSchema, type ToolDefinition } from "../schema" +import { OpenResponses } from "./open-responses" +import { optionalArray, ProviderShared } from "./shared" import { Lifecycle } from "./utils/lifecycle" -import { ToolSchemaProjection } from "./utils/tool-schema" -import { ToolStream } from "./utils/tool-stream" import { OpenAIImage } from "./utils/openai-image" +import { ToolSchemaProjection } from "./utils/tool-schema" const ADAPTER = "openai-responses" -const MEDIA_MIMES = new Set([...ProviderShared.IMAGE_MIMES, ...ProviderShared.PDF_MIMES]) +const NAME = "OpenAI Responses" export const DEFAULT_BASE_URL = "https://api.openai.com/v1" -export const PATH = "/responses" - -// ============================================================================= -// Request Body Schema -// ============================================================================= -const OpenAIResponsesInputText = Schema.Struct({ - type: Schema.tag("input_text"), - text: Schema.String, -}) -const OpenAIResponsesInputImage = Schema.Struct({ - type: Schema.tag("input_image"), - image_url: Schema.String, -}) -const OpenAIResponsesInputFile = Schema.Struct({ - type: Schema.tag("input_file"), - filename: Schema.String, - file_data: Schema.String, - mime_type: Schema.optional(Schema.String), -}) -const OpenAIResponsesInputContent = Schema.Union([ - OpenAIResponsesInputText, - OpenAIResponsesInputImage, - OpenAIResponsesInputFile, -]) -type OpenAIResponsesInputContent = Schema.Schema.Type - -const OpenAIResponsesOutputText = Schema.Struct({ - type: Schema.tag("output_text"), - text: Schema.String, -}) - -const OpenAIResponsesReasoningSummaryText = Schema.Struct({ - type: Schema.tag("summary_text"), - text: Schema.String, -}) - -const OpenAIResponsesReasoningItem = Schema.Struct({ - type: Schema.tag("reasoning"), - id: Schema.optionalKey(Schema.String), - summary: Schema.Array(OpenAIResponsesReasoningSummaryText), - encrypted_content: optionalNull(Schema.String), -}) - -const OpenAIResponsesItemReference = Schema.Struct({ - type: Schema.tag("item_reference"), - id: Schema.String, -}) - -// `function_call_output.output` accepts either a plain string or an ordered -// array of content items so tools can return images and files in addition to text. -// https://platform.openai.com/docs/api-reference/responses/object -const OpenAIResponsesFunctionCallOutputContent = Schema.Union([ - OpenAIResponsesInputText, - OpenAIResponsesInputImage, - OpenAIResponsesInputFile, -]) - -const OpenAIResponsesFunctionCallOutput = Schema.Union([ - Schema.String, - Schema.Array(OpenAIResponsesFunctionCallOutputContent), -]) +export const PATH = OpenResponses.PATH -const OpenAIResponsesInputItem = Schema.Union([ - Schema.Struct({ role: Schema.tag("system"), content: Schema.String }), - Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenAIResponsesInputContent) }), - Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenAIResponsesOutputText) }), - OpenAIResponsesReasoningItem, - OpenAIResponsesItemReference, - Schema.Struct({ - type: Schema.tag("function_call"), - call_id: Schema.String, - name: Schema.String, - arguments: Schema.String, - }), - Schema.Struct({ - type: Schema.tag("function_call_output"), - call_id: Schema.String, - output: OpenAIResponsesFunctionCallOutput, - }), -]) -type OpenAIResponsesInputItem = Schema.Schema.Type - -// Mutable counterpart of the schema reasoning item so `lowerMessages` can fold -// multiple streamed summary parts into the same item before flushing. -type OpenAIResponsesReasoningInput = { - type: "reasoning" - id: string - summary: Array<{ type: "summary_text"; text: string }> - encrypted_content?: string | null -} -type OpenAIResponsesReasoningReplay = Omit - -const OpenAIResponsesTool = Schema.Struct({ - type: Schema.tag("function"), - name: Schema.String, - description: Schema.String, - parameters: JsonObject, - strict: Schema.optional(Schema.Boolean), -}) const OpenAIResponsesImageGenerationTool = Schema.Struct({ type: Schema.tag("image_generation"), action: Schema.optional(Schema.Literals(["auto", "generate", "edit"])), @@ -141,43 +27,18 @@ const OpenAIResponsesImageGenerationTool = Schema.Struct({ quality: Schema.optional(Schema.Literals(["auto", "low", "medium", "high"])), size: Schema.optional(OpenAIImage.Size), }) -const OpenAIResponsesTools = Schema.Union([OpenAIResponsesTool, OpenAIResponsesImageGenerationTool]) -type OpenAIResponsesTool = Schema.Schema.Type + +const OpenAIResponsesTools = Schema.Union([OpenResponses.Tool, OpenAIResponsesImageGenerationTool]) const OpenAIResponsesToolChoice = Schema.Union([ - Schema.Literals(["auto", "none", "required"]), - Schema.Struct({ type: Schema.tag("function"), name: Schema.String }), + OpenResponses.ToolChoice, Schema.Struct({ type: Schema.tag("image_generation") }), ]) -// Fields shared between the HTTP body and the WebSocket `response.create` -// message. The HTTP body adds `stream: true`; the WebSocket message adds -// `type: "response.create"`. Defining the shared shape once keeps the two -// transports in sync without a destructure-and-strip dance. const OpenAIResponsesCoreFields = { - model: Schema.String, - input: Schema.Array(OpenAIResponsesInputItem), - instructions: Schema.optional(Schema.String), + ...OpenResponses.coreFields, tools: optionalArray(OpenAIResponsesTools), tool_choice: Schema.optional(OpenAIResponsesToolChoice), - store: Schema.optional(Schema.Boolean), - service_tier: Schema.optional(OpenAIOptions.OpenAIServiceTier), - prompt_cache_key: Schema.optional(Schema.String), - include: optionalArray(OpenAIOptions.OpenAIResponseIncludable), - reasoning: Schema.optional( - Schema.Struct({ - effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort), - summary: Schema.optional(Schema.Literal("auto")), - }), - ), - text: Schema.optional( - Schema.Struct({ - verbosity: Schema.optional(OpenAIOptions.OpenAITextVerbosity), - }), - ), - max_output_tokens: Schema.optional(Schema.Number), - temperature: Schema.optional(Schema.Number), - top_p: Schema.optional(Schema.Number), } const OpenAIResponsesBody = Schema.Struct({ @@ -196,100 +57,20 @@ const OpenAIResponsesWebSocketMessage = Schema.StructWithRest( type OpenAIResponsesWebSocketMessage = Schema.Schema.Type const encodeWebSocketMessage = Schema.encodeSync(Schema.fromJsonString(OpenAIResponsesWebSocketMessage)) -const OpenAIResponsesUsage = Schema.Struct({ - input_tokens: Schema.optional(Schema.Number), - input_tokens_details: optionalNull(Schema.Struct({ cached_tokens: Schema.optional(Schema.Number) })), - output_tokens: Schema.optional(Schema.Number), - output_tokens_details: optionalNull(Schema.Struct({ reasoning_tokens: Schema.optional(Schema.Number) })), - total_tokens: Schema.optional(Schema.Number), -}) -type OpenAIResponsesUsage = Schema.Schema.Type - -const OpenAIResponsesStreamItem = Schema.Struct({ - type: Schema.String, - id: Schema.optional(Schema.String), - call_id: Schema.optional(Schema.String), - name: Schema.optional(Schema.String), - arguments: Schema.optional(Schema.String), - // Hosted (provider-executed) tool fields. Each hosted tool item carries its - // own subset of these — we capture them generically so we can surface the - // call's typed input portion and round-trip the full result payload without - // hand-rolling a per-tool schema. - status: Schema.optional(Schema.String), - action: Schema.optional(Schema.Unknown), - queries: Schema.optional(Schema.Unknown), - results: Schema.optional(Schema.Unknown), - code: Schema.optional(Schema.String), - container_id: Schema.optional(Schema.String), - outputs: Schema.optional(Schema.Unknown), - server_label: Schema.optional(Schema.String), - output: Schema.optional(Schema.Unknown), - result: Schema.optional(Schema.String), - output_format: Schema.optional(Schema.Literals(["png", "jpeg", "webp"])), - error: Schema.optional(Schema.Unknown), - encrypted_content: optionalNull(Schema.String), -}) -type OpenAIResponsesStreamItem = Schema.Schema.Type - -// The Responses schema puts streaming error details at the top level and -// response failures under `response.error`. The official SDK also recognizes -// an event-level HTTP-style `error` envelope, so accept all three shapes here. -// https://github.com/openai/openai-openapi/blob/5162af98d3147432c14680df789e8e12d4891e6b/openapi.yaml#L67234-L67382 -// https://github.com/openai/openai-node/blob/61539248cbe04665de68a71e6fd878127ae4db87/src/core/streaming.ts#L58-L85 -const OpenAIResponsesErrorPayload = Schema.Struct({ - code: optionalNull(Schema.String), - message: optionalNull(Schema.String), - param: optionalNull(Schema.String), -}) - -const OpenAIResponsesEvent = Schema.Struct({ - type: Schema.String, - delta: Schema.optional(Schema.String), - item_id: Schema.optional(Schema.String), - summary_index: Schema.optional(Schema.Number), - item: Schema.optional(OpenAIResponsesStreamItem), - response: Schema.optional( - Schema.StructWithRest( - Schema.Struct({ - id: Schema.optional(Schema.String), - service_tier: optionalNull(Schema.String), - incomplete_details: optionalNull(Schema.Struct({ reason: Schema.optional(Schema.String) })), - usage: optionalNull(OpenAIResponsesUsage), - error: optionalNull(OpenAIResponsesErrorPayload), - }), - [Schema.Record(Schema.String, Schema.Unknown)], - ), - ), - code: optionalNull(Schema.String), - message: Schema.optional(Schema.String), - param: optionalNull(Schema.String), - error: optionalNull(OpenAIResponsesErrorPayload), -}) -type OpenAIResponsesEvent = Schema.Schema.Type - -interface ParserState { - readonly tools: ToolStream.State - readonly hasFunctionCall: boolean - readonly lifecycle: Lifecycle.State - readonly reasoningItems: Readonly> - readonly store: boolean | undefined -} - -type ReasoningSummaryStatus = "active" | "can-conclude" | "concluded" - -interface ReasoningStreamItem { - readonly encryptedContent: string | null | undefined - // Keyed by OpenAI's numeric `summary_index`. JS object keys coerce to - // strings, but typing the map as `Record` documents intent - // and matches the wire field. - readonly summaryParts: Readonly> -} - -const invalid = ProviderShared.invalidRequest +const extension = { + id: ADAPTER, + name: NAME, + lowerMedia: ({ part, media, request }) => { + if (request.model.provider !== "xai" || media.mime !== "application/pdf") return undefined + return { + type: "input_file", + filename: part.filename ?? "document.pdf", + file_data: media.base64, + mime_type: media.mime, + } + }, +} satisfies OpenResponses.Extension -// ============================================================================= -// Request Lowering -// ============================================================================= const nativeImageToolInput = (tool: ToolDefinition) => { const native = tool.native?.openai return ProviderShared.isRecord(native) && native.type === "image_generation" ? native : undefined @@ -304,20 +85,13 @@ const lowerTool = Effect.fn("OpenAIResponses.lowerTool")(function* (tool: ToolDe const native = nativeImageToolInput(tool) if (native !== undefined) { if (Schema.is(OpenAIResponsesImageGenerationTool)(native)) return native - return yield* invalid("OpenAI Responses image generation tool options are invalid") - } - return { - type: "function" as const, - name: tool.name, - description: tool.description, - parameters: ToolSchemaProjection.openAI(inputSchema), - // TODO: Read this from OpenAI-specific tool options so direct LLM callers can opt into strict schemas. - strict: false, + return yield* ProviderShared.invalidRequest("OpenAI Responses image generation tool options are invalid") } + return yield* OpenResponses.lowerTool(NAME, tool, inputSchema) }) const lowerToolChoice = (toolChoice: NonNullable, tools: ReadonlyArray) => - ProviderShared.matchToolChoice("OpenAI Responses", toolChoice, { + ProviderShared.matchToolChoice(NAME, toolChoice, { auto: () => "auto" as const, none: () => "none" as const, required: () => "required" as const, @@ -327,241 +101,14 @@ const lowerToolChoice = (toolChoice: NonNullable, tool : { type: "function" as const, name }, }) -const lowerToolCall = (part: ToolCallPart): OpenAIResponsesInputItem => ({ - type: "function_call", - call_id: part.id, - name: part.name, - arguments: ProviderShared.encodeJson(part.input), -}) - -const lowerReasoning = (part: ReasoningPart): OpenAIResponsesReasoningInput | undefined => { - const openai = part.providerMetadata?.openai - if (!ProviderShared.isRecord(openai) || typeof openai.itemId !== "string" || openai.itemId.length === 0) - return undefined - const encryptedContent = - typeof openai.reasoningEncryptedContent === "string" - ? openai.reasoningEncryptedContent - : openai.reasoningEncryptedContent === null - ? null - : undefined - return { - type: "reasoning", - id: openai.itemId, - summary: part.text.length > 0 ? [{ type: "summary_text", text: part.text }] : [], - encrypted_content: encryptedContent, - } -} - -const hostedToolItemID = (part: ToolResultPart) => { - const openai = part.providerMetadata?.openai - return ProviderShared.isRecord(openai) && typeof openai.itemId === "string" && openai.itemId.length > 0 - ? openai.itemId - : undefined -} - -const lowerMedia = Effect.fn("OpenAIResponses.lowerMedia")(function* (part: MediaPart, provider: string) { - const media = yield* ProviderShared.validateMedia("OpenAI Responses", part, MEDIA_MIMES) - if (media.mime === "application/pdf") { - // xAI models inline bytes and MIME separately; OpenAI uses a data URL in file_data. - if (provider === "xai") - return { - type: "input_file" as const, - filename: part.filename ?? "document.pdf", - file_data: media.base64, - mime_type: media.mime, - } - return { - type: "input_file" as const, - filename: part.filename ?? "document.pdf", - file_data: media.dataUrl, - } - } - return { type: "input_image" as const, image_url: media.dataUrl } -}) - -const lowerUserContent = Effect.fn("OpenAIResponses.lowerUserContent")(function* ( - part: LLMRequest["messages"][number]["content"][number], - provider: string, -) { - if (part.type === "text") return { type: "input_text" as const, text: part.text } - if (part.type === "media") return yield* lowerMedia(part, provider) - return yield* ProviderShared.unsupportedContent("OpenAI Responses", "user", ["text", "media"]) -}) - -// Tool results may carry structured text, images, and files. Keep media as provider-native -// content instead of JSON-stringifying base64 into a prompt string. -const lowerToolResultContentItem = Effect.fn("OpenAIResponses.lowerToolResultContentItem")(function* ( - item: ToolContent, - provider: string, -) { - if (item.type === "text") return { type: "input_text" as const, text: item.text } - return yield* lowerMedia( - { type: "media", mediaType: item.mime, data: item.uri, filename: item.name }, - provider, - ) -}) - -const lowerToolResultOutput = Effect.fn("OpenAIResponses.lowerToolResultOutput")(function* ( - part: ToolResultPart, - provider: string, -) { - // Text/json/error results are encoded as a plain string for backward - // compatibility with existing cassettes and provider expectations. - if (part.result.type !== "content") return ProviderShared.toolResultText(part) - // Preserve the narrowed array element type when compiled through a consumer package. - const content: ReadonlyArray = part.result.value - return yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, provider)) -}) - -const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (request: LLMRequest) { - const system: OpenAIResponsesInputItem[] = - request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }] - const input: OpenAIResponsesInputItem[] = [...system] - const store = OpenAIOptions.store(request) - - for (const message of request.messages) { - if (message.role === "system") { - const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Responses", message) - const previous = input.at(-1) - if (previous && "role" in previous && previous.role === "user") - input[input.length - 1] = { - role: "user", - content: [...previous.content, { type: "input_text", text: part.text }], - } - else input.push({ role: "user", content: [{ type: "input_text", text: part.text }] }) - continue - } - - if (message.role === "user") { - input.push({ - role: "user", - content: yield* Effect.forEach(message.content, (part) => lowerUserContent(part, request.model.provider)), - }) - continue - } - - if (message.role === "assistant") { - const content: TextPart[] = [] - const reasoningItems: Record = {} - const reasoningReferences = new Set() - const hostedToolReferences = new Set() - const flushText = () => { - if (content.length === 0) return - input.push({ role: "assistant", content: content.map((part) => ({ type: "output_text", text: part.text })) }) - content.splice(0, content.length) - } - for (const part of message.content) { - if (part.type === "text") { - content.push(part) - continue - } - if (part.type === "reasoning") { - flushText() - const reasoning = lowerReasoning(part) - if (!reasoning) continue - if (store !== false) { - if (!reasoningReferences.has(reasoning.id)) input.push({ type: "item_reference", id: reasoning.id }) - reasoningReferences.add(reasoning.id) - continue - } - const existing = reasoningItems[reasoning.id] - if (existing) { - existing.summary.push(...reasoning.summary) - if (typeof reasoning.encrypted_content === "string") - existing.encrypted_content = reasoning.encrypted_content - continue - } - const replay = { - type: reasoning.type, - summary: reasoning.summary, - encrypted_content: reasoning.encrypted_content, - } - reasoningItems[reasoning.id] = replay - input.push(replay) - continue - } - if (part.type === "tool-call") { - flushText() - if (part.providerExecuted === true) continue - input.push(lowerToolCall(part)) - continue - } - if (part.type === "tool-result" && part.providerExecuted === true) { - flushText() - const itemID = hostedToolItemID(part) - if (store !== false && itemID && !hostedToolReferences.has(itemID)) - input.push({ type: "item_reference", id: itemID }) - if (store === false && part.name === "image_generation" && part.result.type === "content") { - const content: ReadonlyArray = part.result.value - input.push({ - role: "user", - content: yield* Effect.forEach(content, (item) => - lowerToolResultContentItem(item, request.model.provider), - ), - }) - } - if (itemID) hostedToolReferences.add(itemID) - continue - } - return yield* ProviderShared.unsupportedContent("OpenAI Responses", "assistant", [ - "text", - "reasoning", - "tool-call", - "tool-result", - ]) - } - flushText() - continue - } - - for (const part of message.content) { - if (!ProviderShared.supportsContent(part, ["tool-result"])) - return yield* ProviderShared.unsupportedContent("OpenAI Responses", "tool", ["tool-result"]) - input.push({ - type: "function_call_output", - call_id: part.id, - output: yield* lowerToolResultOutput(part, request.model.provider), - }) - } - } - - // With store:false, OpenAI only accepts previous reasoning items when the - // complete item has encrypted state. Summary blocks for one item may carry - // that state only on the last block, so filter after they have been joined. - return store === false - ? input.filter( - (item) => !("type" in item) || item.type !== "reasoning" || typeof item.encrypted_content === "string", - ) - : input -}) - -const lowerOptions = Effect.fn("OpenAIResponses.lowerOptions")(function* (request: LLMRequest) { - const store = OpenAIOptions.store(request) - const promptCacheKey = OpenAIOptions.promptCacheKey(request) - const effort = OpenAIOptions.reasoningEffort(request) - const summary = OpenAIOptions.reasoningSummary(request) - const include = OpenAIOptions.include(request) - const verbosity = OpenAIOptions.textVerbosity(request) - const instructions = OpenAIOptions.instructions(request) - const serviceTier = OpenAIOptions.serviceTier(request) - return { - ...(instructions ? { instructions } : {}), - ...(store !== undefined ? { store } : {}), - ...(promptCacheKey ? { prompt_cache_key: promptCacheKey } : {}), - ...(include ? { include } : {}), - ...(effort || summary ? { reasoning: { effort, summary } } : {}), - ...(verbosity ? { text: { verbosity } } : {}), - ...(serviceTier ? { service_tier: serviceTier } : {}), - } -}) - const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request: LLMRequest) { - const generation = request.generation - const options = yield* lowerOptions(request) + const body = yield* OpenResponses.fromRequest( + LLMRequest.update(request, { tools: [], toolChoice: undefined }), + extension, + ) const toolSchemaCompatibility = request.model.compatibility?.toolSchema return { - model: request.model.id, - input: yield* lowerMessages(request), + ...body, tools: request.tools.length === 0 ? undefined @@ -569,58 +116,25 @@ const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request: lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility)), ), tool_choice: request.toolChoice ? yield* lowerToolChoice(request.toolChoice, request.tools) : undefined, - stream: true as const, - max_output_tokens: generation?.maxTokens, - temperature: generation?.temperature, - top_p: generation?.topP, - ...options, - } + } satisfies OpenAIResponsesBody }) -// ============================================================================= -// Stream Parsing -// ============================================================================= -// OpenAI Responses reports `input_tokens` (inclusive total) with a -// `cached_tokens` subset, and `output_tokens` (inclusive total) with a -// `reasoning_tokens` subset. Pass the totals through and derive the -// non-cached breakdown. -const mapUsage = (usage: OpenAIResponsesUsage | null | undefined) => { - if (!usage) return undefined - const cached = usage.input_tokens_details?.cached_tokens - const reasoning = usage.output_tokens_details?.reasoning_tokens - const nonCached = ProviderShared.subtractTokens(usage.input_tokens, cached) - return new Usage({ - inputTokens: usage.input_tokens, - outputTokens: usage.output_tokens, - nonCachedInputTokens: nonCached, - cacheReadInputTokens: cached, - reasoningTokens: reasoning, - totalTokens: ProviderShared.totalTokens(usage.input_tokens, usage.output_tokens, usage.total_tokens), - providerMetadata: { openai: usage }, - }) -} - -const mapFinishReason = (event: OpenAIResponsesEvent, hasFunctionCall: boolean): FinishReason => { - const reason = event.response?.incomplete_details?.reason - if (reason === undefined || reason === null) - return hasFunctionCall ? "tool-calls" : event.type === "response.incomplete" ? "unknown" : "stop" - if (reason === "max_output_tokens") return "length" - if (reason === "content_filter") return "content-filter" - return hasFunctionCall ? "tool-calls" : "unknown" +type HostedToolData = OpenResponses.StreamItem & { + readonly id: string + readonly status?: string + readonly action?: unknown + readonly queries?: unknown + readonly results?: unknown + readonly code?: string + readonly container_id?: string + readonly outputs?: unknown + readonly server_label?: string + readonly output?: unknown + readonly result?: string + readonly output_format?: "png" | "jpeg" | "webp" + readonly error?: unknown } -const openaiMetadata = (metadata: Record): ProviderMetadata => ({ openai: metadata }) - -// Hosted tool items (provider-executed) ship their typed input + status + -// result fields all in one item. We expose them as a `tool-call` + -// `tool-result` pair so consumers can treat them uniformly with client tools, -// only differentiated by `providerExecuted: true`. -// -// One record per OpenAI Responses item type that represents a hosted -// (provider-executed) tool call: the common name we surface, plus an `input` -// extractor that picks the fields the model actually populated for that tool. -// Falling back to `{}` when an entry isn't fully typed keeps unknown tools -// observable without rolling a per-tool schema. const HOSTED_TOOLS = { web_search_call: { name: "web_search", input: (item) => item.action ?? {} }, web_search_preview_call: { name: "web_search_preview", input: (item) => item.action ?? {} }, @@ -636,38 +150,28 @@ const HOSTED_TOOLS = { input: (item) => ({ server_label: item.server_label, name: item.name, arguments: item.arguments }), }, local_shell_call: { name: "local_shell", input: (item) => item.action ?? {} }, -} as const satisfies Record< - string, - { readonly name: string; readonly input: (item: OpenAIResponsesStreamItem) => unknown } -> +} as const satisfies Record unknown }> type HostedToolType = keyof typeof HOSTED_TOOLS +type HostedToolItem = HostedToolData & { readonly type: HostedToolType } -const isHostedToolItem = ( - item: OpenAIResponsesStreamItem, -): item is OpenAIResponsesStreamItem & { type: HostedToolType; id: string } => +const isHostedToolItem = (item: OpenResponses.StreamItem): item is HostedToolItem => item.type in HOSTED_TOOLS && typeof item.id === "string" && item.id.length > 0 -const isReasoningItem = ( - item: OpenAIResponsesStreamItem, -): item is OpenAIResponsesStreamItem & { type: "reasoning"; id: string } => - item.type === "reasoning" && typeof item.id === "string" && item.id.length > 0 - -// Round-trip the full item as the structured result so consumers can extract -// outputs / sources / status without re-decoding. -const hostedToolResult = Effect.fn("OpenAIResponses.hostedToolResult")(function* (item: OpenAIResponsesStreamItem) { - const isError = typeof item.error !== "undefined" && item.error !== null +const hostedToolResult = Effect.fn("OpenAIResponses.hostedToolResult")(function* (item: HostedToolItem) { + const isError = item.error !== undefined && item.error !== null if (item.type === "image_generation_call" && item.result) { yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe( Effect.mapError(() => ProviderShared.eventError(ADAPTER, "OpenAI Responses returned invalid image base64")), ) + const format = item.output_format ?? "png" return { type: "content" as const, value: [ { type: "file" as const, - uri: `data:image/${item.output_format ?? "png"};base64,${item.result}`, - mime: `image/${item.output_format ?? "png"}`, + uri: `data:image/${format};base64,${item.result}`, + mime: `image/${format}`, }, ], } @@ -675,12 +179,15 @@ const hostedToolResult = Effect.fn("OpenAIResponses.hostedToolResult")(function* return isError ? { type: "error" as const, value: item.error } : { type: "json" as const, value: item } }) -const hostedToolEvents = Effect.fn("OpenAIResponses.hostedToolEvents")(function* ( - item: OpenAIResponsesStreamItem & { type: HostedToolType; id: string }, +const onHostedToolDone = Effect.fn("OpenAIResponses.onHostedToolDone")(function* ( + state: OpenResponses.ParserState, + item: HostedToolItem, ) { const tool = HOSTED_TOOLS[item.type] - const providerMetadata = openaiMetadata({ itemId: item.id }) - return [ + const providerMetadata = OpenResponses.providerMetadata(state, { itemId: item.id }) + const events: LLMEvent[] = [] + const lifecycle = Lifecycle.stepStart(state.lifecycle, events) + events.push( LLMEvent.toolCall({ id: item.id, name: tool.name, @@ -695,366 +202,20 @@ const hostedToolEvents = Effect.fn("OpenAIResponses.hostedToolEvents")(function* providerExecuted: true, providerMetadata, }), - ] -}) - -type StepResult = readonly [ParserState, ReadonlyArray] - -const NO_EVENTS: StepResult["1"] = [] - -// `response.completed` / `response.incomplete` are clean finishes that emit a -// `finish` event; `response.failed` is a hard failure. All three end the stream, -// so keep this set aligned with `step` and the protocol's terminal predicate. -const TERMINAL_TYPES = new Set(["response.completed", "response.incomplete", "response.failed"]) - -const onOutputTextDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { - if (!event.delta) return [state, NO_EVENTS] - const events: LLMEvent[] = [] - return [ - { ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, event.item_id ?? "text-0", event.delta) }, - events, - ] -} - -const onOutputTextDone = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { - const events: LLMEvent[] = [] - return [{ ...state, lifecycle: Lifecycle.textEnd(state.lifecycle, events, event.item_id ?? "text-0") }, events] -} - -const onReasoningDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { - if (!event.delta) return [state, NO_EVENTS] - const events: LLMEvent[] = [] - const itemID = event.item_id ?? "reasoning-0" - const id = - event.summary_index !== undefined || state.reasoningItems[itemID] ? `${itemID}:${event.summary_index ?? 0}` : itemID - return [ - { - ...state, - lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, id, event.delta), - }, - events, - ] -} - -const onReasoningDone = (state: ParserState, _event: OpenAIResponsesEvent): StepResult => [state, NO_EVENTS] - -const reasoningMetadata = (item: OpenAIResponsesStreamItem & { id: string }) => - openaiMetadata({ itemId: item.id, reasoningEncryptedContent: item.encrypted_content ?? null }) - -// OpenAI Responses streams reasoning items in a stable order: -// `output_item.added` (reasoning) → -// `reasoning_summary_part.added` (index=0) → -// `reasoning_summary_text.delta` → -// `reasoning_summary_part.done` (index=0) → -// (repeat for index>0) → -// `output_item.done` (reasoning). -// The handlers below rely on this ordering: `onOutputItemAdded` seeds the -// per-item entry, `onReasoningSummaryPartAdded` for `summary_index === 0` -// short-circuits when the entry already exists, and higher-index handlers -// fold against the same entry. Behaviour for out-of-order events is -// best-effort, not guaranteed. -const onOutputItemAdded = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { - const item = event.item - if (item && isReasoningItem(item)) { - const events: LLMEvent[] = [] - return [ - { - ...state, - lifecycle: Lifecycle.reasoningStart(state.lifecycle, events, `${item.id}:0`, reasoningMetadata(item)), - reasoningItems: { - ...state.reasoningItems, - [item.id]: { encryptedContent: item.encrypted_content, summaryParts: { 0: "active" } }, - }, - }, - events, - ] - } - if (item?.type !== "function_call" || !item.id) return [state, NO_EVENTS] - const providerMetadata = openaiMetadata({ itemId: item.id }) - const events: LLMEvent[] = [] - const lifecycle = Lifecycle.stepStart(state.lifecycle, events) - return [ - { - ...state, - lifecycle, - hasFunctionCall: state.hasFunctionCall, - tools: ToolStream.start(state.tools, item.id, { - id: item.call_id ?? item.id, - name: item.name ?? "", - input: item.arguments ?? "", - providerMetadata, - }), - }, - [...events, LLMEvent.toolInputStart({ id: item.call_id ?? item.id, name: item.name ?? "", providerMetadata })], - ] -} - -const onReasoningSummaryPartAdded = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { - if (!event.item_id || event.summary_index === undefined) return [state, NO_EVENTS] - const item = state.reasoningItems[event.item_id] ?? { encryptedContent: undefined, summaryParts: {} } - if (event.summary_index === 0) { - if (state.reasoningItems[event.item_id]) return [state, NO_EVENTS] - const events: LLMEvent[] = [] - return [ - { - ...state, - lifecycle: Lifecycle.reasoningStart( - state.lifecycle, - events, - `${event.item_id}:0`, - openaiMetadata({ itemId: event.item_id, reasoningEncryptedContent: null }), - ), - reasoningItems: { - ...state.reasoningItems, - [event.item_id]: { ...item, summaryParts: { 0: "active" } }, - }, - }, - events, - ] - } - - const events: LLMEvent[] = [] - const closed = Object.entries(item.summaryParts) - .filter((entry) => entry[1] === "can-conclude") - .reduce( - (lifecycle, entry) => - Lifecycle.reasoningEnd( - lifecycle, - events, - `${event.item_id}:${entry[0]}`, - openaiMetadata({ itemId: event.item_id }), - ), - state.lifecycle, - ) - return [ - { - ...state, - lifecycle: Lifecycle.reasoningStart( - closed, - events, - `${event.item_id}:${event.summary_index}`, - openaiMetadata({ itemId: event.item_id, reasoningEncryptedContent: item.encryptedContent ?? null }), - ), - reasoningItems: { - ...state.reasoningItems, - [event.item_id]: { - ...item, - summaryParts: { - ...Object.fromEntries( - Object.entries(item.summaryParts).map((entry) => - entry[1] === "can-conclude" ? [entry[0], "concluded" as const] : entry, - ), - ), - [event.summary_index]: "active", - }, - }, - }, - }, - events, - ] -} - -const onReasoningSummaryPartDone = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { - if (!event.item_id || event.summary_index === undefined) return [state, NO_EVENTS] - const item = state.reasoningItems[event.item_id] - if (!item) return [state, NO_EVENTS] - const events: LLMEvent[] = [] - return [ - { - ...state, - lifecycle: - state.store !== false - ? Lifecycle.reasoningEnd( - state.lifecycle, - events, - `${event.item_id}:${event.summary_index}`, - openaiMetadata({ itemId: event.item_id }), - ) - : state.lifecycle, - reasoningItems: { - ...state.reasoningItems, - [event.item_id]: { - ...item, - summaryParts: { - ...item.summaryParts, - [event.summary_index]: state.store !== false ? "concluded" : "can-conclude", - }, - }, - }, - }, - events, - ] -} - -const onFunctionCallArgumentsDelta = Effect.fn("OpenAIResponses.onFunctionCallArgumentsDelta")(function* ( - state: ParserState, - event: OpenAIResponsesEvent, -) { - if (!event.item_id || !event.delta) return [state, NO_EVENTS] satisfies StepResult - const result = ToolStream.appendExisting( - ADAPTER, - state.tools, - event.item_id, - event.delta, - "OpenAI Responses tool argument delta is missing its tool call", ) - if (ToolStream.isError(result)) return yield* result - const events: LLMEvent[] = [] - const lifecycle = result.events.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle - events.push(...result.events) - return [{ ...state, lifecycle, tools: result.tools }, events] satisfies StepResult + return [{ ...state, lifecycle }, events] satisfies OpenResponses.StepResult }) -const onOutputItemDone = Effect.fn("OpenAIResponses.onOutputItemDone")(function* ( - state: ParserState, - event: OpenAIResponsesEvent, -) { - const item = event.item - if (!item) return [state, NO_EVENTS] satisfies StepResult - - if (item.type === "message" && item.id) return onOutputTextDone(state, { ...event, item_id: item.id }) - - if (item.type === "function_call") { - if (!item.id || !item.call_id || !item.name) return [state, NO_EVENTS] satisfies StepResult - const tools = state.tools[item.id] - ? state.tools - : ToolStream.start(state.tools, item.id, { id: item.call_id, name: item.name }) - const result = - item.arguments === undefined - ? yield* ToolStream.finish(ADAPTER, tools, item.id) - : yield* ToolStream.finishWithInput(ADAPTER, tools, item.id, item.arguments) - const events: LLMEvent[] = [] - const resultEvents = result.events ?? [] - const lifecycle = resultEvents.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle - events.push(...resultEvents) - return [ - { - ...state, - lifecycle, - hasFunctionCall: - resultEvents.some((event) => LLMEvent.is.toolCall(event) || LLMEvent.is.toolInputError(event)) || - state.hasFunctionCall, - tools: result.tools, - }, - events, - ] satisfies StepResult - } - - if (isHostedToolItem(item)) { - const events: LLMEvent[] = [] - const lifecycle = Lifecycle.stepStart(state.lifecycle, events) - events.push(...(yield* hostedToolEvents(item))) - return [{ ...state, lifecycle }, events] satisfies StepResult - } - - if (isReasoningItem(item)) { - const events: LLMEvent[] = [] - const providerMetadata = reasoningMetadata(item) - const reasoningItem = state.reasoningItems[item.id] - if (reasoningItem) { - const lifecycle = Object.entries(reasoningItem.summaryParts) - .filter((entry) => entry[1] === "active" || entry[1] === "can-conclude") - .reduce( - (lifecycle, entry) => Lifecycle.reasoningEnd(lifecycle, events, `${item.id}:${entry[0]}`, providerMetadata), - state.lifecycle, - ) - const { [item.id]: _removed, ...reasoningItems } = state.reasoningItems - return [{ ...state, lifecycle, reasoningItems }, events] satisfies StepResult - } - if (!state.lifecycle.reasoning.has(item.id)) { - const lifecycle = Lifecycle.stepStart(state.lifecycle, events) - events.push(LLMEvent.reasoningStart({ id: item.id, providerMetadata })) - events.push(LLMEvent.reasoningEnd({ id: item.id, providerMetadata })) - return [{ ...state, lifecycle }, events] satisfies StepResult - } - return [ - { ...state, lifecycle: Lifecycle.reasoningEnd(state.lifecycle, events, item.id, providerMetadata) }, - events, - ] satisfies StepResult - } - - return [state, NO_EVENTS] satisfies StepResult -}) - -const onResponseFinish = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { - const events: LLMEvent[] = [] - const lifecycle = Lifecycle.finish(state.lifecycle, events, { - reason: { - normalized: mapFinishReason(event, state.hasFunctionCall), - raw: event.response?.incomplete_details?.reason, - }, - usage: mapUsage(event.response?.usage), - providerMetadata: - event.response?.id || event.response?.service_tier - ? openaiMetadata({ - responseId: event.response.id, - serviceTier: event.response.service_tier, - }) - : undefined, - }) - return [{ ...state, lifecycle }, events] +const step = (state: OpenResponses.ParserState, event: OpenResponses.Event) => { + if (event.type === "response.reasoning_text.delta" || event.type === "response.reasoning_summary.delta") + return Effect.succeed(OpenResponses.onReasoningDelta(state, event)) + if (event.type === "response.reasoning_text.done" || event.type === "response.reasoning_summary.done") + return Effect.succeed(OpenResponses.onReasoningDone(state, event)) + if (event.type === "response.output_item.done" && event.item && isHostedToolItem(event.item)) + return onHostedToolDone(state, event.item) + return OpenResponses.step(state, event) } -// Build a single human-readable message from whatever the provider supplied. -// When both code and message are present, prefix the code so consumers see -// the failure mode (e.g. `rate_limit_exceeded: Slow down`) instead of just -// the bare message — production rate limits and context-length failures used -// to be indistinguishable from generic stream drops. -const providerErrorMessage = (event: OpenAIResponsesEvent, fallback: string): string => { - const nested = event.error ?? event.response?.error ?? undefined - const message = event.message || nested?.message || undefined - const code = event.code || nested?.code || undefined - if (message && code) return `${code}: ${message}` - return message || code || fallback -} - -const providerError = (event: OpenAIResponsesEvent, fallback: string) => { - const code = event.code || event.error?.code || event.response?.error?.code || undefined - const message = providerErrorMessage(event, fallback) - return new LLMError({ - module: ADAPTER, - method: "stream", - reason: classifyProviderFailure({ message, code }), - }) -} - -const step = (state: ParserState, event: OpenAIResponsesEvent) => { - if (event.type === "response.output_text.delta") return Effect.succeed(onOutputTextDelta(state, event)) - if (event.type === "response.output_text.done") return Effect.succeed(onOutputTextDone(state, event)) - if ( - event.type === "response.reasoning_text.delta" || - event.type === "response.reasoning_summary.delta" || - event.type === "response.reasoning_summary_text.delta" - ) - return Effect.succeed(onReasoningDelta(state, event)) - if ( - event.type === "response.reasoning_text.done" || - event.type === "response.reasoning_summary.done" || - event.type === "response.reasoning_summary_text.done" - ) - return Effect.succeed(onReasoningDone(state, event)) - if (event.type === "response.reasoning_summary_part.added") - return Effect.succeed(onReasoningSummaryPartAdded(state, event)) - if (event.type === "response.reasoning_summary_part.done") - return Effect.succeed(onReasoningSummaryPartDone(state, event)) - if (event.type === "response.output_item.added") return Effect.succeed(onOutputItemAdded(state, event)) - if (event.type === "response.function_call_arguments.delta") return onFunctionCallArgumentsDelta(state, event) - if (event.type === "response.output_item.done") return onOutputItemDone(state, event) - if (event.type === "response.completed" || event.type === "response.incomplete") - return Effect.succeed(onResponseFinish(state, event)) - if (event.type === "response.failed") return providerError(event, "OpenAI Responses response failed") - if (event.type === "error") return providerError(event, "OpenAI Responses stream error") - return Effect.succeed([state, NO_EVENTS]) -} - -// ============================================================================= -// Protocol And OpenAI Route -// ============================================================================= -/** - * The OpenAI Responses protocol — request body construction, body schema, and - * the streaming-event state machine. Used by native OpenAI and (once - * registered) Azure OpenAI Responses. - */ export const protocol = Protocol.make({ id: ADAPTER, body: { @@ -1062,16 +223,10 @@ export const protocol = Protocol.make({ from: fromRequest, }, stream: { - event: Protocol.jsonEvent(OpenAIResponsesEvent), - initial: (request) => ({ - hasFunctionCall: false, - tools: ToolStream.empty(), - lifecycle: Lifecycle.initial(), - reasoningItems: {}, - store: OpenAIOptions.store(request), - }), + event: OpenResponses.protocol.stream.event, + initial: (request) => OpenResponses.initial(request, extension), step, - terminal: (event) => TERMINAL_TYPES.has(event.type), + terminal: OpenResponses.terminal, }, }) diff --git a/packages/ai/src/protocols/utils/open-responses-options.ts b/packages/ai/src/protocols/utils/open-responses-options.ts new file mode 100644 index 000000000000..80288ca5cb1d --- /dev/null +++ b/packages/ai/src/protocols/utils/open-responses-options.ts @@ -0,0 +1,65 @@ +import { Schema } from "effect" +import { TextVerbosity, type LLMRequest } from "../../schema" + +export const ResponseIncludables = [ + "file_search_call.results", + "web_search_call.results", + "web_search_call.action.sources", + "message.input_image.image_url", + "computer_call_output.output.image_url", + "code_interpreter_call.outputs", + "reasoning.encrypted_content", + "message.output_text.logprobs", +] as const +export type ResponseIncludable = (typeof ResponseIncludables)[number] + +export const ServiceTiers = ["auto", "default", "flex", "priority"] as const +export type ServiceTier = (typeof ServiceTiers)[number] + +const TEXT_VERBOSITY = new Set(["low", "medium", "high"]) +const INCLUDABLES = new Set(ResponseIncludables) +const SERVICE_TIERS = new Set(ServiceTiers) + +const isTextVerbosity = (value: unknown): value is Schema.Schema.Type => + typeof value === "string" && TEXT_VERBOSITY.has(value) + +const isServiceTier = (value: unknown): value is ServiceTier => typeof value === "string" && SERVICE_TIERS.has(value) + +export const ReasoningEffort = Schema.String +export const TextVerbositySchema = TextVerbosity +export const ResponseIncludableSchema = Schema.Literals(ResponseIncludables) +export const ServiceTierSchema = Schema.Literals(ServiceTiers) + +export interface Resolved { + readonly instructions?: string + readonly store?: boolean + readonly promptCacheKey?: string + readonly reasoningEffort?: string + readonly reasoningSummary?: "auto" | "concise" | "detailed" + readonly include?: ReadonlyArray + readonly textVerbosity?: Schema.Schema.Type + readonly serviceTier?: ServiceTier +} + +export const resolve = (request: LLMRequest): Resolved => { + const input = request.providerOptions?.[request.model.route.providerMetadataKey ?? "openresponses"] + const include = Array.isArray(input?.include) + ? input.include.filter((entry): entry is ResponseIncludable => INCLUDABLES.has(entry)) + : [] + const reasoningSummary = input?.reasoningSummary + return { + instructions: typeof input?.instructions === "string" ? input.instructions : undefined, + store: typeof input?.store === "boolean" ? input.store : undefined, + promptCacheKey: typeof input?.promptCacheKey === "string" ? input.promptCacheKey : undefined, + reasoningEffort: typeof input?.reasoningEffort === "string" ? input.reasoningEffort : undefined, + reasoningSummary: + reasoningSummary === "auto" || reasoningSummary === "concise" || reasoningSummary === "detailed" + ? reasoningSummary + : undefined, + include: include.length > 0 ? include : undefined, + textVerbosity: isTextVerbosity(input?.textVerbosity) ? input.textVerbosity : undefined, + serviceTier: isServiceTier(input?.serviceTier) ? input.serviceTier : undefined, + } +} + +export * as OpenResponsesOptions from "./open-responses-options" diff --git a/packages/ai/src/protocols/utils/openai-options.ts b/packages/ai/src/protocols/utils/openai-options.ts index 5414923eda1d..fc95e6a36233 100644 --- a/packages/ai/src/protocols/utils/openai-options.ts +++ b/packages/ai/src/protocols/utils/openai-options.ts @@ -1,85 +1,23 @@ -import { Schema } from "effect" -import type { LLMRequest, TextVerbosity as TextVerbosityValue } from "../../schema" -import { ReasoningEfforts, TextVerbosity } from "../../schema" +import { ReasoningEfforts } from "../../schema" +import { OpenResponsesOptions } from "./open-responses-options" export const OpenAIReasoningEfforts = ReasoningEfforts export type OpenAIReasoningEffort = string // Mirrors OpenAI's `ResponseIncludable` union from the official SDK. Keep this // in lockstep with `openai-node/src/resources/responses/responses.ts`. -export const OpenAIResponseIncludables = [ - "file_search_call.results", - "web_search_call.results", - "web_search_call.action.sources", - "message.input_image.image_url", - "computer_call_output.output.image_url", - "code_interpreter_call.outputs", - "reasoning.encrypted_content", - "message.output_text.logprobs", -] as const -export type OpenAIResponseIncludable = (typeof OpenAIResponseIncludables)[number] -export const OpenAIServiceTiers = ["auto", "default", "flex", "priority"] as const -export type OpenAIServiceTier = (typeof OpenAIServiceTiers)[number] +export const OpenAIResponseIncludables = OpenResponsesOptions.ResponseIncludables +export type OpenAIResponseIncludable = OpenResponsesOptions.ResponseIncludable +export const OpenAIServiceTiers = OpenResponsesOptions.ServiceTiers +export type OpenAIServiceTier = OpenResponsesOptions.ServiceTier -const TEXT_VERBOSITY = new Set(["low", "medium", "high"]) -const INCLUDABLES = new Set(OpenAIResponseIncludables) -const SERVICE_TIERS = new Set(OpenAIServiceTiers) - -export const OpenAIReasoningEffort = Schema.String -export const OpenAITextVerbosity = TextVerbosity -export const OpenAIResponseIncludable = Schema.Literals(OpenAIResponseIncludables) -export const OpenAIServiceTier = Schema.Literals(OpenAIServiceTiers) +export const OpenAIReasoningEffort = OpenResponsesOptions.ReasoningEffort +export const OpenAITextVerbosity = OpenResponsesOptions.TextVerbositySchema +export const OpenAIResponseIncludable = OpenResponsesOptions.ResponseIncludableSchema +export const OpenAIServiceTier = OpenResponsesOptions.ServiceTierSchema export const isReasoningEffort = (effort: unknown): effort is OpenAIReasoningEffort => typeof effort === "string" -const isTextVerbosity = (value: unknown): value is TextVerbosityValue => - typeof value === "string" && TEXT_VERBOSITY.has(value) - -const options = (request: LLMRequest) => request.providerOptions?.openai - -export const store = (request: LLMRequest): boolean | undefined => { - const value = options(request)?.store - return typeof value === "boolean" ? value : undefined -} - -export const reasoningEffort = (request: LLMRequest): string | undefined => { - const value = options(request)?.reasoningEffort - return typeof value === "string" ? value : undefined -} - -export const reasoningSummary = (request: LLMRequest): "auto" | undefined => - options(request)?.reasoningSummary === "auto" ? "auto" : undefined - -// Resolve the OpenAI Responses `include` field. Filters out unknown -// includable values defensively so a typo in upstream config drops the -// invalid entry instead of poisoning the wire body. An empty array (either -// passed directly or produced by filtering) is treated as "no include" and -// returns undefined so the request body omits the field entirely. -export const include = (request: LLMRequest): ReadonlyArray | undefined => { - const value = options(request)?.include - if (!Array.isArray(value)) return undefined - const filtered = value.filter((entry): entry is OpenAIResponseIncludable => INCLUDABLES.has(entry)) - return filtered.length > 0 ? filtered : undefined -} - -export const promptCacheKey = (request: LLMRequest) => { - const value = options(request)?.promptCacheKey - return typeof value === "string" ? value : undefined -} - -export const textVerbosity = (request: LLMRequest) => { - const value = options(request)?.textVerbosity - return isTextVerbosity(value) ? value : undefined -} - -export const serviceTier = (request: LLMRequest) => { - const value = options(request)?.serviceTier - return typeof value === "string" && SERVICE_TIERS.has(value) ? (value as OpenAIServiceTier) : undefined -} - -export const instructions = (request: LLMRequest) => { - const value = options(request)?.instructions - return typeof value === "string" ? value : undefined -} +export const resolve = OpenResponsesOptions.resolve export * as OpenAIOptions from "./openai-options" diff --git a/packages/ai/src/protocols/utils/tool-schema.ts b/packages/ai/src/protocols/utils/tool-schema.ts index 3a311eb34ce9..473942939ce3 100644 --- a/packages/ai/src/protocols/utils/tool-schema.ts +++ b/packages/ai/src/protocols/utils/tool-schema.ts @@ -63,6 +63,8 @@ const openAI = (schema: JsonSchema): JsonSchema => { return isRecord(normalized) ? normalized : { type: "object" } } +const responses = openAI + const gemini = (schema: JsonSchema): JsonSchema => GeminiToolSchema.convert(schema) ?? {} const modelCompatibility = ( @@ -83,4 +85,5 @@ export const ToolSchemaProjection = { modelCompatibility, moonshot, openAI, + responses, } as const diff --git a/packages/ai/src/providers/google-vertex-responses.ts b/packages/ai/src/providers/google-vertex-responses.ts index 47ede2399643..e36c67472c1a 100644 --- a/packages/ai/src/providers/google-vertex-responses.ts +++ b/packages/ai/src/providers/google-vertex-responses.ts @@ -25,6 +25,7 @@ export interface Settings extends ProviderPackage.Settings { const route = OpenAICompatibleResponses.route.with({ id: "google-vertex-responses", provider: id, + providerOptions: { openresponses: { store: false } }, }) export const routes = [route] diff --git a/packages/ai/src/providers/open-responses-options.ts b/packages/ai/src/providers/open-responses-options.ts new file mode 100644 index 000000000000..686cdcb80836 --- /dev/null +++ b/packages/ai/src/providers/open-responses-options.ts @@ -0,0 +1,20 @@ +import type { ResponseIncludable, ServiceTier } from "../protocols/utils/open-responses-options" +import type { ProviderOptions, ReasoningEffort, TextVerbosity } from "../schema" + +export interface OpenResponsesOptionsInput { + readonly [key: string]: unknown + readonly instructions?: string + readonly store?: boolean + readonly promptCacheKey?: string + readonly reasoningEffort?: ReasoningEffort + readonly reasoningSummary?: "auto" | "concise" | "detailed" + readonly include?: ReadonlyArray + readonly textVerbosity?: TextVerbosity + readonly serviceTier?: ServiceTier +} + +export type OpenResponsesProviderOptionsInput = ProviderOptions & { + readonly openresponses?: OpenResponsesOptionsInput +} + +export * as OpenResponsesProviderOptions from "./open-responses-options" diff --git a/packages/ai/src/providers/openai-compatible-responses.ts b/packages/ai/src/providers/openai-compatible-responses.ts index 58b8ab65aefe..38293eb1d8de 100644 --- a/packages/ai/src/providers/openai-compatible-responses.ts +++ b/packages/ai/src/providers/openai-compatible-responses.ts @@ -3,7 +3,9 @@ import { OpenAICompatibleResponses } from "../protocols/openai-compatible-respon import { AuthOptions, type ProviderAuthOption } from "../route/auth-options" import type { RouteDefaultsInput } from "../route/client" import { ProviderID, type ModelID } from "../schema" -import type { OpenAIProviderOptionsInput } from "./openai-options" +import type { OpenResponsesProviderOptionsInput } from "./open-responses-options" + +export type { OpenResponsesOptionsInput, OpenResponsesProviderOptionsInput } from "./open-responses-options" export const id = ProviderID.make("openai-compatible") @@ -11,13 +13,14 @@ export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly provider?: string readonly baseURL: string + readonly providerOptions?: OpenResponsesProviderOptionsInput } export interface Settings extends ProviderPackage.Settings { readonly apiKey?: string readonly baseURL: string readonly provider?: string - readonly providerOptions?: OpenAIProviderOptionsInput + readonly providerOptions?: OpenResponsesProviderOptionsInput } export const routes = [OpenAICompatibleResponses.route] diff --git a/packages/ai/src/providers/openai-options.ts b/packages/ai/src/providers/openai-options.ts index fb548dd79726..86c7c2e33457 100644 --- a/packages/ai/src/providers/openai-options.ts +++ b/packages/ai/src/providers/openai-options.ts @@ -1,22 +1,10 @@ -import type { ProviderOptions, ReasoningEffort, TextVerbosity } from "../schema" +import type { ProviderOptions } from "../schema" import { mergeProviderOptions } from "../schema" -import type { OpenAIResponseIncludable, OpenAIServiceTier } from "../protocols/utils/openai-options" +import type { OpenResponsesOptionsInput } from "./open-responses-options" export type { OpenAIResponseIncludable, OpenAIServiceTier } from "../protocols/utils/openai-options" -export interface OpenAIOptionsInput { - readonly [key: string]: unknown - readonly store?: boolean - readonly promptCacheKey?: string - readonly reasoningEffort?: ReasoningEffort - readonly reasoningSummary?: "auto" - // OpenAI Responses `include` wire field. Mirrors the official SDK's - // `ResponseIncludable[]` union exactly so AI SDK callers and direct - // native-SDK callers share one shape and no translation is required. - readonly include?: ReadonlyArray - readonly textVerbosity?: TextVerbosity - readonly serviceTier?: OpenAIServiceTier -} +export type OpenAIOptionsInput = OpenResponsesOptionsInput export type OpenAIProviderOptionsInput = ProviderOptions & { readonly openai?: OpenAIOptionsInput diff --git a/packages/ai/src/route/protocol.ts b/packages/ai/src/route/protocol.ts index acb1e78c67bb..c7340ac063e5 100644 --- a/packages/ai/src/route/protocol.ts +++ b/packages/ai/src/route/protocol.ts @@ -12,7 +12,8 @@ import type { LLMError, LLMEvent, LLMRequest, ProtocolID } from "../schema" * Examples: * * - `OpenAIChat.protocol` — chat completions style - * - `OpenAIResponses.protocol` — responses API + * - `OpenResponses.protocol` — provider-neutral Responses API baseline + * - `OpenAIResponses.protocol` — OpenAI extensions to that baseline * - `AnthropicMessages.protocol` — messages API with content blocks * - `Gemini.protocol` — generateContent * - `BedrockConverse.protocol` — Converse with binary event-stream framing diff --git a/packages/ai/test/exports.test.ts b/packages/ai/test/exports.test.ts index 4629100da514..bea36c4c3bec 100644 --- a/packages/ai/test/exports.test.ts +++ b/packages/ai/test/exports.test.ts @@ -11,7 +11,13 @@ import { XAI, } from "@opencode-ai/ai/providers" import * as GitHubCopilot from "@opencode-ai/ai/providers/github-copilot" -import { OpenAIChat, OpenAICompatibleChat, OpenAICompatibleResponses, OpenAIResponses } from "@opencode-ai/ai/protocols" +import { + OpenAIChat, + OpenAICompatibleChat, + OpenAICompatibleResponses, + OpenAIResponses, + OpenResponses, +} from "@opencode-ai/ai/protocols" import * as AnthropicMessages from "@opencode-ai/ai/protocols/anthropic-messages" describe("public exports", () => { @@ -74,7 +80,9 @@ describe("public exports", () => { test("protocol barrels expose supported low-level routes", () => { expect(OpenAIChat.route.id).toBe("openai-chat") expect(OpenAICompatibleChat.route.id).toBe("openai-compatible-chat") + expect(OpenResponses.protocol.id).toBe("open-responses") expect(OpenAICompatibleResponses.route.id).toBe("openai-compatible-responses") + expect(OpenAICompatibleResponses.route.protocol).toBe("open-responses") expect(OpenAIResponses.route.id).toBe("openai-responses") expect(OpenAIResponses.webSocketRoute.id).toBe("openai-responses-websocket") expect(AnthropicMessages.route.id).toBe("anthropic-messages") diff --git a/packages/ai/test/provider-package.test.ts b/packages/ai/test/provider-package.test.ts index 46d4dbb503d7..94a86cdfb53b 100644 --- a/packages/ai/test/provider-package.test.ts +++ b/packages/ai/test/provider-package.test.ts @@ -59,7 +59,7 @@ describe("provider package entrypoints", () => { headers: { "x-application": "opencode" }, body: { service_tier: "priority" }, limits: { context: 200_000, output: 64_000 }, - providerOptions: { openai: { reasoningEffort: "low", store: true } }, + providerOptions: { openresponses: { reasoningEffort: "low", store: true } }, }) expect(String(selected.provider)).toBe("example") @@ -72,7 +72,7 @@ describe("provider package entrypoints", () => { expect(selected.route.defaults.http?.body).toEqual({ service_tier: "priority" }) expect(selected.route.defaults.limits).toEqual({ context: 200_000, output: 64_000 }) expect(selected.route.defaults.providerOptions).toEqual({ - openai: { reasoningEffort: "low", store: true }, + openresponses: { reasoningEffort: "low", store: true }, }) }) @@ -235,12 +235,12 @@ describe("provider package entrypoints", () => { path: "/chat/completions", }) expect(responses.route.id).toBe("google-vertex-responses") - expect(responses.route.protocol).toBe("openai-responses") + expect(responses.route.protocol).toBe("open-responses") expect(responses.route.endpoint).toMatchObject({ baseURL: "https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/endpoints/openapi", path: "/responses", }) - expect(responses.route.defaults.providerOptions).toEqual({ openai: { store: false } }) + expect(responses.route.defaults.providerOptions).toEqual({ openresponses: { store: false } }) }) test("rejects conflicting Vertex auth settings at runtime", async () => { diff --git a/packages/ai/test/provider/openai-compatible-responses.test.ts b/packages/ai/test/provider/openai-compatible-responses.test.ts index a43acb7683e2..39819821097f 100644 --- a/packages/ai/test/provider/openai-compatible-responses.test.ts +++ b/packages/ai/test/provider/openai-compatible-responses.test.ts @@ -1,17 +1,22 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" -import { LLM } from "../../src" +import { LLM, LLMEvent } from "../../src" import { configure } from "../../src/providers/openai-compatible-responses" +import { OpenAI } from "../../src/providers" +import { OpenResponses } from "../../src/protocols/open-responses" import { OpenAICompatibleResponses } from "../../src/protocols/openai-compatible-responses" import { OpenAIResponses } from "../../src/protocols/openai-responses" import { LLMClient } from "../../src/route" import { it } from "../lib/effect" +import { fixedResponse } from "../lib/http" +import { sseEvents } from "../lib/sse" -describe("OpenAI-compatible Responses route", () => { - it.effect("reuses the OpenAI Responses protocol for a configured deployment", () => +describe("Open Responses-compatible route", () => { + it.effect("uses the Open Responses baseline for a configured deployment", () => Effect.gen(function* () { - expect(OpenAICompatibleResponses.route.body).toBe(OpenAIResponses.protocol.body) - expect(OpenAICompatibleResponses.route.transport).toBe(OpenAIResponses.httpTransport) + expect(OpenAICompatibleResponses.route.body).toBe(OpenResponses.protocol.body) + expect(OpenAICompatibleResponses.route.transport).toBe(OpenResponses.httpTransport) + expect(OpenAICompatibleResponses.route.body).not.toBe(OpenAIResponses.protocol.body) const model = configure({ apiKey: "test-key", @@ -27,7 +32,7 @@ describe("OpenAI-compatible Responses route", () => { ) expect(prepared.route).toBe("openai-compatible-responses") - expect(prepared.protocol).toBe("openai-responses") + expect(prepared.protocol).toBe("open-responses") expect(prepared.model).toMatchObject({ id: "example-model", provider: "example", @@ -45,9 +50,67 @@ describe("OpenAI-compatible Responses route", () => { { role: "system", content: "You are concise." }, { role: "user", content: [{ type: "input_text", text: "Say hello." }] }, ], - store: false, stream: true, }) }), ) + + it.effect("rejects OpenAI-native tools", () => + Effect.gen(function* () { + const model = configure({ + apiKey: "test-key", + baseURL: "https://responses.example.test/v1", + }).model("example-model") + const error = yield* LLMClient.prepare( + LLM.request({ model, prompt: "Draw.", tools: [OpenAI.imageGeneration()] }), + ).pipe(Effect.flip) + + expect(error.reason._tag).toBe("InvalidRequest") + expect(error.message).toContain("Open Responses does not support provider-native tool image_generation") + }), + ) + + it.effect("reads standard options from the Open Responses namespace", () => + Effect.gen(function* () { + const model = configure({ + apiKey: "test-key", + baseURL: "https://responses.example.test/v1", + providerOptions: { openresponses: { reasoningEffort: "low", store: true } }, + }).model("example-model") + const prepared = yield* LLMClient.prepare(LLM.request({ model, prompt: "Think." })) + + expect(prepared.body).toMatchObject({ + reasoning: { effort: "low" }, + store: true, + }) + }), + ) + + it.effect("does not interpret OpenAI hosted-tool items", () => + Effect.gen(function* () { + const model = configure({ + apiKey: "test-key", + baseURL: "https://responses.example.test/v1", + provider: "example", + }).model("example-model") + const response = yield* LLMClient.generate(LLM.request({ model, prompt: "Search." })).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { + type: "response.output_item.done", + item: { type: "web_search_call", id: "ws_1", status: "completed", action: { query: "news" } }, + }, + { type: "response.completed", response: { id: "resp_1" } }, + ), + ), + ), + ) + + expect(response.toolCalls).toEqual([]) + expect(response.events.find(LLMEvent.is.finish)).toMatchObject({ + providerMetadata: { openresponses: { responseId: "resp_1" } }, + }) + }), + ) }) From c06186a9d9a3cf5e06a88eec97eee7abb882dcd7 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Fri, 24 Jul 2026 18:50:20 +0530 Subject: [PATCH 077/150] fix(ai): forward Anthropic provider options (#38694) --- packages/ai/src/providers/anthropic-compatible.ts | 4 +++- packages/ai/src/providers/anthropic.ts | 4 +++- packages/ai/test/provider-package.test.ts | 14 ++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/ai/src/providers/anthropic-compatible.ts b/packages/ai/src/providers/anthropic-compatible.ts index 7578d6307514..1350fae5d52e 100644 --- a/packages/ai/src/providers/anthropic-compatible.ts +++ b/packages/ai/src/providers/anthropic-compatible.ts @@ -3,7 +3,7 @@ import { AnthropicMessages } from "../protocols/anthropic-messages" import { Auth } from "../route/auth" import type { ProviderAuthOption } from "../route/auth-options" import type { RouteDefaultsInput } from "../route/client" -import { ProviderID, type ModelID } from "../schema" +import { ProviderID, type ModelID, type ProviderOptions } from "../schema" export const id = ProviderID.make("anthropic-compatible") @@ -20,6 +20,7 @@ export type Settings = ProviderPackage.Settings & ) & { readonly baseURL: string readonly provider?: string + readonly providerOptions?: ProviderOptions } export const routes = [AnthropicMessages.route] @@ -61,6 +62,7 @@ export const model: ProviderPackage.Definition["model"] = (modelID, se http: settings.body === undefined ? undefined : { body: { ...settings.body } }, limits: settings.limits, provider: settings.provider, + providerOptions: settings.providerOptions, }).model(modelID) } diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index d317d49e1c38..d8f1e425ba87 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -2,7 +2,7 @@ import type { RouteDefaultsInput } from "../route/client" import { Auth } from "../route/auth" import type { ProviderAuthOption } from "../route/auth-options" import type { ProviderPackage } from "../provider-package" -import { ProviderID, type ModelID } from "../schema" +import { ProviderID, type ModelID, type ProviderOptions } from "../schema" import { AnthropicMessages } from "../protocols/anthropic-messages" import { AnthropicCompatible } from "./anthropic-compatible" @@ -18,6 +18,7 @@ export type Settings = ProviderPackage.Settings & | { readonly apiKey?: never; readonly authToken?: string } ) & { readonly baseURL?: string + readonly providerOptions?: ProviderOptions } const auth = (options: ProviderAuthOption<"optional">) => { @@ -52,5 +53,6 @@ export const model: ProviderPackage.Definition["model"] = (modelID, se headers: settings.headers === undefined ? undefined : { ...settings.headers }, http: settings.body === undefined ? undefined : { body: { ...settings.body } }, limits: settings.limits, + providerOptions: settings.providerOptions, }).model(modelID) } diff --git a/packages/ai/test/provider-package.test.ts b/packages/ai/test/provider-package.test.ts index 94a86cdfb53b..45ab7dfa8459 100644 --- a/packages/ai/test/provider-package.test.ts +++ b/packages/ai/test/provider-package.test.ts @@ -85,6 +85,7 @@ describe("provider package entrypoints", () => { headers: { "x-application": "opencode" }, body: { metadata: { user_id: "user_1" } }, limits: { context: 200_000, output: 64_000 }, + providerOptions: { anthropic: { effort: "low" } }, }) expect(String(selected.provider)).toBe("example") @@ -96,6 +97,19 @@ describe("provider package entrypoints", () => { expect(selected.route.defaults.headers).toEqual({ "x-application": "opencode" }) expect(selected.route.defaults.http?.body).toEqual({ metadata: { user_id: "user_1" } }) expect(selected.route.defaults.limits).toEqual({ context: 200_000, output: 64_000 }) + expect(selected.route.defaults.providerOptions).toEqual({ anthropic: { effort: "low" } }) + }) + + test("maps Anthropic provider options onto the executable model", async () => { + const Anthropic = await import("@opencode-ai/ai/providers/anthropic") + const selected = Anthropic.model("claude-sonnet-4-6", { + apiKey: "fixture", + providerOptions: { anthropic: { thinking: { type: "adaptive" } } }, + }) + + expect(selected.route.defaults.providerOptions).toEqual({ + anthropic: { thinking: { type: "adaptive" } }, + }) }) test("requires an Anthropic-compatible base URL at runtime", async () => { From d90da82be2092080d5af5309015582acd4f94826 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Fri, 24 Jul 2026 19:02:02 +0530 Subject: [PATCH 078/150] refactor(ai): normalize provider option parsing (#38695) --- .../ai/src/protocols/anthropic-messages.ts | 56 ++---------------- packages/ai/src/protocols/gemini.ts | 27 +++------ .../src/protocols/utils/anthropic-options.ts | 57 +++++++++++++++++++ .../ai/src/protocols/utils/gemini-options.ts | 27 +++++++++ .../ai/src/providers/anthropic-compatible.ts | 8 ++- .../ai/src/providers/anthropic-options.ts | 26 +++++++++ packages/ai/src/providers/anthropic.ts | 13 ++++- packages/ai/src/providers/gemini-options.ts | 13 +++++ .../src/providers/google-vertex-messages.ts | 8 ++- packages/ai/src/providers/google-vertex.ts | 8 ++- packages/ai/src/providers/google.ts | 7 ++- packages/ai/test/auth-options.types.ts | 28 ++++++++- .../test/provider/anthropic-messages.test.ts | 41 ++++++++++++- packages/ai/test/provider/gemini.test.ts | 21 +++++++ 14 files changed, 256 insertions(+), 84 deletions(-) create mode 100644 packages/ai/src/protocols/utils/anthropic-options.ts create mode 100644 packages/ai/src/protocols/utils/gemini-options.ts create mode 100644 packages/ai/src/providers/anthropic-options.ts create mode 100644 packages/ai/src/providers/gemini-options.ts diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index d8fe2f446818..db0a0e34a1d1 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -22,6 +22,7 @@ import { import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" import { classifyProviderFailure } from "../provider-error" import * as Cache from "./utils/cache" +import { AnthropicOptions } from "./utils/anthropic-options" import { Lifecycle } from "./utils/lifecycle" import { ToolSchemaProjection } from "./utils/tool-schema" import { ToolStream } from "./utils/tool-stream" @@ -173,20 +174,6 @@ const AnthropicToolChoice = Schema.Union([ Schema.Struct({ type: Schema.tag("tool"), name: Schema.String }), ]) -const AnthropicThinking = Schema.Union([ - Schema.Struct({ - type: Schema.tag("enabled"), - budget_tokens: Schema.Number, - }), - Schema.Struct({ - type: Schema.tag("adaptive"), - display: Schema.optional(Schema.Literals(["summarized", "omitted"])), - }), - Schema.Struct({ - type: Schema.tag("disabled"), - }), -]) - const AnthropicOutputConfig = Schema.Struct({ effort: Schema.optional(Schema.String), }) @@ -203,7 +190,7 @@ const AnthropicBodyFields = { top_p: Schema.optional(Schema.Number), top_k: Schema.optional(Schema.Number), stop_sequences: optionalArray(Schema.String), - thinking: Schema.optional(AnthropicThinking), + thinking: Schema.optional(AnthropicOptions.ThinkingSchema), output_config: Schema.optional(AnthropicOutputConfig), } export const AnthropicMessagesBody = Schema.Struct(AnthropicBodyFields) @@ -537,37 +524,6 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* ( return messages }) -const anthropicOptions = (request: LLMRequest) => request.providerOptions?.anthropic - -const lowerThinking = Effect.fn("AnthropicMessages.lowerThinking")(function* (request: LLMRequest) { - const thinking = anthropicOptions(request)?.thinking - if (!ProviderShared.isRecord(thinking)) return undefined - if (thinking.type === "adaptive") { - const display = - thinking.display === "summarized" - ? ("summarized" as const) - : thinking.display === "omitted" - ? ("omitted" as const) - : undefined - return { type: "adaptive" as const, ...(display === undefined ? {} : { display }) } - } - if (thinking.type === "disabled") return { type: "disabled" as const } - if (thinking.type !== "enabled") return undefined - const budget = - typeof thinking.budgetTokens === "number" - ? thinking.budgetTokens - : typeof thinking.budget_tokens === "number" - ? thinking.budget_tokens - : undefined - if (budget === undefined) return yield* invalid("Anthropic thinking provider option requires budgetTokens") - return { type: "enabled" as const, budget_tokens: budget } -}) - -const outputConfig = (request: LLMRequest) => { - const effort = anthropicOptions(request)?.effort - return typeof effort === "string" ? { effort } : undefined -} - const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) { const generation = request.generation const toolSchemaCompatibility = request.model.compatibility?.toolSchema @@ -587,8 +543,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques ), ) // Anthropic rejects tool_choice when tools are absent; "none" is only meaningful with tools present. - const toolChoice = - tools === undefined || !request.toolChoice ? undefined : yield* lowerToolChoice(request.toolChoice) + const toolChoice = tools === undefined || !request.toolChoice ? undefined : yield* lowerToolChoice(request.toolChoice) const system = request.system.length === 0 ? undefined @@ -603,6 +558,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques `Anthropic Messages: dropped ${breakpoints.dropped} cache breakpoint(s); the API allows at most ${ANTHROPIC_BREAKPOINT_CAP} per request.`, ) } + const options = yield* AnthropicOptions.resolve(request) return { model: request.model.id, system, @@ -615,8 +571,8 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques top_p: generation?.topP, top_k: generation?.topK, stop_sequences: generation?.stop, - thinking: yield* lowerThinking(request), - output_config: outputConfig(request), + thinking: options.thinking, + output_config: options.effort === undefined ? undefined : { effort: options.effort }, } }) diff --git a/packages/ai/src/protocols/gemini.ts b/packages/ai/src/protocols/gemini.ts index 1c285da235d5..48eca6f599ad 100644 --- a/packages/ai/src/protocols/gemini.ts +++ b/packages/ai/src/protocols/gemini.ts @@ -18,6 +18,7 @@ import { type ToolContent, } from "../schema" import { JsonObject, optionalArray, ProviderShared } from "./shared" +import { GeminiOptions } from "./utils/gemini-options" import { GeminiToolSchema } from "./utils/gemini-tool-schema" import { Lifecycle } from "./utils/lifecycle" import { ToolSchemaProjection } from "./utils/tool-schema" @@ -95,18 +96,13 @@ const GeminiToolConfig = Schema.Struct({ }), }) -const GeminiThinkingConfig = Schema.Struct({ - thinkingBudget: Schema.optional(Schema.Number), - includeThoughts: Schema.optional(Schema.Boolean), -}) - const GeminiGenerationConfig = Schema.Struct({ maxOutputTokens: Schema.optional(Schema.Number), temperature: Schema.optional(Schema.Number), topP: Schema.optional(Schema.Number), topK: Schema.optional(Schema.Number), stopSequences: optionalArray(Schema.String), - thinkingConfig: Schema.optional(GeminiThinkingConfig), + thinkingConfig: Schema.optional(GeminiOptions.ThinkingConfigSchema), }) const GeminiBodyFields = { @@ -203,7 +199,9 @@ const thoughtSignature = (providerMetadata: ProviderMetadata | undefined) => { const functionCallId = (providerMetadata: ProviderMetadata | undefined) => { const google = providerMetadata?.google - return ProviderShared.isRecord(google) && typeof google.functionCallId === "string" ? google.functionCallId : undefined + return ProviderShared.isRecord(google) && typeof google.functionCallId === "string" + ? google.functionCallId + : undefined } const lowerToolCall = (part: ToolCallPart) => ({ @@ -300,21 +298,10 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR return contents }) -const geminiOptions = (request: LLMRequest) => request.providerOptions?.gemini - -const thinkingConfig = (request: LLMRequest) => { - const value = geminiOptions(request)?.thinkingConfig - if (!ProviderShared.isRecord(value)) return undefined - const result = { - thinkingBudget: typeof value.thinkingBudget === "number" ? value.thinkingBudget : undefined, - includeThoughts: typeof value.includeThoughts === "boolean" ? value.includeThoughts : undefined, - } - return Object.values(result).some((item) => item !== undefined) ? result : undefined -} - const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMRequest) { const hasTools = request.tools.length > 0 const generation = request.generation + const options = GeminiOptions.resolve(request) const toolSchemaCompatibility = request.model.compatibility?.toolSchema const generationConfig = { maxOutputTokens: generation?.maxTokens, @@ -322,7 +309,7 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMReque topP: generation?.topP, topK: generation?.topK, stopSequences: generation?.stop, - thinkingConfig: thinkingConfig(request), + thinkingConfig: options.thinkingConfig, } return { diff --git a/packages/ai/src/protocols/utils/anthropic-options.ts b/packages/ai/src/protocols/utils/anthropic-options.ts new file mode 100644 index 000000000000..63ee00b70c71 --- /dev/null +++ b/packages/ai/src/protocols/utils/anthropic-options.ts @@ -0,0 +1,57 @@ +import { Effect, Schema } from "effect" +import type { LLMRequest } from "../../schema" +import { ProviderShared } from "../shared" + +export const ThinkingSchema = Schema.Union([ + Schema.Struct({ + type: Schema.tag("enabled"), + budget_tokens: Schema.Number, + }), + Schema.Struct({ + type: Schema.tag("adaptive"), + display: Schema.optional(Schema.Literals(["summarized", "omitted"])), + }), + Schema.Struct({ + type: Schema.tag("disabled"), + }), +]) +export type Thinking = Schema.Schema.Type + +export interface Resolved { + readonly thinking?: Thinking + readonly effort?: string +} + +export const resolve = Effect.fn("AnthropicOptions.resolve")(function* (request: LLMRequest) { + const input = request.providerOptions?.anthropic + return { + thinking: yield* resolveThinking(input?.thinking), + effort: typeof input?.effort === "string" ? input.effort : undefined, + } satisfies Resolved +}) + +const resolveThinking = Effect.fn("AnthropicOptions.resolveThinking")(function* (input: unknown) { + if (!ProviderShared.isRecord(input)) return undefined + if (input.type === "adaptive") { + const display = + input.display === "summarized" + ? ("summarized" as const) + : input.display === "omitted" + ? ("omitted" as const) + : undefined + return { type: "adaptive" as const, ...(display === undefined ? {} : { display }) } + } + if (input.type === "disabled") return { type: "disabled" as const } + if (input.type !== "enabled") return undefined + const budget = + typeof input.budgetTokens === "number" + ? input.budgetTokens + : typeof input.budget_tokens === "number" + ? input.budget_tokens + : undefined + if (budget === undefined) + return yield* ProviderShared.invalidRequest("Anthropic thinking provider option requires budgetTokens") + return { type: "enabled" as const, budget_tokens: budget } +}) + +export * as AnthropicOptions from "./anthropic-options" diff --git a/packages/ai/src/protocols/utils/gemini-options.ts b/packages/ai/src/protocols/utils/gemini-options.ts new file mode 100644 index 000000000000..f4828a00a41b --- /dev/null +++ b/packages/ai/src/protocols/utils/gemini-options.ts @@ -0,0 +1,27 @@ +import { Schema } from "effect" +import type { LLMRequest } from "../../schema" +import { ProviderShared } from "../shared" + +export const ThinkingConfigSchema = Schema.Struct({ + thinkingBudget: Schema.optional(Schema.Number), + includeThoughts: Schema.optional(Schema.Boolean), +}) +export type ThinkingConfig = Schema.Schema.Type + +export interface Resolved { + readonly thinkingConfig?: ThinkingConfig +} + +export const resolve = (request: LLMRequest): Resolved => { + const value = request.providerOptions?.gemini?.thinkingConfig + if (!ProviderShared.isRecord(value)) return {} + const thinkingConfig = { + thinkingBudget: typeof value.thinkingBudget === "number" ? value.thinkingBudget : undefined, + includeThoughts: typeof value.includeThoughts === "boolean" ? value.includeThoughts : undefined, + } + return { + thinkingConfig: Object.values(thinkingConfig).some((item) => item !== undefined) ? thinkingConfig : undefined, + } +} + +export * as GeminiOptions from "./gemini-options" diff --git a/packages/ai/src/providers/anthropic-compatible.ts b/packages/ai/src/providers/anthropic-compatible.ts index 1350fae5d52e..50867ebb4404 100644 --- a/packages/ai/src/providers/anthropic-compatible.ts +++ b/packages/ai/src/providers/anthropic-compatible.ts @@ -3,7 +3,10 @@ import { AnthropicMessages } from "../protocols/anthropic-messages" import { Auth } from "../route/auth" import type { ProviderAuthOption } from "../route/auth-options" import type { RouteDefaultsInput } from "../route/client" -import { ProviderID, type ModelID, type ProviderOptions } from "../schema" +import { ProviderID, type ModelID } from "../schema" +import type { AnthropicProviderOptionsInput } from "./anthropic-options" + +export type { AnthropicOptionsInput, AnthropicProviderOptionsInput, AnthropicThinkingInput } from "./anthropic-options" export const id = ProviderID.make("anthropic-compatible") @@ -11,6 +14,7 @@ export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly provider?: string readonly baseURL: string + readonly providerOptions?: AnthropicProviderOptionsInput } export type Settings = ProviderPackage.Settings & @@ -20,7 +24,7 @@ export type Settings = ProviderPackage.Settings & ) & { readonly baseURL: string readonly provider?: string - readonly providerOptions?: ProviderOptions + readonly providerOptions?: AnthropicProviderOptionsInput } export const routes = [AnthropicMessages.route] diff --git a/packages/ai/src/providers/anthropic-options.ts b/packages/ai/src/providers/anthropic-options.ts new file mode 100644 index 000000000000..61f81cc07466 --- /dev/null +++ b/packages/ai/src/providers/anthropic-options.ts @@ -0,0 +1,26 @@ +import type { ProviderOptions } from "../schema" + +export type AnthropicThinkingInput = + | { + readonly type: "adaptive" + readonly display?: "summarized" | "omitted" + } + | { + readonly type: "disabled" + } + | ({ readonly type: "enabled" } & ( + | { readonly budgetTokens: number; readonly budget_tokens?: number } + | { readonly budgetTokens?: number; readonly budget_tokens: number } + )) + +export interface AnthropicOptionsInput { + readonly [key: string]: unknown + readonly thinking?: AnthropicThinkingInput + readonly effort?: string +} + +export type AnthropicProviderOptionsInput = ProviderOptions & { + readonly anthropic?: AnthropicOptionsInput +} + +export * as AnthropicProviderOptions from "./anthropic-options" diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index d8f1e425ba87..5eedb63ac300 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -2,15 +2,22 @@ import type { RouteDefaultsInput } from "../route/client" import { Auth } from "../route/auth" import type { ProviderAuthOption } from "../route/auth-options" import type { ProviderPackage } from "../provider-package" -import { ProviderID, type ModelID, type ProviderOptions } from "../schema" +import { ProviderID, type ModelID } from "../schema" import { AnthropicMessages } from "../protocols/anthropic-messages" import { AnthropicCompatible } from "./anthropic-compatible" +import type { AnthropicProviderOptionsInput } from "./anthropic-options" + +export type { AnthropicOptionsInput, AnthropicProviderOptionsInput, AnthropicThinkingInput } from "./anthropic-options" export const id = ProviderID.make("anthropic") export const routes = [AnthropicMessages.route] -export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string } +export type Config = RouteDefaultsInput & + ProviderAuthOption<"optional"> & { + readonly baseURL?: string + readonly providerOptions?: AnthropicProviderOptionsInput + } export type Settings = ProviderPackage.Settings & ( @@ -18,7 +25,7 @@ export type Settings = ProviderPackage.Settings & | { readonly apiKey?: never; readonly authToken?: string } ) & { readonly baseURL?: string - readonly providerOptions?: ProviderOptions + readonly providerOptions?: AnthropicProviderOptionsInput } const auth = (options: ProviderAuthOption<"optional">) => { diff --git a/packages/ai/src/providers/gemini-options.ts b/packages/ai/src/providers/gemini-options.ts new file mode 100644 index 000000000000..067c38e144a0 --- /dev/null +++ b/packages/ai/src/providers/gemini-options.ts @@ -0,0 +1,13 @@ +import type { ThinkingConfig } from "../protocols/utils/gemini-options" +import type { ProviderOptions } from "../schema" + +export interface GeminiOptionsInput { + readonly [key: string]: unknown + readonly thinkingConfig?: ThinkingConfig +} + +export type GeminiProviderOptionsInput = ProviderOptions & { + readonly gemini?: GeminiOptionsInput +} + +export * as GeminiProviderOptions from "./gemini-options" diff --git a/packages/ai/src/providers/google-vertex-messages.ts b/packages/ai/src/providers/google-vertex-messages.ts index 7cb6f9cfb286..b52eb94f9317 100644 --- a/packages/ai/src/providers/google-vertex-messages.ts +++ b/packages/ai/src/providers/google-vertex-messages.ts @@ -6,9 +6,12 @@ import { Route, type RouteDefaultsInput } from "../route/client" import { Endpoint } from "../route/endpoint" import { Framing } from "../route/framing" import { Protocol } from "../route/protocol" -import { ProviderID, type ModelID, type ProviderOptions } from "../schema" +import { ProviderID, type ModelID } from "../schema" +import type { AnthropicProviderOptionsInput } from "./anthropic-options" import { GoogleVertexShared } from "./google-vertex-shared" +export type { AnthropicOptionsInput, AnthropicProviderOptionsInput, AnthropicThinkingInput } from "./anthropic-options" + const VERSION = "vertex-2023-10-16" as const // models.dev uses this provider id even though the API contract is Anthropic Messages. @@ -19,6 +22,7 @@ export type Config = RouteDefaultsInput & readonly baseURL?: string readonly location?: string readonly project?: string + readonly providerOptions?: AnthropicProviderOptionsInput } export interface Settings extends ProviderPackage.Settings { @@ -27,7 +31,7 @@ export interface Settings extends ProviderPackage.Settings { readonly baseURL?: string readonly location?: string readonly project?: string - readonly providerOptions?: ProviderOptions + readonly providerOptions?: AnthropicProviderOptionsInput } const route = Route.make({ diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts index a0f44711d45a..f7994fb16d6f 100644 --- a/packages/ai/src/providers/google-vertex.ts +++ b/packages/ai/src/providers/google-vertex.ts @@ -4,9 +4,12 @@ import { Auth } from "../route/auth" import { Route, type RouteDefaultsInput } from "../route/client" import { Endpoint } from "../route/endpoint" import { Framing } from "../route/framing" -import { ProviderID, type ModelID, type ProviderOptions } from "../schema" +import { ProviderID, type ModelID } from "../schema" +import type { GeminiProviderOptionsInput } from "./gemini-options" import { GoogleVertexShared } from "./google-vertex-shared" +export type { GeminiOptionsInput, GeminiProviderOptionsInput } from "./gemini-options" + export const id = ProviderID.make("google-vertex") export type Config = RouteDefaultsInput & @@ -14,6 +17,7 @@ export type Config = RouteDefaultsInput & readonly baseURL?: string readonly location?: string readonly project?: string + readonly providerOptions?: GeminiProviderOptionsInput } export type Settings = ProviderPackage.Settings & @@ -24,7 +28,7 @@ export type Settings = ProviderPackage.Settings & readonly baseURL?: string readonly location?: string readonly project?: string - readonly providerOptions?: ProviderOptions + readonly providerOptions?: GeminiProviderOptionsInput } const route = Route.make({ diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts index 28220bb847b7..0199d594e631 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -2,11 +2,13 @@ import type { RouteDefaultsInput } from "../route/client" import { Auth } from "../route/auth" import type { ProviderAuthOption } from "../route/auth-options" import type { ProviderPackage } from "../provider-package" -import { HttpOptions, ProviderID, mergeHttpOptions, type ModelID, type ProviderOptions } from "../schema" +import { HttpOptions, ProviderID, mergeHttpOptions, type ModelID } from "../schema" import { Gemini } from "../protocols/gemini" import { GoogleImages } from "../protocols/google-images" +import type { GeminiProviderOptionsInput } from "./gemini-options" export type { GoogleImageOptions } from "../protocols/google-images" +export type { GeminiOptionsInput, GeminiProviderOptionsInput } from "./gemini-options" export const id = ProviderID.make("google") @@ -15,12 +17,13 @@ export const routes = [Gemini.route] export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string + readonly providerOptions?: GeminiProviderOptionsInput } export interface Settings extends ProviderPackage.Settings { readonly apiKey?: string readonly baseURL?: string - readonly providerOptions?: ProviderOptions + readonly providerOptions?: GeminiProviderOptionsInput } const auth = (options: ProviderAuthOption<"optional">) => { diff --git a/packages/ai/test/auth-options.types.ts b/packages/ai/test/auth-options.types.ts index c43010678995..4572a0aee545 100644 --- a/packages/ai/test/auth-options.types.ts +++ b/packages/ai/test/auth-options.types.ts @@ -137,15 +137,26 @@ Azure.configure({ apiKey: "azure-key", resourceName: "resource" }).chat("deploym Azure.configure({ resourceName: "resource", apiKey: "azure-key", auth: Auth.header("api-key", "override") }) Anthropic.configure({ apiKey: "anthropic-key" }).model("claude-haiku") +Anthropic.configure({ + apiKey: "anthropic-key", + providerOptions: { + anthropic: { thinking: { type: "enabled", budgetTokens: 1_024 }, effort: "high" }, + }, +}).model("claude-haiku") // @ts-expect-error Anthropic model selectors only accept model ids. Anthropic.configure({ apiKey: "anthropic-key" }).model("claude-haiku", {}) // @ts-expect-error Anthropic package settings accept only one auth source. Anthropic.model("claude-sonnet-4-6", { apiKey: "anthropic-key", authToken: "anthropic-token" }) +// @ts-expect-error Enabled Anthropic thinking requires a token budget. +Anthropic.configure({ providerOptions: { anthropic: { thinking: { type: "enabled" } } } }) +// @ts-expect-error Anthropic thinking budgets must be numbers. +Anthropic.configure({ providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: "large" } } } }) AnthropicCompatible.configure({ apiKey: "messages-key", baseURL: "https://messages.example.com/v1", provider: "example", + providerOptions: { anthropic: { thinking: { type: "disabled" } } }, }).model("compatible-model") // @ts-expect-error Anthropic-compatible providers require a base URL. AnthropicCompatible.configure({ apiKey: "messages-key" }) @@ -159,10 +170,19 @@ AnthropicCompatible.model("compatible-model", { }) Google.configure({ apiKey: "google-key" }).model("gemini-2.5-flash") +Google.configure({ + apiKey: "google-key", + providerOptions: { gemini: { thinkingConfig: { thinkingBudget: 0, includeThoughts: false } } }, +}).model("gemini-2.5-flash") // @ts-expect-error Google model selectors only accept model ids. Google.configure({ apiKey: "google-key" }).model("gemini-2.5-flash", {}) +// @ts-expect-error Gemini thinking budgets must be numbers. +Google.configure({ providerOptions: { gemini: { thinkingConfig: { thinkingBudget: "large" } } } }) -GoogleVertex.configure({ apiKey: "vertex-key" }).model("gemini-3.5-flash") +GoogleVertex.configure({ + apiKey: "vertex-key", + providerOptions: { gemini: { thinkingConfig: { thinkingBudget: 1_024 } } }, +}).model("gemini-3.5-flash") GoogleVertex.configure({ accessToken: "vertex-token", project: "project" }).model("gemini-3.5-flash") GoogleVertex.configure({ auth: Auth.bearer("vertex-token"), project: "project" }).model("gemini-3.5-flash") // @ts-expect-error Vertex Gemini model selectors only accept model ids. @@ -208,7 +228,11 @@ GoogleVertexResponses.configure({ project: "project", }) -GoogleVertexMessages.configure({ accessToken: "vertex-token", project: "project" }).model("claude-sonnet-4-6") +GoogleVertexMessages.configure({ + accessToken: "vertex-token", + project: "project", + providerOptions: { anthropic: { thinking: { type: "adaptive", display: "omitted" }, effort: "low" } }, +}).model("claude-sonnet-4-6") // @ts-expect-error Vertex Messages package settings do not accept API keys. GoogleVertexMessages.model("claude-sonnet-4-6", { apiKey: "vertex-key", project: "project" }) GoogleVertexMessages.configure({ auth: Auth.bearer("vertex-token"), project: "project" }).model("claude-sonnet-4-6") diff --git a/packages/ai/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts index 021738d75f28..564cf4990012 100644 --- a/packages/ai/test/provider/anthropic-messages.test.ts +++ b/packages/ai/test/provider/anthropic-messages.test.ts @@ -74,6 +74,42 @@ describe("Anthropic Messages route", () => { }), ) + it.effect("normalizes enabled and disabled thinking settings", () => + Effect.gen(function* () { + const enabled = yield* LLMClient.prepare( + LLM.updateRequest(request, { + providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 1_024 } } }, + }), + ) + const legacy = yield* LLMClient.prepare( + LLM.updateRequest(request, { + providerOptions: { anthropic: { thinking: { type: "enabled", budget_tokens: 2_048 } } }, + }), + ) + const disabled = yield* LLMClient.prepare( + LLM.updateRequest(request, { + providerOptions: { anthropic: { thinking: { type: "disabled" } } }, + }), + ) + + expect(enabled.body.thinking).toEqual({ type: "enabled", budget_tokens: 1_024 }) + expect(legacy.body.thinking).toEqual({ type: "enabled", budget_tokens: 2_048 }) + expect(disabled.body.thinking).toEqual({ type: "disabled" }) + }), + ) + + it.effect("rejects enabled thinking without a budget", () => + Effect.gen(function* () { + const error = yield* LLMClient.prepare( + LLM.updateRequest(request, { + providerOptions: { anthropic: { thinking: { type: "enabled" } } }, + }), + ).pipe(Effect.flip) + + expect(error.message).toContain("Anthropic thinking provider option requires budgetTokens") + }), + ) + it.effect("lowers chronological system updates natively for Claude Opus 4.8 with cache hints", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( @@ -993,7 +1029,10 @@ describe("Anthropic Messages route", () => { content: [ { type: "text", text: "What is in this image?" }, { type: "image", source: { type: "base64", media_type: "image/png", data: "AAECAw==" } }, - { type: "document", source: { type: "base64", media_type: "application/pdf", data: "JVBERi0xLjQ=" } }, + { + type: "document", + source: { type: "base64", media_type: "application/pdf", data: "JVBERi0xLjQ=" }, + }, ], }, ], diff --git a/packages/ai/test/provider/gemini.test.ts b/packages/ai/test/provider/gemini.test.ts index 66cfb3482023..7549f36996c0 100644 --- a/packages/ai/test/provider/gemini.test.ts +++ b/packages/ai/test/provider/gemini.test.ts @@ -36,6 +36,27 @@ describe("Gemini route", () => { }), ) + it.effect("normalizes Gemini thinking options", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.updateRequest(request, { + providerOptions: { gemini: { thinkingConfig: { thinkingBudget: 0, includeThoughts: false } } }, + }), + ) + const filtered = yield* LLMClient.prepare( + LLM.updateRequest(request, { + providerOptions: { gemini: { thinkingConfig: { thinkingBudget: "invalid", includeThoughts: false } } }, + }), + ) + + expect(prepared.body.generationConfig?.thinkingConfig).toEqual({ + thinkingBudget: 0, + includeThoughts: false, + }) + expect(filtered.body.generationConfig?.thinkingConfig).toEqual({ includeThoughts: false }) + }), + ) + it.effect("lowers chronological system updates to wrapped user text in order", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( From 0374d292327961f5afcbf9cea22a4a7ddc0345f9 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Fri, 24 Jul 2026 19:08:36 +0530 Subject: [PATCH 079/150] refactor(ai): colocate provider options (#38698) --- .../ai/src/protocols/anthropic-messages.ts | 75 ++++++++++++++++++- packages/ai/src/protocols/gemini.ts | 35 ++++++++- .../src/protocols/utils/anthropic-options.ts | 57 -------------- .../ai/src/protocols/utils/gemini-options.ts | 27 ------- .../ai/src/providers/anthropic-compatible.ts | 9 ++- .../ai/src/providers/anthropic-options.ts | 26 ------- packages/ai/src/providers/anthropic.ts | 9 ++- packages/ai/src/providers/gemini-options.ts | 13 ---- .../src/providers/google-vertex-messages.ts | 9 ++- packages/ai/src/providers/google-vertex.ts | 8 +- packages/ai/src/providers/google.ts | 8 +- 11 files changed, 127 insertions(+), 149 deletions(-) delete mode 100644 packages/ai/src/protocols/utils/anthropic-options.ts delete mode 100644 packages/ai/src/protocols/utils/gemini-options.ts delete mode 100644 packages/ai/src/providers/anthropic-options.ts delete mode 100644 packages/ai/src/providers/gemini-options.ts diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index db0a0e34a1d1..b3a35f986e86 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -13,6 +13,7 @@ import { type JsonSchema, type LLMRequest, type MediaPart, + type ProviderOptions, type ProviderMetadata, type ToolCallPart, type ToolDefinition, @@ -22,7 +23,6 @@ import { import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" import { classifyProviderFailure } from "../provider-error" import * as Cache from "./utils/cache" -import { AnthropicOptions } from "./utils/anthropic-options" import { Lifecycle } from "./utils/lifecycle" import { ToolSchemaProjection } from "./utils/tool-schema" import { ToolStream } from "./utils/tool-stream" @@ -32,6 +32,29 @@ const MEDIA_MIMES = new Set([...ProviderShared.IMAGE_MIMES, ...ProviderS export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1" export const PATH = "/messages" +export type ThinkingInput = + | { + readonly type: "adaptive" + readonly display?: "summarized" | "omitted" + } + | { + readonly type: "disabled" + } + | ({ readonly type: "enabled" } & ( + | { readonly budgetTokens: number; readonly budget_tokens?: number } + | { readonly budgetTokens?: number; readonly budget_tokens: number } + )) + +export interface OptionsInput { + readonly [key: string]: unknown + readonly thinking?: ThinkingInput + readonly effort?: string +} + +export type ProviderOptionsInput = ProviderOptions & { + readonly anthropic?: OptionsInput +} + // ============================================================================= // Request Body Schema // ============================================================================= @@ -174,6 +197,20 @@ const AnthropicToolChoice = Schema.Union([ Schema.Struct({ type: Schema.tag("tool"), name: Schema.String }), ]) +const AnthropicThinking = Schema.Union([ + Schema.Struct({ + type: Schema.tag("enabled"), + budget_tokens: Schema.Number, + }), + Schema.Struct({ + type: Schema.tag("adaptive"), + display: Schema.optional(Schema.Literals(["summarized", "omitted"])), + }), + Schema.Struct({ + type: Schema.tag("disabled"), + }), +]) + const AnthropicOutputConfig = Schema.Struct({ effort: Schema.optional(Schema.String), }) @@ -190,7 +227,7 @@ const AnthropicBodyFields = { top_p: Schema.optional(Schema.Number), top_k: Schema.optional(Schema.Number), stop_sequences: optionalArray(Schema.String), - thinking: Schema.optional(AnthropicOptions.ThinkingSchema), + thinking: Schema.optional(AnthropicThinking), output_config: Schema.optional(AnthropicOutputConfig), } export const AnthropicMessagesBody = Schema.Struct(AnthropicBodyFields) @@ -524,6 +561,38 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* ( return messages }) +const resolveOptions = Effect.fn("AnthropicMessages.resolveOptions")(function* (request: LLMRequest) { + const input = request.providerOptions?.anthropic + return { + thinking: yield* resolveThinking(input?.thinking), + effort: typeof input?.effort === "string" ? input.effort : undefined, + } +}) + +const resolveThinking = Effect.fn("AnthropicMessages.resolveThinking")(function* (input: unknown) { + if (!ProviderShared.isRecord(input)) return undefined + if (input.type === "adaptive") { + const display = + input.display === "summarized" + ? ("summarized" as const) + : input.display === "omitted" + ? ("omitted" as const) + : undefined + return { type: "adaptive" as const, ...(display === undefined ? {} : { display }) } + } + if (input.type === "disabled") return { type: "disabled" as const } + if (input.type !== "enabled") return undefined + const budget = + typeof input.budgetTokens === "number" + ? input.budgetTokens + : typeof input.budget_tokens === "number" + ? input.budget_tokens + : undefined + if (budget === undefined) + return yield* ProviderShared.invalidRequest("Anthropic thinking provider option requires budgetTokens") + return { type: "enabled" as const, budget_tokens: budget } +}) + const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) { const generation = request.generation const toolSchemaCompatibility = request.model.compatibility?.toolSchema @@ -558,7 +627,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques `Anthropic Messages: dropped ${breakpoints.dropped} cache breakpoint(s); the API allows at most ${ANTHROPIC_BREAKPOINT_CAP} per request.`, ) } - const options = yield* AnthropicOptions.resolve(request) + const options = yield* resolveOptions(request) return { model: request.model.id, system, diff --git a/packages/ai/src/protocols/gemini.ts b/packages/ai/src/protocols/gemini.ts index 48eca6f599ad..8a62d50fb893 100644 --- a/packages/ai/src/protocols/gemini.ts +++ b/packages/ai/src/protocols/gemini.ts @@ -11,6 +11,7 @@ import { type JsonSchema, type LLMRequest, type MediaPart, + type ProviderOptions, type ProviderMetadata, type TextPart, type ToolCallPart, @@ -18,7 +19,6 @@ import { type ToolContent, } from "../schema" import { JsonObject, optionalArray, ProviderShared } from "./shared" -import { GeminiOptions } from "./utils/gemini-options" import { GeminiToolSchema } from "./utils/gemini-tool-schema" import { Lifecycle } from "./utils/lifecycle" import { ToolSchemaProjection } from "./utils/tool-schema" @@ -27,6 +27,18 @@ const ADAPTER = "gemini" const MEDIA_MIMES = new Set(ProviderShared.MEDIA_MIMES) export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta" +export interface OptionsInput { + readonly [key: string]: unknown + readonly thinkingConfig?: { + readonly thinkingBudget?: number + readonly includeThoughts?: boolean + } +} + +export type ProviderOptionsInput = ProviderOptions & { + readonly gemini?: OptionsInput +} + // ============================================================================= // Request Body Schema // ============================================================================= @@ -96,13 +108,18 @@ const GeminiToolConfig = Schema.Struct({ }), }) +const GeminiThinkingConfig = Schema.Struct({ + thinkingBudget: Schema.optional(Schema.Number), + includeThoughts: Schema.optional(Schema.Boolean), +}) + const GeminiGenerationConfig = Schema.Struct({ maxOutputTokens: Schema.optional(Schema.Number), temperature: Schema.optional(Schema.Number), topP: Schema.optional(Schema.Number), topK: Schema.optional(Schema.Number), stopSequences: optionalArray(Schema.String), - thinkingConfig: Schema.optional(GeminiOptions.ThinkingConfigSchema), + thinkingConfig: Schema.optional(GeminiThinkingConfig), }) const GeminiBodyFields = { @@ -298,10 +315,22 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR return contents }) +const resolveOptions = (request: LLMRequest) => { + const value = request.providerOptions?.gemini?.thinkingConfig + if (!ProviderShared.isRecord(value)) return {} + const thinkingConfig = { + thinkingBudget: typeof value.thinkingBudget === "number" ? value.thinkingBudget : undefined, + includeThoughts: typeof value.includeThoughts === "boolean" ? value.includeThoughts : undefined, + } + return { + thinkingConfig: Object.values(thinkingConfig).some((item) => item !== undefined) ? thinkingConfig : undefined, + } +} + const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMRequest) { const hasTools = request.tools.length > 0 const generation = request.generation - const options = GeminiOptions.resolve(request) + const options = resolveOptions(request) const toolSchemaCompatibility = request.model.compatibility?.toolSchema const generationConfig = { maxOutputTokens: generation?.maxTokens, diff --git a/packages/ai/src/protocols/utils/anthropic-options.ts b/packages/ai/src/protocols/utils/anthropic-options.ts deleted file mode 100644 index 63ee00b70c71..000000000000 --- a/packages/ai/src/protocols/utils/anthropic-options.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Effect, Schema } from "effect" -import type { LLMRequest } from "../../schema" -import { ProviderShared } from "../shared" - -export const ThinkingSchema = Schema.Union([ - Schema.Struct({ - type: Schema.tag("enabled"), - budget_tokens: Schema.Number, - }), - Schema.Struct({ - type: Schema.tag("adaptive"), - display: Schema.optional(Schema.Literals(["summarized", "omitted"])), - }), - Schema.Struct({ - type: Schema.tag("disabled"), - }), -]) -export type Thinking = Schema.Schema.Type - -export interface Resolved { - readonly thinking?: Thinking - readonly effort?: string -} - -export const resolve = Effect.fn("AnthropicOptions.resolve")(function* (request: LLMRequest) { - const input = request.providerOptions?.anthropic - return { - thinking: yield* resolveThinking(input?.thinking), - effort: typeof input?.effort === "string" ? input.effort : undefined, - } satisfies Resolved -}) - -const resolveThinking = Effect.fn("AnthropicOptions.resolveThinking")(function* (input: unknown) { - if (!ProviderShared.isRecord(input)) return undefined - if (input.type === "adaptive") { - const display = - input.display === "summarized" - ? ("summarized" as const) - : input.display === "omitted" - ? ("omitted" as const) - : undefined - return { type: "adaptive" as const, ...(display === undefined ? {} : { display }) } - } - if (input.type === "disabled") return { type: "disabled" as const } - if (input.type !== "enabled") return undefined - const budget = - typeof input.budgetTokens === "number" - ? input.budgetTokens - : typeof input.budget_tokens === "number" - ? input.budget_tokens - : undefined - if (budget === undefined) - return yield* ProviderShared.invalidRequest("Anthropic thinking provider option requires budgetTokens") - return { type: "enabled" as const, budget_tokens: budget } -}) - -export * as AnthropicOptions from "./anthropic-options" diff --git a/packages/ai/src/protocols/utils/gemini-options.ts b/packages/ai/src/protocols/utils/gemini-options.ts deleted file mode 100644 index f4828a00a41b..000000000000 --- a/packages/ai/src/protocols/utils/gemini-options.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Schema } from "effect" -import type { LLMRequest } from "../../schema" -import { ProviderShared } from "../shared" - -export const ThinkingConfigSchema = Schema.Struct({ - thinkingBudget: Schema.optional(Schema.Number), - includeThoughts: Schema.optional(Schema.Boolean), -}) -export type ThinkingConfig = Schema.Schema.Type - -export interface Resolved { - readonly thinkingConfig?: ThinkingConfig -} - -export const resolve = (request: LLMRequest): Resolved => { - const value = request.providerOptions?.gemini?.thinkingConfig - if (!ProviderShared.isRecord(value)) return {} - const thinkingConfig = { - thinkingBudget: typeof value.thinkingBudget === "number" ? value.thinkingBudget : undefined, - includeThoughts: typeof value.includeThoughts === "boolean" ? value.includeThoughts : undefined, - } - return { - thinkingConfig: Object.values(thinkingConfig).some((item) => item !== undefined) ? thinkingConfig : undefined, - } -} - -export * as GeminiOptions from "./gemini-options" diff --git a/packages/ai/src/providers/anthropic-compatible.ts b/packages/ai/src/providers/anthropic-compatible.ts index 50867ebb4404..56cc7cee3229 100644 --- a/packages/ai/src/providers/anthropic-compatible.ts +++ b/packages/ai/src/providers/anthropic-compatible.ts @@ -4,9 +4,10 @@ import { Auth } from "../route/auth" import type { ProviderAuthOption } from "../route/auth-options" import type { RouteDefaultsInput } from "../route/client" import { ProviderID, type ModelID } from "../schema" -import type { AnthropicProviderOptionsInput } from "./anthropic-options" -export type { AnthropicOptionsInput, AnthropicProviderOptionsInput, AnthropicThinkingInput } from "./anthropic-options" +export type AnthropicOptionsInput = AnthropicMessages.OptionsInput +export type AnthropicProviderOptionsInput = AnthropicMessages.ProviderOptionsInput +export type AnthropicThinkingInput = AnthropicMessages.ThinkingInput export const id = ProviderID.make("anthropic-compatible") @@ -14,7 +15,7 @@ export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly provider?: string readonly baseURL: string - readonly providerOptions?: AnthropicProviderOptionsInput + readonly providerOptions?: AnthropicMessages.ProviderOptionsInput } export type Settings = ProviderPackage.Settings & @@ -24,7 +25,7 @@ export type Settings = ProviderPackage.Settings & ) & { readonly baseURL: string readonly provider?: string - readonly providerOptions?: AnthropicProviderOptionsInput + readonly providerOptions?: AnthropicMessages.ProviderOptionsInput } export const routes = [AnthropicMessages.route] diff --git a/packages/ai/src/providers/anthropic-options.ts b/packages/ai/src/providers/anthropic-options.ts deleted file mode 100644 index 61f81cc07466..000000000000 --- a/packages/ai/src/providers/anthropic-options.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ProviderOptions } from "../schema" - -export type AnthropicThinkingInput = - | { - readonly type: "adaptive" - readonly display?: "summarized" | "omitted" - } - | { - readonly type: "disabled" - } - | ({ readonly type: "enabled" } & ( - | { readonly budgetTokens: number; readonly budget_tokens?: number } - | { readonly budgetTokens?: number; readonly budget_tokens: number } - )) - -export interface AnthropicOptionsInput { - readonly [key: string]: unknown - readonly thinking?: AnthropicThinkingInput - readonly effort?: string -} - -export type AnthropicProviderOptionsInput = ProviderOptions & { - readonly anthropic?: AnthropicOptionsInput -} - -export * as AnthropicProviderOptions from "./anthropic-options" diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 5eedb63ac300..eb175ec50c69 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -5,9 +5,10 @@ import type { ProviderPackage } from "../provider-package" import { ProviderID, type ModelID } from "../schema" import { AnthropicMessages } from "../protocols/anthropic-messages" import { AnthropicCompatible } from "./anthropic-compatible" -import type { AnthropicProviderOptionsInput } from "./anthropic-options" -export type { AnthropicOptionsInput, AnthropicProviderOptionsInput, AnthropicThinkingInput } from "./anthropic-options" +export type AnthropicOptionsInput = AnthropicMessages.OptionsInput +export type AnthropicProviderOptionsInput = AnthropicMessages.ProviderOptionsInput +export type AnthropicThinkingInput = AnthropicMessages.ThinkingInput export const id = ProviderID.make("anthropic") @@ -16,7 +17,7 @@ export const routes = [AnthropicMessages.route] export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string - readonly providerOptions?: AnthropicProviderOptionsInput + readonly providerOptions?: AnthropicMessages.ProviderOptionsInput } export type Settings = ProviderPackage.Settings & @@ -25,7 +26,7 @@ export type Settings = ProviderPackage.Settings & | { readonly apiKey?: never; readonly authToken?: string } ) & { readonly baseURL?: string - readonly providerOptions?: AnthropicProviderOptionsInput + readonly providerOptions?: AnthropicMessages.ProviderOptionsInput } const auth = (options: ProviderAuthOption<"optional">) => { diff --git a/packages/ai/src/providers/gemini-options.ts b/packages/ai/src/providers/gemini-options.ts deleted file mode 100644 index 067c38e144a0..000000000000 --- a/packages/ai/src/providers/gemini-options.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { ThinkingConfig } from "../protocols/utils/gemini-options" -import type { ProviderOptions } from "../schema" - -export interface GeminiOptionsInput { - readonly [key: string]: unknown - readonly thinkingConfig?: ThinkingConfig -} - -export type GeminiProviderOptionsInput = ProviderOptions & { - readonly gemini?: GeminiOptionsInput -} - -export * as GeminiProviderOptions from "./gemini-options" diff --git a/packages/ai/src/providers/google-vertex-messages.ts b/packages/ai/src/providers/google-vertex-messages.ts index b52eb94f9317..53c4e16b0d02 100644 --- a/packages/ai/src/providers/google-vertex-messages.ts +++ b/packages/ai/src/providers/google-vertex-messages.ts @@ -7,10 +7,11 @@ import { Endpoint } from "../route/endpoint" import { Framing } from "../route/framing" import { Protocol } from "../route/protocol" import { ProviderID, type ModelID } from "../schema" -import type { AnthropicProviderOptionsInput } from "./anthropic-options" import { GoogleVertexShared } from "./google-vertex-shared" -export type { AnthropicOptionsInput, AnthropicProviderOptionsInput, AnthropicThinkingInput } from "./anthropic-options" +export type AnthropicOptionsInput = AnthropicMessages.OptionsInput +export type AnthropicProviderOptionsInput = AnthropicMessages.ProviderOptionsInput +export type AnthropicThinkingInput = AnthropicMessages.ThinkingInput const VERSION = "vertex-2023-10-16" as const @@ -22,7 +23,7 @@ export type Config = RouteDefaultsInput & readonly baseURL?: string readonly location?: string readonly project?: string - readonly providerOptions?: AnthropicProviderOptionsInput + readonly providerOptions?: AnthropicMessages.ProviderOptionsInput } export interface Settings extends ProviderPackage.Settings { @@ -31,7 +32,7 @@ export interface Settings extends ProviderPackage.Settings { readonly baseURL?: string readonly location?: string readonly project?: string - readonly providerOptions?: AnthropicProviderOptionsInput + readonly providerOptions?: AnthropicMessages.ProviderOptionsInput } const route = Route.make({ diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts index f7994fb16d6f..78f4e0764da6 100644 --- a/packages/ai/src/providers/google-vertex.ts +++ b/packages/ai/src/providers/google-vertex.ts @@ -5,10 +5,10 @@ import { Route, type RouteDefaultsInput } from "../route/client" import { Endpoint } from "../route/endpoint" import { Framing } from "../route/framing" import { ProviderID, type ModelID } from "../schema" -import type { GeminiProviderOptionsInput } from "./gemini-options" import { GoogleVertexShared } from "./google-vertex-shared" -export type { GeminiOptionsInput, GeminiProviderOptionsInput } from "./gemini-options" +export type GeminiOptionsInput = Gemini.OptionsInput +export type GeminiProviderOptionsInput = Gemini.ProviderOptionsInput export const id = ProviderID.make("google-vertex") @@ -17,7 +17,7 @@ export type Config = RouteDefaultsInput & readonly baseURL?: string readonly location?: string readonly project?: string - readonly providerOptions?: GeminiProviderOptionsInput + readonly providerOptions?: Gemini.ProviderOptionsInput } export type Settings = ProviderPackage.Settings & @@ -28,7 +28,7 @@ export type Settings = ProviderPackage.Settings & readonly baseURL?: string readonly location?: string readonly project?: string - readonly providerOptions?: GeminiProviderOptionsInput + readonly providerOptions?: Gemini.ProviderOptionsInput } const route = Route.make({ diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts index 0199d594e631..2c3476117c71 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -5,10 +5,10 @@ import type { ProviderPackage } from "../provider-package" import { HttpOptions, ProviderID, mergeHttpOptions, type ModelID } from "../schema" import { Gemini } from "../protocols/gemini" import { GoogleImages } from "../protocols/google-images" -import type { GeminiProviderOptionsInput } from "./gemini-options" export type { GoogleImageOptions } from "../protocols/google-images" -export type { GeminiOptionsInput, GeminiProviderOptionsInput } from "./gemini-options" +export type GeminiOptionsInput = Gemini.OptionsInput +export type GeminiProviderOptionsInput = Gemini.ProviderOptionsInput export const id = ProviderID.make("google") @@ -17,13 +17,13 @@ export const routes = [Gemini.route] export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string - readonly providerOptions?: GeminiProviderOptionsInput + readonly providerOptions?: Gemini.ProviderOptionsInput } export interface Settings extends ProviderPackage.Settings { readonly apiKey?: string readonly baseURL?: string - readonly providerOptions?: GeminiProviderOptionsInput + readonly providerOptions?: Gemini.ProviderOptionsInput } const auth = (options: ProviderAuthOption<"optional">) => { From 35d31d8ec15e3f479f25968a73e79ced003a2d06 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Fri, 24 Jul 2026 19:49:07 +0530 Subject: [PATCH 080/150] refactor(ai): remove dead LLM exports (#38700) --- packages/ai/AGENTS.md | 2 +- packages/ai/DESIGN.md | 5 +- packages/ai/example/tutorial.ts | 4 +- packages/ai/src/llm.ts | 22 +------- packages/ai/test/adapter.test.ts | 6 +-- packages/ai/test/llm.test.ts | 15 ++++-- .../test/provider/anthropic-messages.test.ts | 34 ++++++------ .../ai/test/provider/bedrock-converse.test.ts | 44 ++++++++------- packages/ai/test/provider/gemini.test.ts | 20 +++---- packages/ai/test/provider/openai-chat.test.ts | 53 ++++++++++-------- .../provider/openai-compatible-chat.test.ts | 8 +-- .../ai/test/provider/openai-responses.test.ts | 54 ++++++++++--------- packages/ai/test/recorded-scenarios.ts | 12 ++--- packages/ai/test/tool-runtime.test.ts | 4 +- 14 files changed, 150 insertions(+), 133 deletions(-) diff --git a/packages/ai/AGENTS.md b/packages/ai/AGENTS.md index f02b5616b220..6c35cafc344c 100644 --- a/packages/ai/AGENTS.md +++ b/packages/ai/AGENTS.md @@ -241,7 +241,7 @@ const get_weather = tool({ const tools = { get_weather, get_time, ... } const events = yield* LLM.stream( - LLM.updateRequest(request, { tools: Tool.toDefinitions(tools) }), + LLMRequest.update(request, { tools: Tool.toDefinitions(tools) }), ).pipe(Stream.runCollect) const call = Array.from(events).find(LLMEvent.is.toolCall) diff --git a/packages/ai/DESIGN.md b/packages/ai/DESIGN.md index 2e73360300de..5629c13408da 100644 --- a/packages/ai/DESIGN.md +++ b/packages/ai/DESIGN.md @@ -315,7 +315,8 @@ const longer = { } ``` -There is no `LLM.updateRequest(...)` helper and no request Schema class. +There is no `LLM.updateRequest(...)` helper. The current Schema-backed implementation +uses `LLMRequest.update(...)` when canonical request data must be derived. ### Conversation history @@ -436,7 +437,7 @@ const call = Array.from(events).find(LLMEvent.is.toolCall) if (call && !call.providerExecuted) { const dispatched = yield * ToolRuntime.dispatch(tools, call) - const followUp = LLM.updateRequest(request, { + const followUp = LLMRequest.update(request, { messages: [...request.messages, Message.assistant([call]), Message.tool({ ...call, result: dispatched.result })], }) // Caller must invoke the provider again and repeat the loop. diff --git a/packages/ai/example/tutorial.ts b/packages/ai/example/tutorial.ts index 3fc0603b7349..7ee4abb1463b 100644 --- a/packages/ai/example/tutorial.ts +++ b/packages/ai/example/tutorial.ts @@ -1,5 +1,5 @@ import { Config, Effect, Formatter, Layer, Schema, Stream } from "effect" -import { LLM, LLMClient, Message, ProviderID, Tool, ToolRuntime } from "@opencode-ai/ai" +import { LLM, LLMClient, LLMRequest, Message, ProviderID, Tool, ToolRuntime } from "@opencode-ai/ai" import { Route, Auth, Endpoint, Framing, Protocol, RequestExecutor, WebSocketExecutor } from "@opencode-ai/ai/route" import { OpenAI } from "@opencode-ai/ai/providers" @@ -116,7 +116,7 @@ const streamWithTools = Effect.gen(function* () { // A durable agent would persist these messages before starting another // raw model turn. This tutorial keeps the boundary visible instead. - const followUp = LLM.updateRequest(request, { + const followUp = LLMRequest.update(request, { messages: [ ...request.messages, Message.assistant([event]), diff --git a/packages/ai/src/llm.ts b/packages/ai/src/llm.ts index ecdf30ae47d8..8b6f904a5b49 100644 --- a/packages/ai/src/llm.ts +++ b/packages/ai/src/llm.ts @@ -9,24 +9,13 @@ import { LLMRequest, LLMResponse, Message, - type ModelInput as SchemaModelInput, SystemPart, ToolChoice, ToolDefinition, type ContentPart, - ToolResultPart, } from "./schema" import { make as makeTool, toDefinitions, type ToolSchema } from "./tool" -export type ModelInput = SchemaModelInput - -export type MessageInput = Message.Input - -export type ToolChoiceInput = ToolChoice.Input -export type ToolChoiceMode = ToolChoice.Mode - -export type ToolResultInput = Parameters[0] - /** Input accepted by `LLM.request`, normalized into the canonical `LLMRequest` class. */ export type RequestInput = Omit< ConstructorParameters[0], @@ -34,9 +23,9 @@ export type RequestInput = Omit< > & { readonly system?: string | SystemPart | ReadonlyArray readonly prompt?: string | ContentPart | ReadonlyArray - readonly messages?: ReadonlyArray + readonly messages?: ReadonlyArray readonly tools?: ReadonlyArray - readonly toolChoice?: ToolChoiceInput + readonly toolChoice?: ToolChoice.Input readonly generation?: GenerationOptions.Input readonly providerOptions?: ConstructorParameters[0]["providerOptions"] readonly http?: HttpOptions.Input @@ -46,10 +35,6 @@ export const generate = LLMClient.generate export const stream = LLMClient.stream -export const requestInput = (input: LLMRequest): RequestInput => ({ - ...LLMRequest.input(input), -}) - export const request = (input: RequestInput) => { const { system: requestSystem, @@ -74,9 +59,6 @@ export const request = (input: RequestInput) => { }) } -export const updateRequest = (input: LLMRequest, patch: Partial) => - request({ ...requestInput(input), ...patch }) - const GENERATE_OBJECT_TOOL_NAME = "generate_object" const GENERATE_OBJECT_TOOL_DESCRIPTION = "Return the structured result by calling this tool." diff --git a/packages/ai/test/adapter.test.ts b/packages/ai/test/adapter.test.ts index 346013ced6dd..b2b180a2b226 100644 --- a/packages/ai/test/adapter.test.ts +++ b/packages/ai/test/adapter.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect, Schema, Stream } from "effect" -import { LLM, LLMResponse } from "../src" +import { LLM, LLMRequest, LLMResponse } from "../src" import { Route, Endpoint, LLMClient, Protocol, type FramingDef } from "../src/route" import { Model } from "../src/schema" import { testEffect } from "./lib/effect" @@ -141,7 +141,7 @@ describe("llm route", () => { Effect.gen(function* () { const llm = yield* LLMClient.Service const prepared = yield* llm.prepare( - LLM.updateRequest(request, { model: updateModel(request.model, { route: configuredGemini }) }), + LLMRequest.update(request, { model: updateModel(request.model, { route: configuredGemini }) }), ) expect(prepared.route).toBe("gemini-fake") @@ -174,7 +174,7 @@ describe("llm route", () => { }) const prepared = yield* (yield* LLMClient.Service).prepare( - LLM.updateRequest(request, { model: updateModel(request.model, { route: duplicate }) }), + LLMRequest.update(request, { model: updateModel(request.model, { route: duplicate }) }), ) expect(prepared.body).toEqual({ body: "late-default" }) diff --git a/packages/ai/test/llm.test.ts b/packages/ai/test/llm.test.ts index ca8829358564..e5588cf95d6a 100644 --- a/packages/ai/test/llm.test.ts +++ b/packages/ai/test/llm.test.ts @@ -2,7 +2,16 @@ import { describe, expect, test } from "bun:test" import { CacheHint, LLM, LLMResponse } from "../src" import * as OpenAIChat from "../src/protocols/openai-chat" import * as OpenAIResponses from "../src/protocols/openai-responses" -import { LLMRequest, Message, Model, ToolCallPart, ToolChoice, ToolDefinition, ToolResultPart } from "../src/schema" +import { + GenerationOptions, + LLMRequest, + Message, + Model, + ToolCallPart, + ToolChoice, + ToolDefinition, + ToolResultPart, +} from "../src/schema" const chatRoute = OpenAIChat.route const responsesRoute = OpenAIResponses.route @@ -31,8 +40,8 @@ describe("llm constructors", () => { model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }), prompt: "Say hello.", }) - const updated = LLM.updateRequest(base, { - generation: { maxTokens: 20 }, + const updated = LLMRequest.update(base, { + generation: GenerationOptions.make({ maxTokens: 20 }), messages: [...base.messages, Message.assistant("Hi.")], }) diff --git a/packages/ai/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts index 564cf4990012..acdab6a7b0c2 100644 --- a/packages/ai/test/provider/anthropic-messages.test.ts +++ b/packages/ai/test/provider/anthropic-messages.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { HttpClientRequest } from "effect/unstable/http" -import { CacheHint, LLM, LLMError, Message, ToolCallPart, Usage } from "../../src" +import { CacheHint, LLM, LLMError, LLMRequest, Message, ToolCallPart, ToolDefinition, Usage } from "../../src" import { Auth, LLMClient } from "../../src/route" import * as AnthropicMessages from "../../src/protocols/anthropic-messages" import { continuationRequest, nativeAnthropicMessagesContinuation } from "../continuation-scenarios" @@ -60,7 +60,7 @@ describe("Anthropic Messages route", () => { it.effect("lowers adaptive thinking settings with effort", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( - LLM.updateRequest(request, { + LLMRequest.update(request, { providerOptions: { anthropic: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" }, }, @@ -77,17 +77,17 @@ describe("Anthropic Messages route", () => { it.effect("normalizes enabled and disabled thinking settings", () => Effect.gen(function* () { const enabled = yield* LLMClient.prepare( - LLM.updateRequest(request, { + LLMRequest.update(request, { providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 1_024 } } }, }), ) const legacy = yield* LLMClient.prepare( - LLM.updateRequest(request, { + LLMRequest.update(request, { providerOptions: { anthropic: { thinking: { type: "enabled", budget_tokens: 2_048 } } }, }), ) const disabled = yield* LLMClient.prepare( - LLM.updateRequest(request, { + LLMRequest.update(request, { providerOptions: { anthropic: { thinking: { type: "disabled" } } }, }), ) @@ -101,7 +101,7 @@ describe("Anthropic Messages route", () => { it.effect("rejects enabled thinking without a budget", () => Effect.gen(function* () { const error = yield* LLMClient.prepare( - LLM.updateRequest(request, { + LLMRequest.update(request, { providerOptions: { anthropic: { thinking: { type: "enabled" } } }, }), ).pipe(Effect.flip) @@ -548,8 +548,8 @@ describe("Anthropic Messages route", () => { // contents are provider-owned and must be replayed without inspection. const redactedData = "cmVkYWN0ZWQtdGhpbmtpbmc=" const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe( Effect.provide( @@ -587,7 +587,7 @@ describe("Anthropic Messages route", () => { response.message, Message.tool({ id: "call_1", name: "lookup", result: "sunny", resultType: "text" }), ], - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], cache: "none", }), ) @@ -666,8 +666,8 @@ describe("Anthropic Messages route", () => { { type: "message_delta", delta: { stop_reason: "tool_use" }, usage: { output_tokens: 1 } }, ) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedResponse(body))) const usage = new Usage({ @@ -851,8 +851,10 @@ describe("Anthropic Messages route", () => { { type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 8 } }, ) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "web_search", description: "Web search", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ + ToolDefinition.make({ name: "web_search", description: "Web search", inputSchema: { type: "object" } }), + ], }), ).pipe(Effect.provide(fixedResponse(body))) @@ -912,8 +914,10 @@ describe("Anthropic Messages route", () => { { type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } }, ) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "web_search", description: "Web search", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ + ToolDefinition.make({ name: "web_search", description: "Web search", inputSchema: { type: "object" } }), + ], }), ).pipe(Effect.provide(fixedResponse(body))) diff --git a/packages/ai/test/provider/bedrock-converse.test.ts b/packages/ai/test/provider/bedrock-converse.test.ts index 2aa75057f63b..05c0c669fc1f 100644 --- a/packages/ai/test/provider/bedrock-converse.test.ts +++ b/packages/ai/test/provider/bedrock-converse.test.ts @@ -2,7 +2,16 @@ import { EventStreamCodec } from "@smithy/eventstream-codec" import { fromUtf8, toUtf8 } from "@smithy/util-utf8" import { describe, expect } from "bun:test" import { Effect } from "effect" -import { CacheHint, LLM, Message, ToolCallPart, ToolChoice } from "../../src" +import { + CacheHint, + GenerationOptions, + LLM, + LLMRequest, + Message, + ToolCallPart, + ToolChoice, + ToolDefinition, +} from "../../src" import { LLMClient } from "../../src/route" import { AmazonBedrock } from "../../src/providers" import * as BedrockConverse from "../../src/protocols/bedrock-converse" @@ -86,7 +95,9 @@ describe("Bedrock Converse route", () => { it.effect("passes topK through additionalModelRequestFields as top_k", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( - LLM.updateRequest(baseRequest, { generation: { maxTokens: 64, temperature: 0, topK: 40 } }), + LLMRequest.update(baseRequest, { + generation: GenerationOptions.make({ maxTokens: 64, temperature: 0, topK: 40 }), + }), ) // Converse's inferenceConfig has no topK; Anthropic/Nova read it from @@ -123,13 +134,13 @@ describe("Bedrock Converse route", () => { it.effect("prepares tool config with toolSpec and toolChoice", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( - LLM.updateRequest(baseRequest, { + LLMRequest.update(baseRequest, { tools: [ - { + ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }, - }, + }), ], toolChoice: ToolChoice.make({ type: "required" }), }), @@ -157,13 +168,13 @@ describe("Bedrock Converse route", () => { it.effect("keeps tools and omits the unsupported choice when tool choice is none", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( - LLM.updateRequest(baseRequest, { + LLMRequest.update(baseRequest, { tools: [ - { + ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object", properties: { query: { type: "string" } } }, - }, + }), ], toolChoice: ToolChoice.make({ type: "none" }), }), @@ -369,8 +380,8 @@ describe("Bedrock Converse route", () => { ["messageStop", { stopReason: "tool_use" }], ) const response = yield* LLMClient.generate( - LLM.updateRequest(baseRequest, { - tools: [{ name: "lookup", description: "Lookup", inputSchema: { type: "object" } }], + LLMRequest.update(baseRequest, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedBytes(body))) @@ -473,8 +484,8 @@ describe("Bedrock Converse route", () => { // wire. The provider owns the payload and requires byte-exact replay. const redactedData = "cmVkYWN0ZWQtdGhpbmtpbmc=" const response = yield* LLMClient.generate( - LLM.updateRequest(baseRequest, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(baseRequest, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe( Effect.provide( @@ -493,10 +504,7 @@ describe("Bedrock Converse route", () => { start: { toolUse: { toolUseId: "tool_1", name: "lookup" } }, }, ], - [ - "contentBlockDelta", - { contentBlockIndex: 1, delta: { toolUse: { input: '{"query":"weather"}' } } }, - ], + ["contentBlockDelta", { contentBlockIndex: 1, delta: { toolUse: { input: '{"query":"weather"}' } } }], ["contentBlockStop", { contentBlockIndex: 1 }], ["messageStop", { stopReason: "tool_use" }], ), @@ -567,7 +575,7 @@ describe("Bedrock Converse route", () => { const unsignedModel = AmazonBedrock.configure({ baseURL: "https://bedrock-runtime.test", }).model("anthropic.claude-3-5-sonnet-20240620-v1:0") - const error = yield* LLMClient.generate(LLM.updateRequest(baseRequest, { model: unsignedModel })).pipe( + const error = yield* LLMClient.generate(LLMRequest.update(baseRequest, { model: unsignedModel })).pipe( Effect.provide(fixedBytes(eventStreamBody(["messageStop", { stopReason: "end_turn" }]))), Effect.flip, ) @@ -586,7 +594,7 @@ describe("Bedrock Converse route", () => { secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", }, }).model("anthropic.claude-3-5-sonnet-20240620-v1:0") - const prepared = yield* LLMClient.prepare(LLM.updateRequest(baseRequest, { model: signed })) + const prepared = yield* LLMClient.prepare(LLMRequest.update(baseRequest, { model: signed })) expect(prepared.route).toBe("bedrock-converse") expect(prepared.model).toBe(signed) diff --git a/packages/ai/test/provider/gemini.test.ts b/packages/ai/test/provider/gemini.test.ts index 7549f36996c0..2b22401e0a54 100644 --- a/packages/ai/test/provider/gemini.test.ts +++ b/packages/ai/test/provider/gemini.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" -import { LLM, LLMError, Message, ToolCallPart, Usage } from "../../src" +import { LLM, LLMError, LLMRequest, Message, ToolCallPart, ToolDefinition, Usage } from "../../src" import { Auth, LLMClient } from "../../src/route" import * as Gemini from "../../src/protocols/gemini" import { ProviderShared } from "../../src/protocols/shared" @@ -39,12 +39,12 @@ describe("Gemini route", () => { it.effect("normalizes Gemini thinking options", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( - LLM.updateRequest(request, { + LLMRequest.update(request, { providerOptions: { gemini: { thinkingConfig: { thinkingBudget: 0, includeThoughts: false } } }, }), ) const filtered = yield* LLMClient.prepare( - LLM.updateRequest(request, { + LLMRequest.update(request, { providerOptions: { gemini: { thinkingConfig: { thinkingBudget: "invalid", includeThoughts: false } } }, }), ) @@ -261,7 +261,7 @@ describe("Gemini route", () => { id: "req_tool_choice_none", model, prompt: "Say hello.", - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], toolChoice: { type: "none" }, }), ) @@ -431,8 +431,8 @@ describe("Gemini route", () => { ], }) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedResponse(body))) const reasoning = response.events.find((event) => event.type === "reasoning-start") @@ -522,8 +522,8 @@ describe("Gemini route", () => { usageMetadata: { promptTokenCount: 5, candidatesTokenCount: 1 }, }) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedResponse(body))) const usage = new Usage({ @@ -589,8 +589,8 @@ describe("Gemini route", () => { ], }) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedResponse(body))) diff --git a/packages/ai/test/provider/openai-chat.test.ts b/packages/ai/test/provider/openai-chat.test.ts index fb926c030806..926ea30dba02 100644 --- a/packages/ai/test/provider/openai-chat.test.ts +++ b/packages/ai/test/provider/openai-chat.test.ts @@ -1,7 +1,18 @@ import { describe, expect } from "bun:test" import { Effect, Schema, Stream } from "effect" import { HttpClientRequest } from "effect/unstable/http" -import { LLM, LLMError, LLMEvent, Message, Model, ToolCallPart, Usage } from "../../src" +import { + HttpOptions, + LLM, + LLMError, + LLMEvent, + LLMRequest, + Message, + Model, + ToolCallPart, + ToolDefinition, + Usage, +} from "../../src" import * as Azure from "../../src/providers/azure" import * as OpenAI from "../../src/providers/openai" import * as OpenAIChat from "../../src/protocols/openai-chat" @@ -162,7 +173,7 @@ describe("OpenAI Chat route", () => { it.effect("adds native query params to the Chat Completions URL", () => LLMClient.generate( - LLM.updateRequest(request, { + LLMRequest.update(request, { model: Model.update(model, { route: model.route.with({ endpoint: { query: { "api-version": "v1" } } }) }), }), ).pipe( @@ -182,7 +193,7 @@ describe("OpenAI Chat route", () => { it.effect("uses Azure api-key header for static OpenAI Chat keys", () => LLMClient.generate( - LLM.updateRequest(request, { + LLMRequest.update(request, { model: Azure.configure({ baseURL: "https://opencode-test.openai.azure.com/openai/v1/", apiKey: "azure-key", @@ -208,15 +219,15 @@ describe("OpenAI Chat route", () => { it.effect("applies serializable HTTP overlays after payload lowering", () => LLMClient.generate( - LLM.updateRequest(request, { + LLMRequest.update(request, { model: model.route .with({ auth: Auth.bearer("fresh-key"), headers: { authorization: "Bearer stale" } }) .model({ id: model.id }), - http: { + http: HttpOptions.make({ body: { metadata: { source: "test" } }, headers: { authorization: "Bearer request", "x-custom": "yes" }, query: { debug: "1" }, - }, + }), }), ).pipe( Effect.provide( @@ -618,7 +629,7 @@ describe("OpenAI Chat route", () => { it.effect("parses and replays a configured custom reasoning field", () => Effect.gen(function* () { const custom = Model.update(model, { compatibility: { reasoningField: "vendor_reasoning" } }) - const response = yield* LLMClient.generate(LLM.updateRequest(request, { model: custom })).pipe( + const response = yield* LLMClient.generate(LLMRequest.update(request, { model: custom })).pipe( Effect.provide( fixedResponse( sseEvents( @@ -638,9 +649,7 @@ describe("OpenAI Chat route", () => { const replay = yield* LLMClient.prepare( LLM.request({ model: custom, messages: [response.message] }), ) - expect(replay.body.messages).toEqual([ - { role: "assistant", content: "Hello", vendor_reasoning: "thinking" }, - ]) + expect(replay.body.messages).toEqual([{ role: "assistant", content: "Hello", vendor_reasoning: "thinking" }]) }), ) @@ -651,8 +660,8 @@ describe("OpenAI Chat route", () => { { type: "reasoning.encrypted", data: "opaque", format: "anthropic-claude-v1", index: 1 }, ] const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe( Effect.provide( @@ -1024,8 +1033,8 @@ describe("OpenAI Chat route", () => { deltaChunk({}, "tool_calls"), ) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedResponse(body))) @@ -1067,8 +1076,8 @@ describe("OpenAI Chat route", () => { deltaChunk({}, "tool_calls"), ) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedResponse(body))) @@ -1089,8 +1098,8 @@ describe("OpenAI Chat route", () => { deltaChunk({}, "tool_calls"), ) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedResponse(body))) @@ -1107,8 +1116,8 @@ describe("OpenAI Chat route", () => { deltaChunk({}, "tool_calls"), ) const error = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedResponse(body)), Effect.flip) @@ -1125,8 +1134,8 @@ describe("OpenAI Chat route", () => { }), deltaChunk({ tool_calls: [{ index: 0, function: { arguments: ':"weather"}' } }] }), ) - const input = LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + const input = LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }) const events: LLMEvent[] = [] const streamError = yield* LLMClient.stream(input).pipe( diff --git a/packages/ai/test/provider/openai-compatible-chat.test.ts b/packages/ai/test/provider/openai-compatible-chat.test.ts index f32b2bc2d9a7..6565820af899 100644 --- a/packages/ai/test/provider/openai-compatible-chat.test.ts +++ b/packages/ai/test/provider/openai-compatible-chat.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import { Effect, Schema } from "effect" import { HttpClientRequest } from "effect/unstable/http" -import { LLM, Message, ToolCallPart } from "../../src" +import { LLM, LLMRequest, Message, ToolCallPart, ToolChoice, ToolDefinition } from "../../src" import { Auth, LLMClient } from "../../src/route" import * as OpenAICompatible from "../../src/providers/openai-compatible" import * as OpenAICompatibleChat from "../../src/protocols/openai-compatible-chat" @@ -53,9 +53,9 @@ describe("OpenAI-compatible Chat route", () => { it.effect("prepares generic Chat target", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], - toolChoice: { type: "required" }, + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], + toolChoice: ToolChoice.make({ type: "required" }), }), ) diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index 72a2383b041a..cf66e5cbd587 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -1,7 +1,18 @@ import { describe, expect } from "bun:test" import { ConfigProvider, Effect, Layer, Stream } from "effect" import { Headers, HttpClientRequest } from "effect/unstable/http" -import { LLM, LLMError, LLMEvent, Message, Model, ToolCallPart, ToolResultPart, Usage } from "../../src" +import { + LLM, + LLMError, + LLMEvent, + LLMRequest, + Message, + Model, + ToolCallPart, + ToolDefinition, + ToolResultPart, + Usage, +} from "../../src" import { Auth, LLMClient, RequestExecutor, WebSocketExecutor } from "../../src/route" import * as Azure from "../../src/providers/azure" import * as OpenAI from "../../src/providers/openai" @@ -96,7 +107,7 @@ describe("OpenAI Responses route", () => { it.effect("lowers semantic service tier options", () => Effect.gen(function* () { - const input = LLM.updateRequest(request, { providerOptions: { openai: { serviceTier: "priority" } } }) + const input = LLMRequest.update(request, { providerOptions: { openai: { serviceTier: "priority" } } }) expect(input.providerOptions).toEqual({ openai: { serviceTier: "priority" } }) const prepared = yield* LLMClient.prepare(input) @@ -108,7 +119,7 @@ describe("OpenAI Responses route", () => { it.effect("passes through custom OpenAI reasoning effort strings", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( - LLM.updateRequest(request, { providerOptions: { openai: { reasoningEffort: "experimental" } } }), + LLMRequest.update(request, { providerOptions: { openai: { reasoningEffort: "experimental" } } }), ) expect(prepared.body.reasoning).toEqual({ effort: "experimental" }) @@ -118,7 +129,7 @@ describe("OpenAI Responses route", () => { it.effect("omits unsupported semantic service tiers", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( - LLM.updateRequest(request, { providerOptions: { openai: { serviceTier: "unsupported" } } }), + LLMRequest.update(request, { providerOptions: { openai: { serviceTier: "unsupported" } } }), ) expect(prepared.body).not.toHaveProperty("service_tier") @@ -128,9 +139,9 @@ describe("OpenAI Responses route", () => { it.effect("flattens top-level object unions in function schemas", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( - LLM.updateRequest(request, { + LLMRequest.update(request, { tools: [ - { + ToolDefinition.make({ name: "read", description: "Read a path or resource.", inputSchema: { @@ -152,7 +163,7 @@ describe("OpenAI Responses route", () => { }, ], }, - }, + }), ], }), ) @@ -207,7 +218,7 @@ describe("OpenAI Responses route", () => { it.effect("prepares OpenAI Responses WebSocket target", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( - LLM.updateRequest(request, { + LLMRequest.update(request, { model: OpenAIResponses.webSocketRoute .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) .model({ id: "gpt-4.1-mini" }), @@ -291,7 +302,7 @@ describe("OpenAI Responses route", () => { it.effect("adds native query params to the Responses URL", () => Effect.gen(function* () { yield* LLMClient.generate( - LLM.updateRequest(request, { + LLMRequest.update(request, { model: Model.update(model, { route: model.route.with({ endpoint: { query: { "api-version": "v1" } } }) }), }), ).pipe( @@ -313,7 +324,7 @@ describe("OpenAI Responses route", () => { it.effect("uses Azure api-key header for static OpenAI Responses keys", () => Effect.gen(function* () { yield* LLMClient.generate( - LLM.updateRequest(request, { + LLMRequest.update(request, { model: Azure.configure({ baseURL: "https://opencode-test.openai.azure.com/openai/v1/", apiKey: "azure-key", @@ -340,7 +351,7 @@ describe("OpenAI Responses route", () => { it.effect("loads OpenAI default auth from Effect Config", () => LLMClient.generate( - LLM.updateRequest(request, { + LLMRequest.update(request, { model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/" }).responses("gpt-4.1-mini"), }), ).pipe( @@ -361,7 +372,7 @@ describe("OpenAI Responses route", () => { it.effect("lets explicit auth override OpenAI default API key auth", () => LLMClient.generate( - LLM.updateRequest(request, { + LLMRequest.update(request, { model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", auth: Auth.bearer("oauth-token"), @@ -889,12 +900,7 @@ describe("OpenAI Responses route", () => { const unknown = yield* generate({}) const custom = yield* generate({ reason: "provider_limit" }) - expect([ - length.finishReason, - contentFilter.finishReason, - unknown.finishReason, - custom.finishReason, - ]).toEqual([ + expect([length.finishReason, contentFilter.finishReason, unknown.finishReason, custom.finishReason]).toEqual([ { normalized: "length", raw: "max_output_tokens" }, { normalized: "content-filter", raw: "content_filter" }, { normalized: "unknown", raw: undefined }, @@ -999,7 +1005,7 @@ describe("OpenAI Responses route", () => { it.effect("streams each reasoning summary part as a separate block", () => Effect.gen(function* () { const response = yield* LLMClient.generate( - LLM.updateRequest(request, { providerOptions: { openai: { store: false } } }), + LLMRequest.update(request, { providerOptions: { openai: { store: false } } }), ).pipe( Effect.provide( fixedResponse( @@ -1054,7 +1060,7 @@ describe("OpenAI Responses route", () => { it.effect("closes reasoning summary parts when storage is not disabled", () => Effect.gen(function* () { const response = yield* LLMClient.generate( - LLM.updateRequest(request, { providerOptions: { openai: { store: true } } }), + LLMRequest.update(request, { providerOptions: { openai: { store: true } } }), ).pipe( Effect.provide( fixedResponse( @@ -1381,8 +1387,8 @@ describe("OpenAI Responses route", () => { { type: "response.completed", response: { usage: { input_tokens: 5, output_tokens: 1 } } }, ) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedResponse(body))) const usage = new Usage({ @@ -1467,8 +1473,8 @@ describe("OpenAI Responses route", () => { { type: "response.completed", response: { usage: { input_tokens: 5, output_tokens: 1 } } }, ) const response = yield* LLMClient.generate( - LLM.updateRequest(request, { - tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + LLMRequest.update(request, { + tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], }), ).pipe(Effect.provide(fixedResponse(body))) diff --git a/packages/ai/test/recorded-scenarios.ts b/packages/ai/test/recorded-scenarios.ts index cd762dc4fd9b..d0f043cca74c 100644 --- a/packages/ai/test/recorded-scenarios.ts +++ b/packages/ai/test/recorded-scenarios.ts @@ -3,6 +3,7 @@ import { Effect, Schema } from "effect" import { LLM, LLMEvent, + LLMRequest, LLMResponse, Message, ToolRuntime, @@ -11,7 +12,6 @@ import { toDefinitions, type ContentPart, type FinishReason, - type LLMRequest, type Model, } from "../src" import { LLMClient } from "../src/route" @@ -91,7 +91,7 @@ const restroomImage = () => export const runWeatherToolLoop = (request: LLMRequest) => Effect.gen(function* () { const tools = { [weatherToolName]: weatherRuntimeTool } - let next = LLM.updateRequest(request, { tools: toDefinitions(tools) }) + let next = LLMRequest.update(request, { tools: toDefinitions(tools) }) const events: LLMEvent[] = [] for (let step = 0; step < 10; step++) { @@ -108,7 +108,7 @@ export const runWeatherToolLoop = (request: LLMRequest) => ToolRuntime.dispatch(tools, call).pipe(Effect.map((result) => [call, result] as const)), ) events.push(...dispatched.flatMap(([, result]) => result.events)) - next = LLM.updateRequest(next, { + next = LLMRequest.update(next, { messages: [ ...next.messages, Message.assistant(assistantContent(response.events)), @@ -123,10 +123,8 @@ export const runWeatherToolLoop = (request: LLMRequest) => const assistantContent = (events: ReadonlyArray) => events.reduce(LLMResponse.reduce, LLMResponse.empty()).message.content -export const expectFinish = ( - events: ReadonlyArray, - reason: FinishReason, -) => expect(events.at(-1)).toMatchObject({ type: "finish", reason: { normalized: reason } }) +export const expectFinish = (events: ReadonlyArray, reason: FinishReason) => + expect(events.at(-1)).toMatchObject({ type: "finish", reason: { normalized: reason } }) export const expectWeatherToolCall = (response: LLMResponse) => expect(response.toolCalls).toMatchObject([ diff --git a/packages/ai/test/tool-runtime.test.ts b/packages/ai/test/tool-runtime.test.ts index e6e97887193a..aa4c85f6efee 100644 --- a/packages/ai/test/tool-runtime.test.ts +++ b/packages/ai/test/tool-runtime.test.ts @@ -553,7 +553,7 @@ describe("LLMClient tools", () => { ) yield* TestToolRuntime.runTools({ - request: LLM.updateRequest(baseRequest, { + request: LLMRequest.update(baseRequest, { model: AnthropicMessages.route .with({ auth: Auth.header("x-api-key", "test") }) .model({ id: "claude-sonnet-4-5" }), @@ -808,7 +808,7 @@ describe("LLMClient tools", () => { ) const events = Array.from( yield* TestToolRuntime.runTools({ - request: LLM.updateRequest(baseRequest, { + request: LLMRequest.update(baseRequest, { model: AnthropicMessages.route .with({ auth: Auth.header("x-api-key", "test") }) .model({ id: "claude-sonnet-4-5" }), From 0f3c30118ce08eabfa49dbdf516510d3ffc4da0e Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 24 Jul 2026 10:27:23 -0400 Subject: [PATCH 081/150] refactor(core): simplify session runner loop and pending input scopes (#38602) --- packages/core/src/session/pending.ts | 145 ++++++----- packages/core/src/session/runner/index.ts | 2 +- packages/core/src/session/runner/llm.ts | 290 +++++++++++----------- packages/core/src/session/runner/retry.ts | 23 +- packages/core/test/session-create.test.ts | 16 +- packages/core/test/session-prompt.test.ts | 37 ++- packages/core/test/session-runner.test.ts | 10 +- packages/core/test/tool-subagent.test.ts | 2 +- 8 files changed, 283 insertions(+), 242 deletions(-) diff --git a/packages/core/src/session/pending.ts b/packages/core/src/session/pending.ts index 70f0b34c5b36..6753903be9b2 100644 --- a/packages/core/src/session/pending.ts +++ b/packages/core/src/session/pending.ts @@ -1,6 +1,6 @@ export * as SessionPending from "./pending" -import { and, asc, eq } from "drizzle-orm" +import { and, asc, eq, or } from "drizzle-orm" import { DateTime, Effect, Schema } from "effect" import { Compaction, @@ -26,6 +26,13 @@ type DatabaseService = Database.Interface["db"] export { Compaction, Delivery, Info, Message, Synthetic, SyntheticData, User, UserData } +/** + * Which pending input `promote` may consume: "steer" promotes steers only (a step + * boundary mid-work), while "input" also allows one queued input when no steers are + * waiting (the idle boundary, where the Session picks up fresh work). + */ +export type Promotable = "input" | "steer" + const decodeUser = Schema.decodeUnknownSync(UserData) const encodeUser = Schema.encodeSync(UserData) const decodeSynthetic = Schema.decodeUnknownSync(SyntheticData) @@ -355,16 +362,32 @@ export const list = Effect.fn("SessionPending.list")(function* (db: DatabaseServ return rows.map(fromRow) }) +/** + * Which pending rows count: "any" counts every row including compaction, while + * delivery scopes are blocked behind a pending compaction barrier. "input" means + * any model-facing input, steered or queued. + */ +export type Scope = "any" | "input" | Delivery + export const has = Effect.fn("SessionPending.has")(function* ( db: DatabaseService, sessionID: SessionSchema.ID, - delivery: Delivery, + scope: Scope, ) { - if (yield* compaction(db, sessionID)) return false + if (scope !== "any" && (yield* compaction(db, sessionID))) return false const row = yield* db .select({ id: SessionPendingTable.id }) .from(SessionPendingTable) - .where(and(eq(SessionPendingTable.session_id, sessionID), eq(SessionPendingTable.delivery, delivery))) + .where( + and( + eq(SessionPendingTable.session_id, sessionID), + scope === "any" + ? undefined + : scope === "input" + ? or(eq(SessionPendingTable.delivery, "steer"), eq(SessionPendingTable.delivery, "queue")) + : eq(SessionPendingTable.delivery, scope), + ), + ) .limit(1) .get() .pipe(Effect.orDie) @@ -394,65 +417,73 @@ const publish = Effect.fn("SessionPending.publish")(function* ( sessionID: SessionSchema.ID, rows: ReadonlyArray, ) { - return yield* inboxLocks.withLock(sessionID)( - Effect.gen(function* () { - if (yield* compaction(db, sessionID)) return 0 - yield* Effect.forEach( - rows, - (row) => { - const entry = fromRow(row) - if (entry.type === "compaction") return Effect.die(new LifecycleConflict({ id: entry.id })) - return events - .publish(SessionEvent.InputPromoted, { - sessionID, - inputID: entry.id, - }) - .pipe( - Effect.catchDefect((defect) => - defect instanceof LifecycleConflict - ? promotedFromHistory(db, sessionID, entry.id).pipe( - Effect.flatMap((stored) => (stored !== undefined ? Effect.void : Effect.die(defect))), - ) - : Effect.die(defect), - ), - ) - }, - { discard: true }, - ) - return rows.length - }), + if (yield* compaction(db, sessionID)) return 0 + yield* Effect.forEach( + rows, + (row) => { + const entry = fromRow(row) + if (entry.type === "compaction") return Effect.die(new LifecycleConflict({ id: entry.id })) + return events + .publish(SessionEvent.InputPromoted, { + sessionID, + inputID: entry.id, + }) + .pipe( + Effect.catchDefect((defect) => + defect instanceof LifecycleConflict + ? promotedFromHistory(db, sessionID, entry.id).pipe( + Effect.flatMap((stored) => (stored !== undefined ? Effect.void : Effect.die(defect))), + ) + : Effect.die(defect), + ), + ) + }, + { discard: true }, ) + return rows.length }) -export const promoteSteers = Effect.fn("SessionPending.promoteSteers")(function* ( +/** + * Promotes pending input into visible messages and returns the promoted count. + * Steers always go first; only the "input" scope may fall through to one queued + * input, and it then collects steers that arrived during promotion. + */ +export const promote = Effect.fn("SessionPending.promote")(function* ( db: DatabaseService, events: EventV2.Interface, sessionID: SessionSchema.ID, + scope: Promotable, ) { - if (yield* compaction(db, sessionID)) return 0 - const rows = yield* db - .select() - .from(SessionPendingTable) - .where(and(eq(SessionPendingTable.session_id, sessionID), eq(SessionPendingTable.delivery, "steer"))) - .orderBy(asc(SessionPendingTable.admitted_seq)) - .all() - .pipe(Effect.orDie) - return yield* publish(db, events, sessionID, rows) -}) + return yield* inboxLocks.withLock(sessionID)( + Effect.gen(function* () { + if (yield* compaction(db, sessionID)) return 0 + const steers = yield* db + .select() + .from(SessionPendingTable) + .where(and(eq(SessionPendingTable.session_id, sessionID), eq(SessionPendingTable.delivery, "steer"))) + .orderBy(asc(SessionPendingTable.admitted_seq)) + .all() + .pipe(Effect.orDie) + if (steers.length > 0 || scope === "steer") return yield* publish(db, events, sessionID, steers) -export const promoteNextQueued = Effect.fn("SessionPending.promoteNextQueued")(function* ( - db: DatabaseService, - events: EventV2.Interface, - sessionID: SessionSchema.ID, -) { - if (yield* compaction(db, sessionID)) return false - const row = yield* db - .select() - .from(SessionPendingTable) - .where(and(eq(SessionPendingTable.session_id, sessionID), eq(SessionPendingTable.delivery, "queue"))) - .orderBy(asc(SessionPendingTable.admitted_seq)) - .limit(1) - .get() - .pipe(Effect.orDie) - return row === undefined ? false : yield* publish(db, events, sessionID, [row]).pipe(Effect.as(true)) + const queued = yield* db + .select() + .from(SessionPendingTable) + .where(and(eq(SessionPendingTable.session_id, sessionID), eq(SessionPendingTable.delivery, "queue"))) + .orderBy(asc(SessionPendingTable.admitted_seq)) + .limit(1) + .get() + .pipe(Effect.orDie) + if (!queued) return 0 + const promoted = yield* publish(db, events, sessionID, [queued]) + const arrivedSteers = yield* db + .select() + .from(SessionPendingTable) + .where(and(eq(SessionPendingTable.session_id, sessionID), eq(SessionPendingTable.delivery, "steer"))) + .orderBy(asc(SessionPendingTable.admitted_seq)) + .all() + .pipe(Effect.orDie) + return promoted + (yield* publish(db, events, sessionID, arrivedSteers)) + }), + ) }) diff --git a/packages/core/src/session/runner/index.ts b/packages/core/src/session/runner/index.ts index ee33da093f8f..65fdb223524e 100644 --- a/packages/core/src/session/runner/index.ts +++ b/packages/core/src/session/runner/index.ts @@ -20,7 +20,7 @@ export type RunError = /** Runs one local continuation from already-recorded Session history. */ export interface Interface { - /** Drains eligible durable work. Explicit runs perform one physical attempt even when no work is eligible. */ + /** Drains eligible durable work. Explicit runs make one model call even when no work is eligible. */ readonly drain: (input: { readonly sessionID: SessionSchema.ID readonly force: boolean diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index ff00a8337bb1..67666da35bdb 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -1,8 +1,7 @@ export * as SessionRunnerLLM from "./llm" import { LLMClient, LLMError, LLMEvent, isContextOverflowFailure, type ProviderErrorEvent } from "@opencode-ai/ai" -import { SessionError } from "@opencode-ai/schema/session-error" -import { Cause, Effect, Exit, Fiber, FiberSet, Layer, Option, Semaphore, Stream } from "effect" +import { Cause, Data, Effect, Exit, Fiber, FiberSet, Layer, Option, Pull, Schedule, Semaphore, Stream } from "effect" import { Database } from "../../database/database" import { EventV2 } from "../../event" import { PermissionV2 } from "../../permission" @@ -28,6 +27,14 @@ import { toSessionError } from "../to-session-error" import { SessionRunnerRetry } from "./retry" import { SessionUsage } from "../usage" +/** How one model call ended: settled, awaiting a scheduled retry, or restarted by compaction. */ +type CallOutcome = Data.TaggedEnum<{ + Completed: { readonly needsContinuation: boolean; readonly step: number } + Retry: { readonly step: number; readonly assistantMessageID: SessionMessage.ID } + Restart: { readonly step: number; readonly recoveredOverflow: boolean } +}> +const CallOutcome = Data.taggedEnum() + const layer = Layer.effect( Service, Effect.gen(function* () { @@ -43,14 +50,21 @@ const layer = Layer.effect( // Title generation is a side effect of the first step; it must not delay step continuation. // Tracked per process so repeated wakes before the second user message arrives don't // re-fire a redundant LLM call; `SessionTitle` itself is idempotent based on durable history. - const titleAttempted = new Set() + const titleStarted = new Set() const forkTitle = yield* FiberSet.makeRuntime() const getSession = Effect.fn("SessionRunner.getSession")(function* (sessionID: SessionSchema.ID) { const session = yield* store.get(sessionID) if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`)) return session }) - const failInterruptedTools = Effect.fn("SessionRunner.failInterruptedTools")(function* ( + /** Fires title generation once per process after the first step makes a user message visible. */ + const startTitleOnce = Effect.fnUntraced(function* (sessionID: SessionSchema.ID) { + if (titleStarted.has(sessionID)) return + titleStarted.add(sessionID) + forkTitle(title.generateForFirstPrompt(yield* getSession(sessionID)).pipe(Effect.ignore)) + }) + /** Closes stale tool calls left active by an earlier interrupted drain. */ + const settleStaleToolCalls = Effect.fn("SessionRunner.settleStaleToolCalls")(function* ( sessionID: SessionSchema.ID, ) { for (const message of yield* store.context(sessionID)) { @@ -76,11 +90,15 @@ const layer = Layer.effect( (reason.defect instanceof PermissionV2.DeclinedError || reason.defect instanceof QuestionTool.CancelledError), ) - const attemptStep = Effect.fn("SessionRunner.attemptStep")(function* ( + /** + * Prepares and runs at most one model call, executes its local tools, and durably + * settles the step. Compaction may instead request that the logical step restart. + */ + const callModel = Effect.fn("SessionRunner.callModel")(function* ( sessionID: SessionSchema.ID, - promotion: SessionPending.Delivery | undefined, + promotable: SessionPending.Promotable | undefined, step: number, - recoverOverflow?: typeof compaction.compact, + recoverOverflow: boolean, assistantMessageID?: SessionMessage.ID, ) { const selected = yield* context.select(sessionID) @@ -88,24 +106,20 @@ const layer = Layer.effect( // a blocked first step leaves pending inputs untouched. yield* InstructionState.prepare(db, events, selected.instructions, selected.session.id) let currentStep = step - if (promotion) { - let promoted = 0 - if (promotion === "steer") promoted = yield* SessionPending.promoteSteers(db, events, selected.session.id) - if (promotion === "queue") { - promoted += Number(yield* SessionPending.promoteNextQueued(db, events, selected.session.id)) - promoted += yield* SessionPending.promoteSteers(db, events, selected.session.id) - } + if (promotable) { + const promoted = yield* SessionPending.promote(db, events, selected.session.id, promotable) if (promoted > 0) currentStep = 1 } const loaded = yield* context.load(selected) - const session = loaded.session - const agent = loaded.agent + const { session, agent } = loaded const resolved = loaded.model const model = resolved.model + // Make room: history must fit the context window before the call. A pending manual + // compaction owns this instead; the runner executes it between steps. const compactionInput = { session, messages: loaded.messages, model, cost: resolved.cost } if (compaction.required(compactionInput) && !(yield* SessionPending.compaction(db, session.id))) { const compacted = yield* compaction.compact(compactionInput) - if (compacted.status === "completed") return { _tag: "RestartAfterCompaction", step: currentStep } as const + if (compacted.status === "completed") return CallOutcome.Restart({ step: currentStep, recoveredOverflow: false }) return yield* new StepFailedError({ error: compacted.error }) } const prepared = yield* modelRequests.prepare({ @@ -131,6 +145,40 @@ const layer = Layer.effect( // mid-event. const serialized = (effect: Effect.Effect) => publication.withPermit(effect) const publish = (event: LLMEvent) => serialized(publisher.publish(event)) + + const stepUsage = (settlement: NonNullable>) => ({ + cost: SessionUsage.calculateCost(resolved.cost, settlement.tokens), + tokens: settlement.tokens, + }) + + const captureStepEnd = Effect.fnUntraced(function* () { + const snapshot = yield* snapshots.capture() + const files = + startSnapshot && snapshot + ? yield* snapshots + .files({ from: startSnapshot, to: snapshot }) + .pipe(Effect.catch(() => Effect.succeed(undefined))) + : undefined + return { snapshot, files } + }) + + const publishStepEnd = (settlement: NonNullable>) => + Effect.gen(function* () { + const end = yield* captureStepEnd() + yield* serialized( + events.publish(SessionEvent.Step.Ended, { + sessionID: session.id, + assistantMessageID: yield* publisher.startAssistant(), + finish: settlement.finish, + ...stepUsage(settlement), + ...end, + }), + ) + }) + + // The stream is defined here but runs inside the settlement mask below: publish each + // event durably, fork one fiber per local tool call, and hold back a virgin + // context-overflow provider error so settlement may recover it via compaction. let overflowFailure: ProviderErrorEvent | undefined const providerStream = llm.stream(prepared.request).pipe( Stream.runForEach((event) => @@ -172,39 +220,10 @@ const layer = Layer.effect( Effect.ensuring(serialized(publisher.flush())), ) - const stepUsage = (settlement: NonNullable>) => ({ - cost: SessionUsage.calculateCost(resolved.cost, settlement.tokens), - tokens: settlement.tokens, - }) - - const captureStepEnd = Effect.fnUntraced(function* () { - const snapshot = yield* snapshots.capture() - const files = - startSnapshot && snapshot - ? yield* snapshots - .files({ from: startSnapshot, to: snapshot }) - .pipe(Effect.catch(() => Effect.succeed(undefined))) - : undefined - return { snapshot, files } - }) - - const publishStepEnd = (settlement: NonNullable>) => - Effect.gen(function* () { - const end = yield* captureStepEnd() - yield* serialized( - events.publish(SessionEvent.Step.Ended, { - sessionID: session.id, - assistantMessageID: yield* publisher.startAssistant(), - finish: settlement.finish, - ...stepUsage(settlement), - ...end, - }), - ) - }) - + // Settle: only the stream itself is interruptible (restore); every line after it is + // protected so a started call always reaches one durable outcome. return yield* Effect.uninterruptibleMask((restore) => Effect.gen(function* () { - // Gather the evidence: how did the provider stream end? const stream = yield* restore(providerStream).pipe(Effect.exit) const streamFailure = Option.getOrUndefined(Exit.findErrorOption(stream)) // Note: Exit.hasInterrupts is a type guard whose false branch unsoundly narrows @@ -217,10 +236,10 @@ const layer = Layer.effect( recoverOverflow && !publisher.hasRetryEvidence() && isContextOverflowFailure(overflowFailure ?? streamFailure) && - (yield* restore(recoverOverflow({ session, messages: loaded.messages, model, cost: resolved.cost }))) + (yield* restore(compaction.compact({ session, messages: loaded.messages, model, cost: resolved.cost }))) .status === "completed" ) - return { _tag: "RestartAfterOverflowCompaction", step: currentStep } as const + return CallOutcome.Restart({ step: currentStep, recoveredOverflow: true }) // An unrecovered held-back overflow becomes the step's durable provider error. A // thrown LLM failure records the assistant failure unless a provider error was @@ -324,63 +343,59 @@ const layer = Layer.effect( return yield* Effect.failCause(settledFailure) if (toolsInterrupted && settled._tag === "Failure") return yield* Effect.failCause(settled.cause) if (stepFailure) return yield* new StepFailedError({ error: stepFailure }) - return { - _tag: "Completed", - needsContinuation, - step: currentStep, - } as const + return CallOutcome.Completed({ needsContinuation, step: currentStep }) }), ) }, Effect.scoped) + /** Completes one logical model step, transparently retrying or rebuilding after compaction. */ const runStep = Effect.fnUntraced(function* ( sessionID: SessionSchema.ID, - promotion: SessionPending.Delivery | undefined, + promotable: SessionPending.Promotable, step: number, ) { - // Compaction restarts rebuild the request from compacted history without re-promoting. - // Overflow recovery is one-shot: a post-compaction attempt must not recover another - // overflow, so the recovery hook is dropped after it fires. - let recoverOverflow: typeof compaction.compact | undefined = compaction.compact - let currentPromotion = promotion - let currentStep = step - let assistantMessageID: SessionMessage.ID | undefined - while (true) { - const attempt = yield* Effect.suspend(() => - attemptStep(sessionID, currentPromotion, currentStep, recoverOverflow, assistantMessageID), - ).pipe( - Effect.tapError((error) => - error instanceof SessionRunnerRetry.RetryableFailure - ? Effect.sync(() => { - currentStep = error.step - assistantMessageID = error.assistantMessageID - currentPromotion = undefined - }) - : Effect.void, - ), - Effect.retryOrElse(SessionRunnerRetry.schedule(events, sessionID), (error) => { - if (!(error instanceof SessionRunnerRetry.RetryableFailure)) return Effect.fail(error) - return events + const retry = yield* Schedule.toStepWithSleep(SessionRunnerRetry.schedule(events, sessionID)) + /** + * Consumes one retry allowance: sleeps the scheduled backoff and reports what the next + * attempt should reuse, or publishes Step.Failed and fails once attempts are exhausted. + * The step loop performs the retry itself on the next iteration. + */ + const waitForRetry = (failure: SessionRunnerRetry.RetryableFailure) => + retry(failure).pipe( + Effect.as(CallOutcome.Retry({ step: failure.step, assistantMessageID: failure.assistantMessageID })), + Pull.catchDone(() => + events .publish(SessionEvent.Step.Failed, { sessionID, - assistantMessageID: error.assistantMessageID, - error: error.error, + assistantMessageID: failure.assistantMessageID, + error: failure.error, }) - .pipe(Effect.andThen(Effect.fail(error.cause))) - }), + .pipe(Effect.andThen(Effect.fail(failure.cause))), + ), ) - if (attempt._tag === "Completed") - return { - needsContinuation: attempt.needsContinuation, - step: attempt.step, - } - if (attempt._tag === "RestartAfterOverflowCompaction") recoverOverflow = undefined - yield* Effect.yieldNow - currentPromotion = undefined - currentStep = attempt.step + let currentPromotable: SessionPending.Promotable | undefined = promotable + let currentStep = step + let assistantMessageID: SessionMessage.ID | undefined + // Overflow recovery is one-shot: a call after recovery must not recover another overflow. + let recoverOverflow = true + while (true) { + const outcome = yield* callModel( + sessionID, + currentPromotable, + currentStep, + recoverOverflow, + assistantMessageID, + ).pipe(Effect.catchTag("SessionRunner.RetryableFailure", waitForRetry)) + if (outcome._tag === "Completed") return { needsContinuation: outcome.needsContinuation, step: outcome.step } + if (outcome._tag === "Retry") assistantMessageID = outcome.assistantMessageID + if (outcome._tag === "Restart" && outcome.recoveredOverflow) recoverOverflow = false + // Neither a retry nor a compaction restart re-promotes input. + currentPromotable = undefined + currentStep = outcome.step } }) + /** Executes a previously admitted manual compaction request, if one is pending. */ const runPendingCompaction = Effect.fn("SessionRunner.runPendingCompaction")(function* ( sessionID: SessionSchema.ID, ) { @@ -399,67 +414,54 @@ const layer = Layer.effect( }), ).pipe(Effect.exit) if (Exit.isSuccess(compacted)) return - if (Exit.isFailure(compacted)) { - const unsettled = yield* SessionPending.compaction(db, sessionID) - if (unsettled) - yield* events.publish(SessionEvent.Compaction.Failed, { - sessionID, - reason: "manual", - error: Cause.hasInterruptsOnly(compacted.cause) - ? { type: "aborted", message: "Compaction cancelled" } - : { type: "compaction.failed", message: Cause.pretty(compacted.cause) }, - inputID: unsettled.id, - }) - return yield* Effect.failCause(compacted.cause) - } + const unsettled = yield* SessionPending.compaction(db, sessionID) + if (unsettled) + yield* events.publish(SessionEvent.Compaction.Failed, { + sessionID, + reason: "manual", + error: Cause.hasInterruptsOnly(compacted.cause) + ? { type: "aborted", message: "Compaction cancelled" } + : { type: "compaction.failed", message: Cause.pretty(compacted.cause) }, + inputID: unsettled.id, + }) + return yield* Effect.failCause(compacted.cause) }), ) }) - // Execution lifecycle is published per busy period by SessionExecution, not per drain here. + /** + * Runs logical steps until no tool result or newly admitted steer requires another + * model call. Queued inputs remain pending until the current model work reaches idle. + */ + const runSteps = Effect.fn("SessionRunner.runSteps")(function* (sessionID: SessionSchema.ID) { + // Fresh work may promote queued input; later steps absorb steers only. + let promotable: SessionPending.Promotable = "input" + let step = 1 + while (true) { + const result = yield* runStep(sessionID, promotable, step) + yield* startTitleOnce(sessionID) + yield* runPendingCompaction(sessionID) + if (!result.needsContinuation && !(yield* SessionPending.has(db, sessionID, "steer"))) return + promotable = "steer" + step = result.step + 1 + } + }) + + /** + * Drains eligible manual compaction and user input until the Session becomes idle. + * Execution lifecycle is published per busy period by SessionExecution, not here. + */ const drain = Effect.fn("SessionRunner.drain")(function* (input: { readonly sessionID: SessionSchema.ID readonly force: boolean }) { + if (!input.force && !(yield* SessionPending.has(db, input.sessionID, "any"))) return + yield* settleStaleToolCalls(input.sessionID) yield* runPendingCompaction(input.sessionID) - const hasSteer = yield* SessionPending.has(db, input.sessionID, "steer") - const hasQueue = hasSteer ? false : yield* SessionPending.has(db, input.sessionID, "queue") - if (!input.force && !hasSteer && !hasQueue) return - yield* failInterruptedTools(input.sessionID) - let promotion: SessionPending.Delivery | undefined = hasSteer ? "steer" : hasQueue ? "queue" : undefined - let shouldRun = input.force || hasSteer || hasQueue - while (shouldRun) { - let needsContinuation = true - let step = 1 - // Repeat steps while continuation is needed. A step needs continuation only - // when it recorded local tool calls whose results the model has not yet seen; - // a provider error suppresses it. Pending steers also continue the loop so - // interjections are answered before the session goes idle. - while (needsContinuation) { - const result = yield* runStep(input.sessionID, promotion, step) - // Steer/queue promotion inside runStep has already made the pending input a visible - // user message by this point, so the first-user-message check below is reliable. - if (!titleAttempted.has(input.sessionID)) { - titleAttempted.add(input.sessionID) - forkTitle(title.generateForFirstPrompt(yield* getSession(input.sessionID)).pipe(Effect.ignore)) - } - needsContinuation = result.needsContinuation - step = result.step + 1 - if (needsContinuation) { - yield* runPendingCompaction(input.sessionID) - promotion = "steer" - continue - } - yield* runPendingCompaction(input.sessionID) - promotion = "steer" - needsContinuation = yield* SessionPending.has(db, input.sessionID, "steer") - } - yield* runPendingCompaction(input.sessionID) - const hasSteer = yield* SessionPending.has(db, input.sessionID, "steer") - const hasQueue = hasSteer ? false : yield* SessionPending.has(db, input.sessionID, "queue") - shouldRun = hasSteer || hasQueue - promotion = hasSteer ? "steer" : hasQueue ? "queue" : undefined - } + if (!input.force && !(yield* SessionPending.has(db, input.sessionID, "input"))) return + do { + yield* runSteps(input.sessionID) + } while (yield* SessionPending.has(db, input.sessionID, "input")) }) return Service.of({ drain }) diff --git a/packages/core/src/session/runner/retry.ts b/packages/core/src/session/runner/retry.ts index 11626b586d98..94401a35d68b 100644 --- a/packages/core/src/session/runner/retry.ts +++ b/packages/core/src/session/runner/retry.ts @@ -7,7 +7,6 @@ import { EventV2 } from "../../event" import { SessionEvent } from "../event" import { SessionMessage } from "../message" import { SessionSchema } from "../schema" -import type { SessionRunner } from "./index" export class RetryableFailure extends Data.TaggedError("SessionRunner.RetryableFailure")<{ readonly cause: LLMError @@ -45,22 +44,18 @@ const retryAfter = (failure: RetryableFailure) => { export const schedule = (events: EventV2.Interface, sessionID: SessionSchema.ID) => Schedule.max([Schedule.exponential("2 seconds"), Schedule.recurs(4)]).pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => input instanceof RetryableFailure), + Schedule.setInputType(), Schedule.modifyDelay(({ input: failure, duration: delay }) => { - const minimum = failure instanceof RetryableFailure ? retryAfter(failure) : undefined + const minimum = retryAfter(failure) return Effect.succeed(minimum === undefined ? delay : Duration.max(delay, Duration.millis(minimum))) }), Schedule.tap((metadata) => - metadata.input instanceof RetryableFailure - ? events.publish(SessionEvent.RetryScheduled, { - sessionID, - assistantMessageID: metadata.input.assistantMessageID, - attempt: metadata.attempt + 1, - at: metadata.now + Duration.toMillis(metadata.duration), - error: metadata.input.error, - }) - : Effect.void, + events.publish(SessionEvent.RetryScheduled, { + sessionID, + assistantMessageID: metadata.input.assistantMessageID, + attempt: metadata.attempt + 1, + at: metadata.now + Duration.toMillis(metadata.duration), + error: metadata.input.error, + }), ), ) diff --git a/packages/core/test/session-create.test.ts b/packages/core/test/session-create.test.ts index 80136f02f866..422d5892300e 100644 --- a/packages/core/test/session-create.test.ts +++ b/packages/core/test/session-create.test.ts @@ -195,9 +195,9 @@ describe("SessionV2.create", () => { text: "First", resume: false, }) - yield* SessionPending.promoteSteers(db, events, parent.id) + yield* SessionPending.promote(db, events, parent.id, "steer") yield* session.synthetic({ sessionID: parent.id, text: "parent note", resume: false }) - yield* SessionPending.promoteSteers(db, events, parent.id) + yield* SessionPending.promote(db, events, parent.id, "steer") const forked = yield* session.fork({ sessionID: parent.id }) const parentContext = yield* session.context(parent.id) @@ -232,13 +232,13 @@ describe("SessionV2.create", () => { text: "Parent changed", resume: false, }) - yield* SessionPending.promoteSteers(db, events, parent.id) + yield* SessionPending.promote(db, events, parent.id, "steer") yield* session.prompt({ sessionID: forked.id, text: "Child continues", resume: false, }) - yield* SessionPending.promoteSteers(db, events, forked.id) + yield* SessionPending.promote(db, events, forked.id, "steer") expect((yield* session.context(parent.id)).map((message) => message.type)).toEqual(["user", "synthetic", "user"]) expect((yield* session.context(forked.id)).map((message) => message.type)).toEqual(["user", "synthetic", "user"]) @@ -263,13 +263,13 @@ describe("SessionV2.create", () => { text: "First", resume: false, }) - yield* SessionPending.promoteSteers(db, events, parent.id) + yield* SessionPending.promote(db, events, parent.id, "steer") const second = yield* session.prompt({ sessionID: parent.id, text: "Second", resume: false, }) - yield* SessionPending.promoteSteers(db, events, parent.id) + yield* SessionPending.promote(db, events, parent.id, "steer") const assistantMessageID = SessionMessage.ID.create() const model = ModelV2.Ref.make({ id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }) yield* events.publish(SessionEvent.Step.Started, { @@ -414,7 +414,7 @@ describe("SessionV2.create", () => { text: "Hello", resume: false, }) - yield* SessionPending.promoteSteers(db, events, created.id) + yield* SessionPending.promote(db, events, created.id, "steer") expect( Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(2), Stream.runCollect)), @@ -440,7 +440,7 @@ describe("SessionV2.create", () => { text: "Replay lifecycle", resume: false, }) - yield* SessionPending.promoteSteers(sourceDb, sourceEvents, created.id) + yield* SessionPending.promote(sourceDb, sourceEvents, created.id, "steer") const serialized = (yield* sourceDb .select() .from(EventTable) diff --git a/packages/core/test/session-prompt.test.ts b/packages/core/test/session-prompt.test.ts index 941154351247..8cab48f14a15 100644 --- a/packages/core/test/session-prompt.test.ts +++ b/packages/core/test/session-prompt.test.ts @@ -216,7 +216,7 @@ describe("SessionV2.prompt", () => { text: "boundary", resume: false, }) - yield* SessionPending.promoteSteers(db, events, sessionID) + yield* SessionPending.promote(db, events, sessionID, "steer") const stale = SessionMessage.ID.make("msg_stale_assistant") yield* db.insert(SessionMessageTable).values(assistantRow(stale, 100)).run().pipe(Effect.orDie) yield* events.publish(SessionEvent.RevertEvent.Staged, { @@ -248,7 +248,7 @@ describe("SessionV2.prompt", () => { text: "boundary", resume: false, }) - yield* SessionPending.promoteSteers(db, events, sessionID) + yield* SessionPending.promote(db, events, sessionID, "steer") yield* events.publish(SessionEvent.RevertEvent.Staged, { sessionID, revert: { messageID: boundary.id, files: [] }, @@ -448,7 +448,7 @@ describe("SessionV2.prompt", () => { yield* session.prompt({ sessionID, text: "First", resume: false }) yield* session.prompt({ sessionID, text: "Second", resume: false }) - yield* SessionPending.promoteSteers(db, events, sessionID) + yield* SessionPending.promote(db, events, sessionID, "steer") const streamed = Array.from(yield* Fiber.join(fiber)) expect(streamed.map((event): [number | undefined, string] => [event.durable?.seq, event.type])).toEqual([ @@ -625,7 +625,7 @@ describe("SessionV2.prompt", () => { }) yield* Effect.all( - [SessionPending.promoteSteers(db, events, sessionID), SessionPending.promoteSteers(db, events, sessionID)], + [SessionPending.promote(db, events, sessionID, "steer"), SessionPending.promote(db, events, sessionID, "steer")], { concurrency: "unbounded" }, ) @@ -855,7 +855,7 @@ describe("SessionV2.prompt", () => { }, }) - yield* SessionPending.promoteSteers(db, events, sessionID) + yield* SessionPending.promote(db, events, sessionID, "steer") expect(yield* session.messages({ sessionID })).toMatchObject([ { @@ -880,7 +880,7 @@ describe("SessionV2.prompt", () => { const entries = yield* Effect.all([session.synthetic(input), session.synthetic(input)], { concurrency: "unbounded", }) - yield* SessionPending.promoteSteers(database.db, events, sessionID) + yield* SessionPending.promote(database.db, events, sessionID, "steer") const promotedRetry = yield* session.synthetic(input) const failure = yield* session.synthetic({ ...input, text: "Different completion" }).pipe(Effect.flip) @@ -892,7 +892,7 @@ describe("SessionV2.prompt", () => { }), ) - it.effect("keeps synthetic queue input pending until the queue boundary", () => + it.effect("keeps queued input pending until the idle boundary", () => Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service @@ -907,9 +907,15 @@ describe("SessionV2.prompt", () => { }) expect(input.delivery).toBe("queue") - expect(yield* SessionPending.promoteSteers(db, events, sessionID)).toBe(0) + expect(yield* SessionPending.has(db, sessionID, "input")).toBe(true) + expect( + yield* SessionPending.promote(db, events, sessionID, "steer"), + ).toBe(0) expect(yield* session.messages({ sessionID })).toEqual([]) - expect(yield* SessionPending.promoteNextQueued(db, events, sessionID)).toBe(true) + expect( + yield* SessionPending.promote(db, events, sessionID, "input"), + ).toBe(1) + expect(yield* SessionPending.has(db, sessionID, "input")).toBe(false) expect(yield* session.messages({ sessionID })).toMatchObject([ { id: input.id, type: "synthetic", text: "Queued completion" }, ]) @@ -935,7 +941,7 @@ describe("SessionV2.prompt", () => { resume: false, }) - yield* SessionPending.promoteSteers(db, events, sessionID) + yield* SessionPending.promote(db, events, sessionID, "steer") expect( (yield* session.messages({ sessionID, order: "asc" })).map((message) => @@ -978,10 +984,14 @@ describe("SessionV2.pending", () => { { id: second.id, type: "user", delivery: "steer" }, ]) - yield* SessionPending.promoteSteers(db, events, sessionID) + expect( + yield* SessionPending.promote(db, events, sessionID, "input"), + ).toBe(2) expect(yield* session.pending(sessionID)).toMatchObject([{ id: queued.id, type: "synthetic" }]) - yield* SessionPending.promoteNextQueued(db, events, sessionID) + expect( + yield* SessionPending.promote(db, events, sessionID, "input"), + ).toBe(1) expect(yield* session.pending(sessionID)).toEqual([]) }), ) @@ -993,9 +1003,12 @@ describe("SessionV2.pending", () => { const { db } = yield* Database.Service const barrier = yield* session.compact({ sessionID }) + expect(yield* SessionPending.has(db, sessionID, "any")).toBe(true) + expect(yield* SessionPending.has(db, sessionID, "input")).toBe(false) expect(yield* session.pending(sessionID)).toMatchObject([{ id: barrier.id, type: "compaction" }]) yield* SessionPending.settleCompaction(db, { sessionID }) + expect(yield* SessionPending.has(db, sessionID, "any")).toBe(false) expect(yield* session.pending(sessionID)).toEqual([]) }), ) diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index a96e08eb5452..a6f6d0c43e94 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -3099,7 +3099,7 @@ describe("SessionRunnerLLM", () => { streamFailure = undefined streamGate = undefined streamStarted = undefined - yield* Effect.yieldNow + yield* session.wait(sessionID) expect(requests).toHaveLength(2) expect(userTexts(requests[1]!)).toEqual(["Start working", "Recover with this"]) @@ -3111,7 +3111,7 @@ describe("SessionRunnerLLM", () => { const session = yield* setup const events = yield* EventV2.Service yield* admit(session, "Recover interrupted tool") - yield* SessionPending.promoteSteers((yield* Database.Service).db, events, sessionID) + yield* SessionPending.promote((yield* Database.Service).db, events, sessionID, "steer") const assistantMessageID = SessionMessage.ID.create() yield* events.publish(SessionEvent.Step.Started, { sessionID, @@ -3168,7 +3168,7 @@ describe("SessionRunnerLLM", () => { const session = yield* setup const events = yield* EventV2.Service yield* admit(session, "Recover interrupted hosted tool") - yield* SessionPending.promoteSteers((yield* Database.Service).db, events, sessionID) + yield* SessionPending.promote((yield* Database.Service).db, events, sessionID, "steer") const assistantMessageID = SessionMessage.ID.create() yield* events.publish(SessionEvent.Step.Started, { sessionID, @@ -3219,7 +3219,7 @@ describe("SessionRunnerLLM", () => { const session = yield* setup const events = yield* EventV2.Service yield* admit(session, "Recover interrupted tool input") - yield* SessionPending.promoteSteers((yield* Database.Service).db, events, sessionID) + yield* SessionPending.promote((yield* Database.Service).db, events, sessionID, "steer") const assistantMessageID = SessionMessage.ID.create() yield* events.publish(SessionEvent.Step.Started, { sessionID, @@ -4220,7 +4220,7 @@ describe("SessionRunnerLLM", () => { }), ) - it.effect("retries a physical attempt without consuming the logical agent step", () => + it.effect("retries a model call without consuming the logical agent step", () => Effect.gen(function* () { const session = yield* setup const agents = yield* AgentV2.Service diff --git a/packages/core/test/tool-subagent.test.ts b/packages/core/test/tool-subagent.test.ts index 8ec8ecec162e..03705eee217b 100644 --- a/packages/core/test/tool-subagent.test.ts +++ b/packages/core/test/tool-subagent.test.ts @@ -406,7 +406,7 @@ describe("SubagentTool", () => { }, }) const database = yield* Database.Service - yield* SessionPending.promoteSteers(database.db, events, parent.id) + yield* SessionPending.promote(database.db, events, parent.id, "steer") const synthetic = (yield* sessions.context(parent.id)).filter((message) => message.type === "synthetic") expect(synthetic).toHaveLength(1) expect(synthetic[0]?.text).toContain(` Date: Fri, 24 Jul 2026 11:08:57 -0400 Subject: [PATCH 082/150] refactor(core): select small model without sorting (#38707) --- packages/core/src/catalog.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts index 9b8f916e7075..8d2c10c1a883 100644 --- a/packages/core/src/catalog.ts +++ b/packages/core/src/catalog.ts @@ -1,7 +1,7 @@ export * as Catalog from "./catalog" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { Array, Context, Effect, Layer, Option, Order, pipe } from "effect" +import { Array, Context, Effect, Layer, Order, pipe } from "effect" import { Catalog } from "@opencode-ai/schema/catalog" import { ModelV2 } from "./model" import { ProviderV2 } from "./provider" @@ -242,23 +242,22 @@ const layer = Layer.effect( ) const pick = (items: typeof candidates) => { + if (!Array.isReadonlyArrayNonEmpty(items)) return const maxCost = Math.max(...items.map((item) => item.cost), 0.01) const maxAge = Math.max(...items.map((item) => item.age), 0.01) - return pipe( + const selected = Array.min( items, - Array.sortWith((item) => (item.cost / maxCost) * 0.8 + (item.age / maxAge) * 0.2, Order.Number), - Array.map((item) => projectModel(item.model, provider)), - Array.head, + Order.mapInput( + Order.Number, + (item: (typeof candidates)[number]) => + (item.cost / maxCost) * 0.8 + (item.age / maxAge) * 0.2, + ), ) + return projectModel(selected.model, provider) } - return Option.getOrUndefined( - pipe( - candidates, - Array.filter((item) => item.small), - (items) => (items.length > 0 ? pick(items) : pick(candidates)), - ), - ) + const small = candidates.filter((item) => item.small) + return pick(small.length > 0 ? small : candidates) }), }, } From 6e4a972bb99b609b55f9354eb6ba4568fc9e6ff3 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 24 Jul 2026 11:17:20 -0400 Subject: [PATCH 083/150] refactor(core): clean up callModel readability (#38706) --- packages/core/src/session/runner/llm.ts | 284 ++++++++++++------------ 1 file changed, 145 insertions(+), 139 deletions(-) diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 67666da35bdb..94d5c7580eec 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -35,6 +35,33 @@ type CallOutcome = Data.TaggedEnum<{ }> const CallOutcome = Data.taggedEnum() +// Declining an interactive prompt halts the drain instead of becoming model-facing tool output. +const isUserDeclined = (cause: Cause.Cause) => + cause.reasons.some( + (reason) => + Cause.isDieReason(reason) && + (reason.defect instanceof PermissionV2.DeclinedError || reason.defect instanceof QuestionTool.CancelledError), + ) + +/** + * Classifies how the owned tool fibers ended. Interrupts and interactive declines abort + * the step; a defect from a tool implementation becomes a failed tool call the model can + * read; a typed infrastructure failure must fail the assistant and then the drain. + */ +const classifyToolExits = (settled: Exit.Exit>, never>) => { + const causes = + settled._tag === "Failure" + ? [settled.cause] + : settled.value.flatMap((exit) => (exit._tag === "Failure" ? [exit.cause] : [])) + const failure = causes.find((cause) => !Cause.hasInterrupts(cause) && !isUserDeclined(cause)) + return { + interrupted: causes.some(Cause.hasInterrupts), + declined: causes.some(isUserDeclined), + failure, + infraError: failure === undefined ? undefined : Option.getOrUndefined(Cause.findErrorOption(failure)), + } +} + const layer = Layer.effect( Service, Effect.gen(function* () { @@ -52,44 +79,88 @@ const layer = Layer.effect( // re-fire a redundant LLM call; `SessionTitle` itself is idempotent based on durable history. const titleStarted = new Set() const forkTitle = yield* FiberSet.makeRuntime() - const getSession = Effect.fn("SessionRunner.getSession")(function* (sessionID: SessionSchema.ID) { - const session = yield* store.get(sessionID) - if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`)) - return session + /** + * Drains eligible manual compaction and user input until the Session becomes idle. + * Execution lifecycle is published per busy period by SessionExecution, not here. + */ + const drain = Effect.fn("SessionRunner.drain")(function* (input: { + readonly sessionID: SessionSchema.ID + readonly force: boolean + }) { + if (!input.force && !(yield* SessionPending.has(db, input.sessionID, "any"))) return + yield* settleStaleToolCalls(input.sessionID) + yield* runPendingCompaction(input.sessionID) + if (!input.force && !(yield* SessionPending.has(db, input.sessionID, "input"))) return + do { + yield* runSteps(input.sessionID) + } while (yield* SessionPending.has(db, input.sessionID, "input")) }) - /** Fires title generation once per process after the first step makes a user message visible. */ - const startTitleOnce = Effect.fnUntraced(function* (sessionID: SessionSchema.ID) { - if (titleStarted.has(sessionID)) return - titleStarted.add(sessionID) - forkTitle(title.generateForFirstPrompt(yield* getSession(sessionID)).pipe(Effect.ignore)) + + /** + * Runs logical steps until no tool result or newly admitted steer requires another + * model call. Queued inputs remain pending until the current model work reaches idle. + */ + const runSteps = Effect.fn("SessionRunner.runSteps")(function* (sessionID: SessionSchema.ID) { + // Fresh work may promote queued input; later steps absorb steers only. + let promotable: SessionPending.Promotable = "input" + let step = 1 + while (true) { + const result = yield* runStep(sessionID, promotable, step) + yield* startTitleOnce(sessionID) + yield* runPendingCompaction(sessionID) + if (!result.needsContinuation && !(yield* SessionPending.has(db, sessionID, "steer"))) return + promotable = "steer" + step = result.step + 1 + } }) - /** Closes stale tool calls left active by an earlier interrupted drain. */ - const settleStaleToolCalls = Effect.fn("SessionRunner.settleStaleToolCalls")(function* ( + + /** Completes one logical model step, transparently retrying or rebuilding after compaction. */ + const runStep = Effect.fnUntraced(function* ( sessionID: SessionSchema.ID, + promotable: SessionPending.Promotable, + step: number, ) { - for (const message of yield* store.context(sessionID)) { - if (message.type !== "assistant") continue - for (const tool of message.content) { - if (tool.type !== "tool" || (tool.state.status !== "streaming" && tool.state.status !== "running")) continue - yield* events.publish(SessionEvent.Tool.Failed, { - sessionID, - assistantMessageID: message.id, - callID: tool.id, - error: { type: "aborted", message: `Tool execution interrupted: ${tool.name}` }, - executed: tool.executed === true, - }) - } + const retry = yield* Schedule.toStepWithSleep(SessionRunnerRetry.schedule(events, sessionID)) + /** + * Consumes one retry allowance: sleeps the scheduled backoff and reports what the next + * attempt should reuse, or publishes Step.Failed and fails once attempts are exhausted. + * The step loop performs the retry itself on the next iteration. + */ + const waitForRetry = (failure: SessionRunnerRetry.RetryableFailure) => + retry(failure).pipe( + Effect.as(CallOutcome.Retry({ step: failure.step, assistantMessageID: failure.assistantMessageID })), + Pull.catchDone(() => + events + .publish(SessionEvent.Step.Failed, { + sessionID, + assistantMessageID: failure.assistantMessageID, + error: failure.error, + }) + .pipe(Effect.andThen(Effect.fail(failure.cause))), + ), + ) + let currentPromotable: SessionPending.Promotable | undefined = promotable + let currentStep = step + let assistantMessageID: SessionMessage.ID | undefined + // Overflow recovery is one-shot: a call after recovery must not recover another overflow. + let recoverOverflow = true + while (true) { + const outcome = yield* callModel( + sessionID, + currentPromotable, + currentStep, + recoverOverflow, + assistantMessageID, + ).pipe(Effect.catchTag("SessionRunner.RetryableFailure", waitForRetry)) + if (outcome._tag === "Completed") return { needsContinuation: outcome.needsContinuation, step: outcome.step } + if (outcome._tag === "Retry") assistantMessageID = outcome.assistantMessageID + if (outcome._tag === "Restart" && outcome.recoveredOverflow) recoverOverflow = false + // Neither a retry nor a compaction restart re-promotes input. + currentPromotable = undefined + currentStep = outcome.step } }) - // Declining an interactive prompt halts the drain instead of becoming model-facing tool output. - const isUserDeclined = (cause: Cause.Cause) => - cause.reasons.some( - (reason) => - Cause.isDieReason(reason) && - (reason.defect instanceof PermissionV2.DeclinedError || reason.defect instanceof QuestionTool.CancelledError), - ) - /** * Prepares and runs at most one model call, executes its local tools, and durably * settles the step. Compaction may instead request that the logical step restart. @@ -105,11 +176,9 @@ const layer = Layer.effect( // Establish what the model knows before admitting what the user said, so // a blocked first step leaves pending inputs untouched. yield* InstructionState.prepare(db, events, selected.instructions, selected.session.id) - let currentStep = step - if (promotable) { - const promoted = yield* SessionPending.promote(db, events, selected.session.id, promotable) - if (promoted > 0) currentStep = 1 - } + const promoted = promotable ? yield* SessionPending.promote(db, events, selected.session.id, promotable) : 0 + // Promoted input opens a fresh step allowance. + const currentStep = promoted > 0 ? 1 : step const loaded = yield* context.load(selected) const { session, agent } = loaded const resolved = loaded.model @@ -119,7 +188,8 @@ const layer = Layer.effect( const compactionInput = { session, messages: loaded.messages, model, cost: resolved.cost } if (compaction.required(compactionInput) && !(yield* SessionPending.compaction(db, session.id))) { const compacted = yield* compaction.compact(compactionInput) - if (compacted.status === "completed") return CallOutcome.Restart({ step: currentStep, recoveredOverflow: false }) + if (compacted.status === "completed") + return CallOutcome.Restart({ step: currentStep, recoveredOverflow: false }) return yield* new StepFailedError({ error: compacted.error }) } const prepared = yield* modelRequests.prepare({ @@ -236,8 +306,7 @@ const layer = Layer.effect( recoverOverflow && !publisher.hasRetryEvidence() && isContextOverflowFailure(overflowFailure ?? streamFailure) && - (yield* restore(compaction.compact({ session, messages: loaded.messages, model, cost: resolved.cost }))) - .status === "completed" + (yield* restore(compaction.compact(compactionInput))).status === "completed" ) return CallOutcome.Restart({ step: currentStep, recoveredOverflow: true }) @@ -267,30 +336,17 @@ const layer = Layer.effect( const settled = yield* restore( Effect.forEach(ownedToolFibers, Fiber.await, { concurrency: "unbounded" }), ).pipe(Effect.exit) - const settledCauses = - settled._tag === "Failure" - ? [settled.cause] - : settled.value.flatMap((exit) => (exit._tag === "Failure" ? [exit.cause] : [])) - const toolsInterrupted = settledCauses.some(Cause.hasInterrupts) - const userDeclined = settledCauses.some(isUserDeclined) - if (settled._tag === "Failure") yield* FiberSet.clear(toolFibers) - if (userDeclined || streamInterrupted || toolsInterrupted) { + const tools = classifyToolExits(settled) + + if (tools.declined || streamInterrupted || tools.interrupted) { yield* serialized(publisher.failUnsettledTools({ type: "aborted", message: "Tool execution interrupted" })) yield* serialized(publisher.failAssistant({ type: "aborted", message: "Step interrupted" })) } - // A settled tool fiber failure is one of two things. A defect from a tool - // implementation becomes a failed tool call the model can read, and the step still - // settles so the model may recover. A typed infrastructure failure (tool output - // could not be persisted) also fails the assistant and then fails the drain. - const settledFailure = settledCauses.find((cause) => !Cause.hasInterrupts(cause) && !isUserDeclined(cause)) - const infraError = - settledFailure === undefined ? undefined : Option.getOrUndefined(Cause.findErrorOption(settledFailure)) - if (settledFailure !== undefined) { - const failure = infraError ?? Cause.squash(settledFailure) - const error = toSessionError(failure) + if (tools.failure !== undefined) { + const error = toSessionError(tools.infraError ?? Cause.squash(tools.failure)) yield* serialized(publisher.failUnsettledTools(error)) - if (infraError !== undefined) yield* serialized(publisher.failAssistant(error)) + if (tools.infraError !== undefined) yield* serialized(publisher.failAssistant(error)) } // Fail unresolved calls before the terminal step event. Local calls have joined, so @@ -338,63 +394,16 @@ const layer = Layer.effect( } if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause) - if (userDeclined) return yield* Effect.interrupt - if ((toolsInterrupted || infraError !== undefined) && settledFailure) - return yield* Effect.failCause(settledFailure) - if (toolsInterrupted && settled._tag === "Failure") return yield* Effect.failCause(settled.cause) + if (tools.declined) return yield* Effect.interrupt + if ((tools.interrupted || tools.infraError !== undefined) && tools.failure) + return yield* Effect.failCause(tools.failure) + if (tools.interrupted && settled._tag === "Failure") return yield* Effect.failCause(settled.cause) if (stepFailure) return yield* new StepFailedError({ error: stepFailure }) return CallOutcome.Completed({ needsContinuation, step: currentStep }) }), ) }, Effect.scoped) - /** Completes one logical model step, transparently retrying or rebuilding after compaction. */ - const runStep = Effect.fnUntraced(function* ( - sessionID: SessionSchema.ID, - promotable: SessionPending.Promotable, - step: number, - ) { - const retry = yield* Schedule.toStepWithSleep(SessionRunnerRetry.schedule(events, sessionID)) - /** - * Consumes one retry allowance: sleeps the scheduled backoff and reports what the next - * attempt should reuse, or publishes Step.Failed and fails once attempts are exhausted. - * The step loop performs the retry itself on the next iteration. - */ - const waitForRetry = (failure: SessionRunnerRetry.RetryableFailure) => - retry(failure).pipe( - Effect.as(CallOutcome.Retry({ step: failure.step, assistantMessageID: failure.assistantMessageID })), - Pull.catchDone(() => - events - .publish(SessionEvent.Step.Failed, { - sessionID, - assistantMessageID: failure.assistantMessageID, - error: failure.error, - }) - .pipe(Effect.andThen(Effect.fail(failure.cause))), - ), - ) - let currentPromotable: SessionPending.Promotable | undefined = promotable - let currentStep = step - let assistantMessageID: SessionMessage.ID | undefined - // Overflow recovery is one-shot: a call after recovery must not recover another overflow. - let recoverOverflow = true - while (true) { - const outcome = yield* callModel( - sessionID, - currentPromotable, - currentStep, - recoverOverflow, - assistantMessageID, - ).pipe(Effect.catchTag("SessionRunner.RetryableFailure", waitForRetry)) - if (outcome._tag === "Completed") return { needsContinuation: outcome.needsContinuation, step: outcome.step } - if (outcome._tag === "Retry") assistantMessageID = outcome.assistantMessageID - if (outcome._tag === "Restart" && outcome.recoveredOverflow) recoverOverflow = false - // Neither a retry nor a compaction restart re-promotes input. - currentPromotable = undefined - currentStep = outcome.step - } - }) - /** Executes a previously admitted manual compaction request, if one is pending. */ const runPendingCompaction = Effect.fn("SessionRunner.runPendingCompaction")(function* ( sessionID: SessionSchema.ID, @@ -429,39 +438,36 @@ const layer = Layer.effect( ) }) - /** - * Runs logical steps until no tool result or newly admitted steer requires another - * model call. Queued inputs remain pending until the current model work reaches idle. - */ - const runSteps = Effect.fn("SessionRunner.runSteps")(function* (sessionID: SessionSchema.ID) { - // Fresh work may promote queued input; later steps absorb steers only. - let promotable: SessionPending.Promotable = "input" - let step = 1 - while (true) { - const result = yield* runStep(sessionID, promotable, step) - yield* startTitleOnce(sessionID) - yield* runPendingCompaction(sessionID) - if (!result.needsContinuation && !(yield* SessionPending.has(db, sessionID, "steer"))) return - promotable = "steer" - step = result.step + 1 + /** Closes stale tool calls left active by an earlier interrupted drain. */ + const settleStaleToolCalls = Effect.fn("SessionRunner.settleStaleToolCalls")(function* ( + sessionID: SessionSchema.ID, + ) { + for (const message of yield* store.context(sessionID)) { + if (message.type !== "assistant") continue + for (const tool of message.content) { + if (tool.type !== "tool" || (tool.state.status !== "streaming" && tool.state.status !== "running")) continue + yield* events.publish(SessionEvent.Tool.Failed, { + sessionID, + assistantMessageID: message.id, + callID: tool.id, + error: { type: "aborted", message: `Tool execution interrupted: ${tool.name}` }, + executed: tool.executed === true, + }) + } } }) - /** - * Drains eligible manual compaction and user input until the Session becomes idle. - * Execution lifecycle is published per busy period by SessionExecution, not here. - */ - const drain = Effect.fn("SessionRunner.drain")(function* (input: { - readonly sessionID: SessionSchema.ID - readonly force: boolean - }) { - if (!input.force && !(yield* SessionPending.has(db, input.sessionID, "any"))) return - yield* settleStaleToolCalls(input.sessionID) - yield* runPendingCompaction(input.sessionID) - if (!input.force && !(yield* SessionPending.has(db, input.sessionID, "input"))) return - do { - yield* runSteps(input.sessionID) - } while (yield* SessionPending.has(db, input.sessionID, "input")) + /** Fires title generation once per process after the first step makes a user message visible. */ + const startTitleOnce = Effect.fnUntraced(function* (sessionID: SessionSchema.ID) { + if (titleStarted.has(sessionID)) return + titleStarted.add(sessionID) + forkTitle(title.generateForFirstPrompt(yield* getSession(sessionID)).pipe(Effect.ignore)) + }) + + const getSession = Effect.fn("SessionRunner.getSession")(function* (sessionID: SessionSchema.ID) { + const session = yield* store.get(sessionID) + if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`)) + return session }) return Service.of({ drain }) From c64d8133472dabea41491595bb5fb436ae18c859 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:17:37 -0500 Subject: [PATCH 084/150] fix(core): report truncated glob results (#38631) --- packages/core/src/tool/glob.ts | 28 +++++++++++++++++--------- packages/core/test/tool-search.test.ts | 7 +++++-- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/core/src/tool/glob.ts b/packages/core/src/tool/glob.ts index 50ef68539424..3da274fcf10b 100644 --- a/packages/core/src/tool/glob.ts +++ b/packages/core/src/tool/glob.ts @@ -25,11 +25,16 @@ export const Input = Schema.Struct({ }) export const Output = Schema.Array(FileSystem.Entry) -type ModelOutput = typeof Output.Encoded +type EncodedOutput = typeof Output.Encoded /** Format raw search results into the concise line-oriented output models expect. */ -export const toModelOutput = (output: ModelOutput) => { - const lines = output.length === 0 ? ["No files found"] : output.map((item) => item.path) +export const toModelContent = (entries: EncodedOutput, truncated = false) => { + const lines = entries.length === 0 ? ["No files found"] : entries.map((item) => item.path) + if (truncated) + lines.push( + "", + `(Results are truncated: showing first ${entries.length} results. Consider using a more specific path or pattern.)`, + ) return lines.join("\n") } @@ -74,11 +79,12 @@ export const Plugin = { Effect.fail(new ToolFailure({ message: `Search path does not exist: ${input.path ?? "."}` })), ), ) - return yield* ripgrep + const limit = input.limit ?? FileSystem.DEFAULT_SEARCH_LIMIT + const entries = yield* ripgrep .glob({ cwd, pattern: input.pattern, - limit: input.limit ?? FileSystem.DEFAULT_SEARCH_LIMIT, + limit: limit + 1, }) .pipe( Effect.map((result) => @@ -90,13 +96,15 @@ export const Plugin = { ), ), ) + return { entries: entries.slice(0, limit), truncated: entries.length > limit } }).pipe( - Effect.map((output) => ({ - output, - content: toModelOutput( - output.map((entry) => ({ ...entry, path: path.resolve(location.directory, entry.path) })), + Effect.map((result) => ({ + output: result.entries, + content: toModelContent( + result.entries.map((entry) => ({ ...entry, path: path.resolve(location.directory, entry.path) })), + result.truncated, ), - metadata: { count: output.length }, + metadata: { count: result.entries.length, truncated: result.truncated }, })), Effect.mapError((error) => error instanceof ToolFailure diff --git a/packages/core/test/tool-search.test.ts b/packages/core/test/tool-search.test.ts index 9958b0dfb4b7..7f924a94dd14 100644 --- a/packages/core/test/tool-search.test.ts +++ b/packages/core/test/tool-search.test.ts @@ -86,13 +86,16 @@ describe("search tools", () => { const glob = yield* executeTool(registry, call("glob", { pattern: "*" })) const grep = yield* executeTool(registry, call("grep", { pattern: "needle" })) - expect(glob.metadata).toEqual({ count: FileSystem.DEFAULT_SEARCH_LIMIT }) + expect(glob.metadata).toEqual({ count: FileSystem.DEFAULT_SEARCH_LIMIT, truncated: true }) expect(grep.metadata).toEqual({ matches: FileSystem.DEFAULT_SEARCH_LIMIT }) expect(glob.content).toHaveLength(1) expect(grep.content).toHaveLength(1) const globText = glob.content?.[0]?.type === "text" ? glob.content[0].text : "" const grepText = grep.content?.[0]?.type === "text" ? grep.content[0].text : "" - expect(globText.split("\n")).toHaveLength(FileSystem.DEFAULT_SEARCH_LIMIT) + expect(globText.split("\n")).toHaveLength(FileSystem.DEFAULT_SEARCH_LIMIT + 2) + expect(globText).toEndWith( + `(Results are truncated: showing first ${FileSystem.DEFAULT_SEARCH_LIMIT} results. Consider using a more specific path or pattern.)`, + ) expect(grepText).toStartWith(`Found ${FileSystem.DEFAULT_SEARCH_LIMIT} matches\n`) }), ) From 4605308be2bb1d34aa1045812ba46416348ea320 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:20:26 -0500 Subject: [PATCH 085/150] feat(core): render CodeMode catalog deltas from structured snapshots (#38183) --- packages/codemode/README.md | 13 +- packages/codemode/src/codemode.ts | 17 +- packages/codemode/src/index.ts | 1 + packages/codemode/src/tool-runtime.ts | 162 ++------------- packages/codemode/test/codemode.test.ts | 184 +----------------- packages/codemode/test/signature.test.ts | 20 +- packages/codemode/test/tool-paths.test.ts | 29 ++- packages/core/src/codemode.ts | 5 +- packages/core/src/codemode/catalog.ts | 103 ++++++++++ packages/core/src/codemode/instructions.ts | 145 ++++++++++++-- packages/core/src/session/context.ts | 2 +- packages/core/src/tool/execute.ts | 4 +- packages/core/src/tool/registry.ts | 9 +- packages/core/test/codemode.test.ts | 9 +- packages/core/test/codemode/catalog.test.ts | 183 +++++++++++++++++ .../core/test/codemode/instructions.test.ts | 69 ++++--- packages/core/test/session-generate.test.ts | 16 +- .../test/session-runner-tool-registry.test.ts | 2 +- packages/core/test/session-runner.test.ts | 23 ++- 19 files changed, 565 insertions(+), 431 deletions(-) create mode 100644 packages/core/src/codemode/catalog.ts create mode 100644 packages/core/test/codemode/catalog.test.ts diff --git a/packages/codemode/README.md b/packages/codemode/README.md index 923da8d441d7..b43831903735 100644 --- a/packages/codemode/README.md +++ b/packages/codemode/README.md @@ -72,7 +72,6 @@ Dots in tool names create namespaces: `{ "issues.list": tool }` and `{ issues: { const runtime = CodeMode.make({ tools, limits: { timeoutMs: 30_000 } }) runtime.catalog() // structured tool descriptions -runtime.instructions() // model-facing syntax and tool guide runtime.execute(source) // Effect ``` @@ -145,13 +144,13 @@ safe refusal to the model; its optional cause remains private. ## Discovery -Generated instructions contain a tool catalog with a default budget of 2,000 estimated tokens. Configure it with -`discovery: { catalogBudget }`. Every namespace remains visible, and the instructions say whether the catalog is -complete or partial. +`runtime.catalog()` returns structured descriptors — exact path, description, and generated TypeScript signature — for +every visible tool. Hosts render their own model-facing instructions from these descriptors; `CodeMode.searchSignature` +and `CodeMode.toolExpression(path)` supply the exact callable forms. -The synchronous `search(...)` built-in is always available and advertised when the catalog is partial. It supports -exact-path lookup, namespace-scoped search, empty-query browsing, and pagination, and returns callable paths with full -signatures. Search counts toward `maxToolCalls`. +The synchronous `search(...)` built-in is always available. It supports exact-path lookup, namespace-scoped search, +empty-query browsing, and pagination, and returns callable paths with full signatures. Search counts toward +`maxToolCalls`. ## Execution Limits diff --git a/packages/codemode/src/codemode.ts b/packages/codemode/src/codemode.ts index 1f99c01e7412..786b8b4f4ea8 100644 --- a/packages/codemode/src/codemode.ts +++ b/packages/codemode/src/codemode.ts @@ -5,6 +5,8 @@ import type { Tools } from "./tools.js" /** A tool call admitted during an execution. */ export type { ToolCall, ToolCallEnded, ToolCallHooks, ToolCallStarted, ToolDescription } from "./tool-runtime.js" +/** Signature-construction helpers for host-owned catalog instructions. */ +export { searchSignature, toolExpression } from "./tool-runtime.js" /** Resource budgets enforced independently during each CodeMode program execution. */ export type ExecutionLimits = { @@ -22,12 +24,6 @@ export type ExecutionLimits = { readonly maxOutputBytes?: number } -/** Controls how much of the tool catalog is inlined in agent instructions. */ -export type DiscoveryOptions = { - /** Approximate token budget (chars/4, default 2000) for full catalog entries. */ - readonly catalogBudget?: number -} - export type ResolvedExecutionLimits = { readonly timeoutMs: number | undefined readonly maxToolCalls: number | undefined @@ -52,10 +48,7 @@ export type ExecuteOptions = {}> = { export type DataValue = Schema.Json /** Configuration shared by `CodeMode.make` and `CodeMode.execute`. */ -export type Options = {}> = Omit, "code"> & { - /** Progressive-disclosure configuration for the agent-facing tool catalog. */ - readonly discovery?: DiscoveryOptions -} +export type Options = {}> = Omit, "code"> /** Schema for a host tool input containing CodeMode source. */ export const Input = Schema.Struct({ code: Schema.String }) @@ -116,7 +109,6 @@ export type Result = typeof Result.Type /** Reusable confined runtime over explicit tools. */ export type Runtime = { readonly catalog: () => ReadonlyArray - readonly instructions: () => string readonly execute: (code: string) => Effect.Effect } @@ -147,11 +139,10 @@ export const make = = {}>( ): Runtime> => { const tools = (options.tools ?? {}) as Tools> const limits = resolveExecutionLimits(options.limits) - const prepared = ToolRuntime.prepare(tools, options.discovery?.catalogBudget) + const prepared = ToolRuntime.prepare(tools) return { catalog: () => prepared.catalog, - instructions: () => prepared.instructions, execute: (code) => executeWithLimits({ ...options, code }, limits, prepared.searchIndex), } } diff --git a/packages/codemode/src/index.ts b/packages/codemode/src/index.ts index 92019825004b..fe43be2290d6 100644 --- a/packages/codemode/src/index.ts +++ b/packages/codemode/src/index.ts @@ -1,4 +1,5 @@ export * as CodeMode from "./codemode.js" export * as Tool from "./tool.js" export * as OpenAPI from "./openapi/index.js" +export { searchSignature, toolExpression } from "./codemode.js" export { ToolError, toolError } from "./tool-error.js" diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index 053bbee62251..e39c30e38a93 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -20,7 +20,6 @@ import { CodeModeURLSearchParams, } from "./values.js" -const estimateTokens = (input: string) => Math.max(0, Math.round(input.length / 4)) const compareText = (left: string, right: string) => (left < right ? -1 : left > right ? 1 : 0) export type Services = ServicesOf @@ -70,7 +69,6 @@ export type ToolDescription = { export type SafeObject = Record -const defaultCatalogBudget = 2_000 const defaultSearchLimit = 10 const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0)) const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) @@ -90,7 +88,7 @@ const SearchOutput = Schema.Struct({ remaining: NonNegativeInt, next: Schema.NullOr(Schema.Struct({ offset: NonNegativeInt })), }) -const toolExpression = (path: string) => +export const toolExpression = (path: string) => "tools" + path .split(".") @@ -339,7 +337,6 @@ const visibleTools = (tools: Tools) => export type DiscoveryPlan = { readonly catalog: ReadonlyArray - readonly instructions: string readonly searchIndex: ReadonlyArray } @@ -423,17 +420,12 @@ const makeSearchTool = (searchIndex: ReadonlyArray): Tool => ({ }), }) -const searchSignature = (() => { +/** Exact callable signature of the built-in `search` function, for host-owned instructions. */ +export const searchSignature = (() => { const tool = makeSearchTool([]) return `search(input: ${inputTypeScript(tool, true)}): ${outputTypeScript(tool, true)}` })() -const catalogLine = (tool: ToolDescription) => { - const line = tool.description.split("\n", 1)[0]!.trim() - const description = line.length > 120 ? line.slice(0, 119) + "..." : line - return description === "" ? ` - ${tool.signature}` : ` - ${tool.signature} // ${description}` -} - const toSearchEntry = (path: string, tool: Tool, description: ToolDescription): SearchEntry => ({ description, namespace: path.split(".", 1)[0]!, @@ -451,146 +443,10 @@ const toSearchEntry = (path: string, tool: Tool, description: ToolDescript export const searchIndex = (tools: Tools): ReadonlyArray => visibleTools(tools).map(({ path, tool, description }) => toSearchEntry(path, tool, description)) -// Budget signatures round-robin so every namespace remains visible. -export const prepare = (tools: Tools, catalogBudget = defaultCatalogBudget): DiscoveryPlan => { - if (!Number.isSafeInteger(catalogBudget) || catalogBudget < 0) { - throw new RangeError("discovery.catalogBudget must be a non-negative safe integer") - } +export const prepare = (tools: Tools): DiscoveryPlan => { const visible = visibleTools(tools) - const described = visible.map(({ description }) => description) - - const namespaces = new Map>() - for (const tool of described) { - const [namespace = tool.path] = tool.path.split(".") - const group = namespaces.get(namespace) ?? [] - group.push(tool) - namespaces.set(namespace, group) - } - const ordered = [...namespaces].sort(([left], [right]) => compareText(left, right)) - - const selections = ordered.map(([namespace, group]) => ({ - namespace, - picked: new Set(), - queue: [...group].sort( - (left, right) => - estimateTokens(catalogLine(left)) - estimateTokens(catalogLine(right)) || compareText(left.path, right.path), - ), - })) - let used = 0 - let active = selections.filter((selection) => selection.queue.length > 0) - while (active.length > 0) { - const stillActive: typeof active = [] - for (const selection of active) { - const tool = selection.queue[0]! - const cost = estimateTokens(catalogLine(tool)) - if (used + cost > catalogBudget) continue - selection.queue.shift() - selection.picked.add(tool) - used += cost - if (selection.queue.length > 0) stillActive.push(selection) - } - active = stillActive - } - const shown = new Map>( - selections.map(({ namespace, picked }) => [namespace, picked]), - ) - const totalShown = selections.reduce((total, { picked }) => total + picked.size, 0) - const complete = totalShown === described.length - - const empty = described.length === 0 - - const intro = [ - empty - ? "This is a restricted JavaScript language for calling tools, not a general-purpose runtime." - : complete - ? "This is a restricted JavaScript language for calling tools, not a general-purpose runtime. Inside the confined interpreter, `tools` contains the tools listed below; surrounding agent tools are not available." - : "This is a restricted JavaScript language for calling tools, not a general-purpose runtime. Inside the confined interpreter, `tools` contains the tools listed or searchable below; surrounding agent tools are not available.", - ...(empty - ? [] - : ["Do not infer or normalize tool names; use only exact signatures shown below or returned by search."]), - ] - - const workflow = empty - ? [] - : [ - "", - "## Workflow", - "", - ...(complete - ? [ - "1. Pick a tool from the list under `## Available tools` - each line is the exact call signature; use it as-is rather than guessing segments.", - "2. Call it using the exact signature shown: `const result = await tools..(input)`; bracket notation and quotes are part of the path.", - "3. Return only the fields you need from structured results; narrow unknown results before reading fields, and avoid returning large raw payloads.", - ] - : [ - '1. If needed, discover tools with the built-in search function: `return search({ query: "" })`.', - "2. In the next execution, copy a returned path exactly, call it, and return only the needed fields.", - ]), - ] - - const rules = empty - ? [] - : [ - "", - "## Rules", - "", - complete - ? "- Only tools listed here are available; surrounding agent tools are not implicitly exposed." - : "- Only tools listed here or returned by the built-in `search` function are available; surrounding agent tools are not implicitly exposed.", - "- Filter, aggregate, and transform collections in code - never return them raw or call a tool per item across messages.", - "- A result typed `Promise` may be structured data or text. Before reading fields, check that it is a non-null object and not an array; otherwise handle the returned text or primitive directly.", - '- Run independent calls in parallel: `await Promise.all(items.map((item) => tools..(item)))`, or use `tools.["tool-name"](item)` when the listed signature uses bracket notation.', - "- Execution ends when the program returns; pending promises are interrupted, so await every call whose completion matters.", - "- `Object.keys(tools)` lists namespaces; `Object.keys(tools.)` lists its tools; `for...in` works on both.", - ...(complete - ? [] - : [ - '- Browse one namespace: `search({ query: "", namespace: "" })`.', - "- If search returns `next`, repeat the same search with `offset: next.offset`.", - ]), - ] - - const language = [ - "", - "## Language", - "", - "Use common JavaScript data operations, functions, control flow, selected standard-library methods, and awaited tool calls. Built-ins include Date, RegExp, Map, Set, URL, URLSearchParams, and URI encoding helpers.", - "Modules/imports, classes, timers, fetch, eval, prototype access, and unlisted methods are unavailable. Use tools for external operations. Use await with try/catch.", - "Prefer explicit `return`; otherwise only the final top-level expression becomes the result.", - "Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.", - ] - - const toolSection: Array = [""] - if (empty) { - toolSection.push("## Available tools", "", "No tools are currently available.") - } else { - toolSection.push( - complete - ? "## Available tools (COMPLETE list - every tool is shown below with its full call signature)" - : `## Available tools (PARTIAL - ${totalShown} of ${described.length} shown; find the rest with search(...))`, - "", - ) - for (const [namespace, group] of ordered) { - const picked = shown.get(namespace)! - const count = `${group.length} tool${group.length === 1 ? "" : "s"}` - const label = - picked.size === group.length - ? count - : picked.size === 0 - ? `${count}, none shown` - : `${count}, ${picked.size} shown` - toolSection.push(`- ${namespace} (${label})`) - for (const tool of group) if (picked.has(tool)) toolSection.push(catalogLine(tool)) - } - if (!complete) { - toolSection.push("", "Search returns complete callable signatures:", `- ${searchSignature}`) - } - } - - const lines = [...intro, ...workflow, ...rules, ...language, ...toolSection] return { - catalog: described, - instructions: lines.join("\n"), + catalog: visible.map(({ description }) => description), searchIndex: visible.map(({ path, tool, description }) => toSearchEntry(path, tool, description)), } } @@ -612,7 +468,7 @@ const resolve = (root: ToolNode, path: ReadonlyArray): Tool => const node = lookup(root, segments) if (node === undefined) { throw new ToolRuntimeError("UnknownTool", `Unknown tool '${segments.join(".")}'.`, [ - "Use search({ query }) to find available described tools.", + "The tool may have been removed or renamed. Use search to find available tools.", ]) } if (node.tool === undefined) { @@ -685,7 +541,11 @@ export const make = ( const input = yield* Effect.try({ try: () => decodeToolInput(tool, externalArgs[0]), catch: (cause) => - new ToolRuntimeError("InvalidToolInput", `Invalid input for tool '${name}': ${String(cause)}`), + new ToolRuntimeError( + "InvalidToolInput", + `Invalid input for tool '${name}': ${String(cause)}`, + name === "search" ? [] : ["The signature may have changed. Use search to get the current signature."], + ), }) const index = yield* recordAndObserve(name, input) return yield* observeEnd( diff --git a/packages/codemode/test/codemode.test.ts b/packages/codemode/test/codemode.test.ts index 2d83f4bcb4c8..86b1e076f5e8 100644 --- a/packages/codemode/test/codemode.test.ts +++ b/packages/codemode/test/codemode.test.ts @@ -592,7 +592,7 @@ describe("CodeMode public contract", () => { expect(second).toStrictEqual({ ok: true, value: 1, logs: ["hi"], toolCalls: [{ name: "host.echo" }] }) }) - test("inlines a COMPLETE small catalog and keeps search registered but unadvertised", async () => { + test("describes the catalog and keeps the search built-in registered", async () => { const runtime = CodeMode.make({ tools }) expect(runtime.catalog()).toStrictEqual([ { @@ -601,16 +601,7 @@ describe("CodeMode public contract", () => { signature: "tools.orders.lookup(input: {\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}>", }, ]) - expect(runtime.instructions()).toContain("Available tools (COMPLETE list") - expect(runtime.instructions()).toContain("- orders (1 tool)") - expect(runtime.instructions()).toContain( - " - tools.orders.lookup(input: {\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}> // Look up an order by ID", - ) - // A fully inlined catalog does not advertise search in the instructions... - expect(runtime.instructions()).not.toContain("search(") - // ...but the search built-in stays available, so a speculative call still works with the - // same signature as the inline catalog. const result = await Effect.runPromise(runtime.execute(`return search({ query: "order" })`)) expect(result.ok).toBe(true) if (result.ok) { @@ -646,22 +637,7 @@ describe("CodeMode public contract", () => { const second = CodeMode.make({ tools: { alpha: { alpha, zeta }, zeta: { alpha, zeta } } }) expect(first.catalog()).toStrictEqual(second.catalog()) - expect(first.instructions()).toBe(second.instructions()) expect(first.catalog().map((tool) => tool.path)).toEqual(["alpha.alpha", "alpha.zeta", "zeta.alpha", "zeta.zeta"]) - - for (const catalogBudget of [0, 10, 20, 40]) { - expect( - CodeMode.make({ - tools: { zeta: { zeta, alpha }, alpha: { zeta, alpha } }, - discovery: { catalogBudget }, - }).instructions(), - ).toBe( - CodeMode.make({ - tools: { alpha: { alpha, zeta }, zeta: { alpha, zeta } }, - discovery: { catalogBudget }, - }).instructions(), - ) - } }) test("renders bracket notation for tool names that are not JavaScript identifiers", async () => { @@ -680,9 +656,6 @@ describe("CodeMode public contract", () => { signature: 'tools.context7["resolve-library-id"](input: {\n libraryName: string,\n}): Promise', }, ]) - expect(runtime.instructions()).toContain( - 'tools.context7["resolve-library-id"](input: {\n libraryName: string,\n}): Promise', - ) const search = await Effect.runPromise(runtime.execute(`return search({ query: "resolve library id" })`)) expect(search.ok).toBe(true) @@ -713,88 +686,6 @@ describe("CodeMode public contract", () => { if (exact.ok) expect(exact.value).toMatchObject({ remaining: 0, next: null }) }) - test("instructions use markdown sections with placeholder-only call forms", () => { - const runtime = CodeMode.make({ tools }) - const instructions = runtime.instructions() - // Sections in order: workflow at the top, catalog at the bottom. - expect(instructions).toContain("## Workflow") - expect(instructions).toContain("## Rules") - expect(instructions).toContain("## Language") - expect(instructions.indexOf("## Workflow")).toBeLessThan(instructions.indexOf("## Rules")) - expect(instructions.indexOf("## Rules")).toBeLessThan(instructions.indexOf("## Language")) - expect(instructions.indexOf("## Language")).toBeLessThan( - instructions.indexOf("\n## Available tools (COMPLETE list"), - ) - expect(instructions).not.toContain("JSON.parse(res)") - expect(instructions).toContain("Return only the fields you need") - expect(instructions).toContain("avoid returning large raw payloads") - expect(instructions).toContain("Do not infer or normalize tool names") - expect(instructions).toContain("bracket notation and quotes are part of the path") - expect(instructions).toContain("surrounding agent tools are not available") - expect(instructions).toContain("Only tools listed here are available") - // Placeholders use generic namespace/tool/field names only - no fabricated real tools - // and no real catalog tools cherry-picked into example lines. - expect(instructions).toContain("`const result = await tools..(input)`") - expect(instructions).toContain("Return only the fields you need from structured results") - expect(instructions).toContain("check that it is a non-null object and not an array") - expect(instructions).not.toContain("result.") - expect(instructions).not.toContain("data.") - expect(instructions).not.toContain("total_count") - expect(instructions).not.toContain("list_issues") - expect(instructions).not.toContain("tools.orders.lookup({") - // COMPLETE: step 1 picks from the inlined list; search is not advertised. - expect(instructions).toContain("1. Pick a tool from the list under `## Available tools`") - expect(instructions).not.toContain("Browse one namespace") - - const partial = CodeMode.make({ tools, discovery: { catalogBudget: 0 } }).instructions() - // PARTIAL: the workflow starts with search (with query-style guidance that is clearly - // a query string, never a tool name) and the browse-namespace rule appears. - expect(partial).toContain( - '1. If needed, discover tools with the built-in search function: `return search({ query: "" })`.', - ) - expect(partial).toContain("In the next execution, copy a returned path exactly") - expect(partial).toContain("Only tools listed here or returned by the built-in `search` function") - expect(partial).toContain('- Browse one namespace: `search({ query: "", namespace: "" })`.') - expect(partial).toContain("repeat the same search with `offset: next.offset`") - expect(partial).toContain(" limit?: number,\n offset?: number,") - expect(partial).not.toContain("total_count") - expect(partial).not.toContain("tools.orders.lookup({") - }) - - test("the language section describes the restricted runtime without overclaiming", () => { - const instructions = CodeMode.make({ tools }).instructions() - expect(instructions).toContain("restricted JavaScript language for calling tools") - expect(instructions).toContain("not a general-purpose runtime") - expect(instructions).not.toContain("Standard modern JavaScript works") - expect(instructions).not.toContain("TypeScript type annotations") - for (const missing of ["Modules/imports", "classes", "fetch"]) { - expect(instructions).toContain(missing) - } - expect(instructions).not.toContain("generators") - expect(instructions).not.toContain("new Promise(...) are unavailable") - expect(instructions).not.toContain("promise chaining") - expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers") - expect(instructions).not.toContain("host globals") - expect(instructions).toContain("Use tools for external operations") - expect(instructions).toContain( - "Prefer explicit `return`; otherwise only the final top-level expression becomes the result.", - ) - expect(instructions).toContain( - "Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.", - ) - }) - - test("zero tools keep minimal sections and the no-tools notice", () => { - const runtime = CodeMode.make({}) - const instructions = runtime.instructions() - expect(instructions).toContain("No tools are currently available.") - expect(instructions).toContain("## Language") - expect(instructions).toContain("## Available tools") - expect(instructions).not.toContain("## Workflow") - expect(instructions).not.toContain("## Rules") - expect(instructions).not.toContain("search(") - }) - test("uses one ranked search returning complete tools for large catalogs", async () => { const upload = Tool.make({ description: "Upload one readable local file to the current Discord thread", @@ -810,13 +701,7 @@ describe("CodeMode public contract", () => { }) const runtime = CodeMode.make({ tools: { thread: { uploadFile: upload, generateImage: generate }, orders: { lookup } }, - discovery: { catalogBudget: 0 }, }) - expect(runtime.instructions()).toContain("Available tools (PARTIAL - 0 of 3 shown; find the rest with search(...))") - expect(runtime.instructions()).toContain("- thread (2 tools, none shown)") - expect(runtime.instructions()).toContain("- orders (1 tool, none shown)") - expect(runtime.instructions()).toContain("Search returns complete callable signatures:\n- search(input: {") - expect(runtime.instructions()).not.toMatch(/tools\.thread\.uploadFile\(input/) const result = await Effect.runPromise( runtime.execute(` @@ -1101,64 +986,6 @@ describe("CodeMode public contract", () => { if (exhausted.ok) expect(exhausted.value).toStrictEqual({ items: [], remaining: 0, next: null }) }) - test("inlines round-robin across namespaces so one expensive namespace cannot starve the rest", () => { - const cheap = Tool.make({ - description: "Cheap", - input: Schema.Struct({ q: Schema.String }), - output: Schema.String, - execute: () => Effect.succeed("ok"), - }) - const expensive = Tool.make({ - description: - "An expensive tool whose description alone consumes far more than the remaining inline catalog byte budget for this runtime", - input: Schema.Struct({ - someRatherLongParameterName: Schema.String, - anotherEvenLongerParameterName: Schema.Number, - }), - output: Schema.String, - execute: () => Effect.succeed("ok"), - }) - // Round 1 places alpha.cheap (~17 estimated tokens) and beta.cheap (~17); in round 2 - // alpha.expensive does not fit, which marks only alpha done - it must NOT prevent - // other namespaces from inlining (beta already got its line in the same round). - const runtime = CodeMode.make({ - tools: { alpha: { cheap, expensive }, beta: { cheap } }, - discovery: { catalogBudget: 40 }, - }) - - const instructions = runtime.instructions() - expect(instructions).toContain("Available tools (PARTIAL - 2 of 3 shown; find the rest with search(...))") - expect(instructions).toContain("- alpha (2 tools, 1 shown)") - expect(instructions).toContain(" - tools.alpha.cheap(input: {\n q: string,\n}): Promise // Cheap") - expect(instructions).not.toContain("tools.alpha.expensive(") - // Fully shown namespaces read cleanly (no "shown" annotation). - expect(instructions).toContain("- beta (1 tool)") - expect(instructions).toContain(" - tools.beta.cheap(input: {\n q: string,\n}): Promise // Cheap") - expect(instructions).toContain("Search returns complete callable signatures:\n- search(input: {") - }) - - test("charges inline JSDoc against the catalog token budget", () => { - const documented = Tool.make({ - description: "Look up a record", - input: { - type: "object", - properties: { - id: { type: "string", description: "A detailed identifier description. ".repeat(20) }, - }, - required: ["id"], - } as const, - execute: () => Effect.succeed("ok"), - }) - const runtime = CodeMode.make({ - tools: { records: { lookup: documented } }, - discovery: { catalogBudget: 40 }, - }) - - expect(runtime.catalog()[0]?.signature).toContain("/** A detailed identifier description.") - expect(runtime.instructions()).toContain("Available tools (PARTIAL - 0 of 1 shown; find the rest with search(...))") - expect(runtime.instructions()).not.toContain("tools.records.lookup(input:") - }) - test("decodes tool input and output before exposing either side", async () => { const observed: Array = [] const transformed = Tool.make({ @@ -1219,7 +1046,7 @@ describe("CodeMode public contract", () => { expect(result).toStrictEqual({ ok: true, value: null, toolCalls: [] }) }) - test("rejects invalid configuration and discovery limits", async () => { + test("rejects invalid configuration and search limits", async () => { expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: 0 } })).toThrow(RangeError) expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: Number.POSITIVE_INFINITY } })).toThrow( RangeError, @@ -1227,13 +1054,8 @@ describe("CodeMode public contract", () => { expect(() => CodeMode.execute({ code: "return 1", limits: { maxToolCalls: -1 } })).toThrow(RangeError) expect(() => CodeMode.execute({ code: "return 1", limits: { maxOutputBytes: -1 } })).toThrow(RangeError) - expect(() => CodeMode.make({ tools, discovery: { catalogBudget: -1 } })).toThrow(RangeError) - const result = await Effect.runPromise( - CodeMode.make({ - tools, - discovery: { catalogBudget: 0 }, - }).execute(`return search({ query: "order", limit: 0.5 })`), + CodeMode.make({ tools }).execute(`return search({ query: "order", limit: 0.5 })`), ) expect(result.ok).toBe(false) if (result.ok) return diff --git a/packages/codemode/test/signature.test.ts b/packages/codemode/test/signature.test.ts index 38121e5c6ef0..4c29281d47f1 100644 --- a/packages/codemode/test/signature.test.ts +++ b/packages/codemode/test/signature.test.ts @@ -395,15 +395,15 @@ describe("JSDoc signatures in catalogs and search results", () => { } }) - test("the inline catalog uses the same JSDoc signatures", async () => { - const instructions = runtime.instructions() + test("the catalog uses the same JSDoc signatures as search", async () => { + const catalog = runtime.catalog() const github = (await search("list issues repository")).items.find( ({ path }) => path === "tools.github.list_issues", )! const orders = (await search("look up order")).items.find(({ path }) => path === "tools.orders.lookup")! - expect(instructions).toContain(` - ${github.signature} // List issues in a repository`) - expect(instructions).toContain(` - ${orders.signature} // Look up an order`) - expect(instructions).toContain("/** Repository owner */") + expect(catalog.map(({ signature }) => signature)).toContain(github.signature) + expect(catalog.map(({ signature }) => signature)).toContain(orders.signature) + expect(github.signature).toContain("/** Repository owner */") }) }) @@ -423,16 +423,10 @@ describe("non-identifier tool paths", () => { }) const runtime = CodeMode.make({ tools: { context7: { "resolve-library-id": resolveLibrary } } }) - test("inline catalog uses bracket notation for dashed tool names", () => { - const instructions = runtime.instructions() - - expect(instructions).toContain( + test("catalog signatures use bracket notation for dashed tool names", () => { + expect(runtime.catalog()[0]?.signature).toBe( 'tools.context7["resolve-library-id"](input: {\n query: string,\n libraryName: string,\n}): Promise', ) - expect(instructions).toContain("Do not infer or normalize tool names") - expect(instructions).toContain("bracket notation and quotes are part of the path") - expect(instructions).not.toContain("tools.context7.resolve-library-id") - expect(instructions).not.toContain("tools.context7.resolve_library_id") }) test("search results return callable bracket-notation paths and signatures", async () => { diff --git a/packages/codemode/test/tool-paths.test.ts b/packages/codemode/test/tool-paths.test.ts index 359cb7b9c6b6..3cc92d6dfcdb 100644 --- a/packages/codemode/test/tool-paths.test.ts +++ b/packages/codemode/test/tool-paths.test.ts @@ -30,7 +30,6 @@ describe("dotted tool names", () => { expect(catalog).toHaveLength(1) expect(catalog[0]?.path).toBe("api.issues.list") expect(catalog[0]?.signature).toStartWith("tools.api.issues.list(input:") - expect(runtime.instructions()).toContain("tools.api.issues.list(input:") }) test("the advertised dotted path is executable", async () => { @@ -86,6 +85,9 @@ describe("callable namespaces", () => { const diagnostic = await failure(runtime, `return await tools.issues.missing({})`) expect(diagnostic.kind).toBe("UnknownTool") expect(diagnostic.message).toContain("Unknown tool 'issues.missing'") + expect(diagnostic.suggestions).toEqual([ + "The tool may have been removed or renamed. Use search to find available tools.", + ]) }) test("a namespace without its own tool stays non-callable", async () => { @@ -96,6 +98,31 @@ describe("callable namespaces", () => { }) }) +describe("tool input diagnostics", () => { + const runtime = CodeMode.make({ + tools: { + "notes.echo": Tool.make({ + description: "Echo text", + input: Schema.Struct({ text: Schema.String }), + output: Schema.String, + execute: ({ text }) => Effect.succeed(text), + }), + }, + }) + + test("a schema mismatch suggests searching for the current signature", async () => { + const diagnostic = await failure(runtime, `return await tools.notes.echo({ message: "hello" })`) + expect(diagnostic.kind).toBe("InvalidToolInput") + expect(diagnostic.suggestions).toEqual(["The signature may have changed. Use search to get the current signature."]) + }) + + test("a wrong argument count keeps the existing error without a stale-signature hint", async () => { + const diagnostic = await failure(runtime, `return await tools.notes.echo()`) + expect(diagnostic.kind).toBe("InvalidToolInput") + expect(diagnostic.suggestions).toBeUndefined() + }) +}) + describe("blocked member names on tool paths", () => { const runtime = CodeMode.make({ tools: { diff --git a/packages/core/src/codemode.ts b/packages/core/src/codemode.ts index 7d2dd2c57085..20dcea1d427d 100644 --- a/packages/core/src/codemode.ts +++ b/packages/core/src/codemode.ts @@ -1,6 +1,7 @@ export * as CodeMode from "./codemode" import { Context, Effect, Layer, Scope } from "effect" +import { CodeModeCatalog } from "./codemode/catalog" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { PermissionV2 } from "./permission" import { ExecuteTool } from "./tool/execute" @@ -9,7 +10,7 @@ import { Wildcard } from "./util/wildcard" export interface Materialization { readonly tool?: Any - readonly instructions?: string + readonly catalog?: ReadonlyArray } export interface Interface { @@ -67,7 +68,7 @@ const layer = Layer.effect( if (executeRule?.resource === "*" && executeRule.effect === "deny") return {} return { tool: ExecuteTool.create(registrations), - instructions: ExecuteTool.instructions(registrations), + catalog: ExecuteTool.catalog(registrations), } }), }) diff --git a/packages/core/src/codemode/catalog.ts b/packages/core/src/codemode/catalog.ts new file mode 100644 index 000000000000..eafc1568949e --- /dev/null +++ b/packages/core/src/codemode/catalog.ts @@ -0,0 +1,103 @@ +export * as CodeModeCatalog from "./catalog" + +import { Schema } from "effect" + +export const Entry = Schema.Struct({ + path: Schema.String, + description: Schema.String, + signature: Schema.String, +}) +export type Entry = typeof Entry.Type + +const Listing = Schema.Struct({ + path: Schema.String, + line: Schema.String, +}) + +const Namespace = Schema.Struct({ + name: Schema.String, + count: Schema.Number, + entries: Schema.Array(Listing), +}) + +export const Summary = Schema.Struct({ + total: Schema.Number, + shown: Schema.Number, + namespaces: Schema.Array(Namespace), +}) +export type Summary = typeof Summary.Type + +const DESCRIPTION_LIMIT = 120 +const CHARACTERS_PER_TOKEN = 4 +const INLINE_BUDGET = 2_000 + +// Keep every namespace searchable, then select full listings one per namespace per round, +// considering shorter listings first until the inline budget is exhausted. +export function summarize(entries: ReadonlyArray, budget = INLINE_BUDGET): Summary { + const namespaces = [...Map.groupBy(entries, (entry) => entry.path.split(".", 1)[0] ?? entry.path)] + .sort(([left], [right]) => { + if (left < right) return -1 + if (left > right) return 1 + return 0 + }) + .map(([name, namespaceEntries]) => { + const listings = namespaceEntries + .map((entry) => { + const firstLine = entry.description.split("\n", 1)[0]?.trim() ?? "" + const description = + firstLine.length > DESCRIPTION_LIMIT + ? firstLine.slice(0, DESCRIPTION_LIMIT - 3) + "..." + : firstLine + const suffix = description.length === 0 ? "" : ` // ${description}` + return { path: entry.path, line: ` - ${entry.signature}${suffix}` } + }) + .toSorted((left, right) => { + if (left.path < right.path) return -1 + if (left.path > right.path) return 1 + return 0 + }) + return { + name, + listings, + selectionOrder: rankListings(listings), + selectedListings: new Set(), + } + }) + + const active = new Set(namespaces) + let remaining = budget + while (active.size > 0) { + for (const namespace of active) { + const candidate = namespace.selectionOrder[namespace.selectedListings.size] + if (!candidate || candidate.cost > remaining) { + active.delete(namespace) + continue + } + namespace.selectedListings.add(candidate.listing) + remaining -= candidate.cost + if (namespace.selectedListings.size === namespace.selectionOrder.length) active.delete(namespace) + } + } + + const namespaceSummaries = namespaces.map((namespace) => ({ + name: namespace.name, + count: namespace.listings.length, + entries: namespace.listings.filter((listing) => namespace.selectedListings.has(listing)), + })) + return { + total: entries.length, + shown: namespaceSummaries.reduce((total, namespace) => total + namespace.entries.length, 0), + namespaces: namespaceSummaries, + } +} + +function rankListings(listings: ReadonlyArray) { + return listings + .map((listing) => ({ listing, cost: Math.round(listing.line.length / CHARACTERS_PER_TOKEN) })) + .toSorted((left, right) => { + if (left.cost !== right.cost) return left.cost - right.cost + if (left.listing.path < right.listing.path) return -1 + if (left.listing.path > right.listing.path) return 1 + return 0 + }) +} diff --git a/packages/core/src/codemode/instructions.ts b/packages/core/src/codemode/instructions.ts index 0934a4a953a8..66e5150dc536 100644 --- a/packages/core/src/codemode/instructions.ts +++ b/packages/core/src/codemode/instructions.ts @@ -1,24 +1,141 @@ export * as CodeModeInstructions from "./instructions" +import { searchSignature, toolExpression } from "@opencode-ai/codemode" import { Effect, Schema } from "effect" import { Instructions } from "../instructions/index" +import { CodeModeCatalog } from "./catalog" -const key = Instructions.Key.make("core/codemode") -const codec = Schema.toCodecJson(Schema.String) -const render = { - initial: (current: string) => current, - changed: (_previous: string, current: string) => - [ - "The Code Mode tool catalog has changed. This catalog supersedes the previous Code Mode tool catalog.", - current, - ].join("\n\n"), - removed: () => "Code Mode tools are no longer available. Do not use any previously listed Code Mode tools.", +// prettier-ignore +const prompt = (hasMoreTools: boolean) => `Run JavaScript to orchestrate tool calls and compose their results. Imports, direct filesystem access, and timers are unavailable. Do not use \`fetch\`; all external access goes through \`tools\`. + +Prefer an explicit \`return\`; if omitted, the final top-level expression becomes the result. Await tool calls before returning; any calls still pending when execution ends are interrupted. Run independent calls concurrently with \`Promise.all\`. + +Do not infer or normalize tool names; use only the exact signatures shown below${hasMoreTools ? " or returned by `search`" : ""}, preserving bracket notation such as \`tools.["tool-name"](input)\`.${hasMoreTools ? ` + +## Search + +Only some tool signatures are shown. Use \`search\` to discover exact paths and signatures for additional tools: + +- ${searchSignature}` : ""} + +## Available tools` + +export function render(catalog: CodeModeCatalog.Summary) { + if (catalog.total === 0) return "No tools are currently available." + + const tools = catalog.namespaces.flatMap((namespace) => { + const count = namespace.count === 1 ? "1 tool" : `${namespace.count} tools` + const label = + namespace.entries.length === namespace.count + ? count + : namespace.entries.length === 0 + ? `${count}, none shown` + : `${count}, ${namespace.entries.length} shown` + return [`- ${namespace.name} (${label})`, ...namespace.entries.map((entry) => entry.line)] + }) + + return `${prompt(catalog.shown < catalog.total)} + +${tools.join("\n")}` } -export const make = (content?: string): Instructions.Instructions => - Instructions.make({ +export function update(previous: CodeModeCatalog.Summary, current: CodeModeCatalog.Summary) { + const full = `The Code Mode tool catalog has changed. This catalog supersedes the previous Code Mode tool catalog. + +${render(current)}` + const previousComplete = previous.shown === previous.total + const currentComplete = current.shown === current.total + if (previousComplete !== currentComplete) return full + + const diff = Instructions.diffByKey( + previous.namespaces.flatMap((namespace) => namespace.entries), + current.namespaces.flatMap((namespace) => namespace.entries), + (entry) => entry.path, + (before, after) => before.line !== after.line, + ) + const entriesChanged = diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0 + + if (!currentComplete) { + if (entriesChanged) return full + const namespaces = Instructions.diffByKey( + previous.namespaces, + current.namespaces, + (namespace) => namespace.name, + (before, after) => before.count !== after.count, + ) + const changed = namespaces.added.length > 0 || namespaces.removed.length > 0 || namespaces.changed.length > 0 + if (!changed) return full + + const parts = ["The Code Mode tool catalog has changed."] + if (namespaces.added.length > 0) { + parts.push( + `New tool namespaces are available: ${namespaces.added + .map((namespace) => `\`${namespace.name}\` (${namespace.count} tools)`) + .join(", ")}.`, + ) + } + if (namespaces.changed.length > 0) { + parts.push( + `The following namespace inventories changed; search them again before relying on previous results: ${namespaces.changed + .map((change) => `\`${change.current.name}\` now has ${change.current.count} tools`) + .join(", ")}.`, + ) + } + if (namespaces.removed.length > 0) { + parts.push( + `The following tool namespaces are no longer available and must not be used: ${namespaces.removed + .map((namespace) => `\`${namespace.name}\``) + .join(", ")}.`, + ) + } + const delta = parts.join("\n\n") + if (delta.length < full.length) return delta + return full + } + + if (!entriesChanged) return full + const parts = ["The Code Mode tool catalog has changed."] + if (diff.added.length > 0) { + parts.push( + [ + "New tools are available in addition to those previously listed:", + ...diff.added.map((entry) => entry.line), + ].join("\n"), + ) + } + if (diff.changed.length > 0) { + parts.push( + [ + "Changed tool listings supersede the previously listed ones:", + ...diff.changed.map((change) => change.current.line), + ].join("\n"), + ) + } + if (diff.removed.length > 0) { + parts.push( + `The following tools are no longer available and must not be called: ${diff.removed + .map((entry) => toolExpression(entry.path)) + .join(", ")}.`, + ) + } + const delta = parts.join("\n\n") + if (delta.length < full.length) return delta + return full +} + +const key = Instructions.Key.make("core/codemode") +const codec = Schema.toCodecJson(CodeModeCatalog.Summary) + +export const make = (entries?: ReadonlyArray): Instructions.Instructions => { + const catalog = CodeModeCatalog.summarize(entries ?? []) + return Instructions.make({ key, codec, - read: Effect.succeed(content ?? Instructions.removed), - render, + read: Effect.succeed(catalog.total === 0 ? Instructions.removed : catalog), + render: { + initial: render, + changed: update, + removed: () => "Code Mode tools are no longer available. Do not use any previously listed Code Mode tools.", + }, }) +} diff --git a/packages/core/src/session/context.ts b/packages/core/src/session/context.ts index be3d9bb08f86..c2c41e329984 100644 --- a/packages/core/src/session/context.ts +++ b/packages/core/src/session/context.ts @@ -97,7 +97,7 @@ const layer = Layer.effect( agent: { ...agent, info: agent.info }, instructions: Instructions.combine([ loaded.builtins, - CodeModeInstructions.make(loaded.toolSet.codeModeInstructions), + CodeModeInstructions.make(loaded.toolSet.codeModeCatalog), loaded.discovery, loaded.skills, loaded.references, diff --git a/packages/core/src/tool/execute.ts b/packages/core/src/tool/execute.ts index 30ca66712cf0..a9d005af4567 100644 --- a/packages/core/src/tool/execute.ts +++ b/packages/core/src/tool/execute.ts @@ -126,8 +126,8 @@ export const create = (registrations: ReadonlyMap) => { }) } -export const instructions = (registrations: ReadonlyMap) => { - return runtime(registrations, () => Effect.fail(toolError("Execute context is unavailable"))).instructions() +export const catalog = (registrations: ReadonlyMap) => { + return runtime(registrations, () => Effect.fail(toolError("Execute context is unavailable"))).catalog() } function runtime( diff --git a/packages/core/src/tool/registry.ts b/packages/core/src/tool/registry.ts index c572541a948e..c5e3d1014b57 100644 --- a/packages/core/src/tool/registry.ts +++ b/packages/core/src/tool/registry.ts @@ -3,6 +3,7 @@ export * as ToolRegistry from "./registry" import { type ToolCall, type ToolContent, type ToolDefinition } from "@opencode-ai/ai" import { Context, Effect, Layer, Schema, Scope, Semaphore } from "effect" import type { AgentV2 } from "../agent" +import { CodeModeCatalog } from "../codemode/catalog" import { Image } from "../image" import { PermissionV2 } from "../permission" import { SessionMessage } from "../session/message" @@ -44,13 +45,13 @@ export interface Interface { } /** - * One request-scoped snapshot pairing Code Mode instructions and advertised + * One request-scoped snapshot pairing the Code Mode catalog and advertised * definitions with captured tools. A model request executes exactly the tool * values it advertised even if registration changes while it is in flight. */ export interface ToolSet { readonly definitions: ReadonlyArray - readonly codeModeInstructions?: string + readonly codeModeCatalog?: ReadonlyArray readonly execute: (input: ExecuteInput) => Effect.Effect } @@ -324,9 +325,9 @@ const registryLayer = Layer.effect( const codeModeMaterialization = yield* codeMode.materialize(permissions) const codemodeTool = codeModeMaterialization.tool return { - ...(codeModeMaterialization.instructions === undefined + ...(codeModeMaterialization.catalog === undefined ? {} - : { codeModeInstructions: codeModeMaterialization.instructions }), + : { codeModeCatalog: codeModeMaterialization.catalog }), definitions: [ // Definitions are prompt-cache prefix bytes, so order only after effective registrations settle. ...Array.from(direct) diff --git a/packages/core/test/codemode.test.ts b/packages/core/test/codemode.test.ts index b80eb1a9855b..9d9f8ccfd3c2 100644 --- a/packages/core/test/codemode.test.ts +++ b/packages/core/test/codemode.test.ts @@ -22,8 +22,13 @@ describe("CodeMode", () => { const materialized = yield* codeMode.materialize() expect(materialized.tool).toBeDefined() - expect(materialized.instructions).toContain("Echo text") - expect(materialized.instructions).toContain("tools.echo(input:") + expect(materialized.catalog).toStrictEqual([ + { + path: "echo", + description: "Echo text", + signature: "tools.echo(input: {\n text: string,\n}): Promise", + }, + ]) }).pipe(Effect.scoped, Effect.provide(AppNodeBuilder.build(CodeMode.node))), ) }) diff --git a/packages/core/test/codemode/catalog.test.ts b/packages/core/test/codemode/catalog.test.ts new file mode 100644 index 000000000000..176fe1c6350e --- /dev/null +++ b/packages/core/test/codemode/catalog.test.ts @@ -0,0 +1,183 @@ +import { describe, expect, test } from "bun:test" +import { CodeModeCatalog } from "@opencode-ai/core/codemode/catalog" +import { CodeModeInstructions } from "@opencode-ai/core/codemode/instructions" + +const entry = (path: string, description: string, signature?: string): CodeModeCatalog.Entry => ({ + path, + description, + signature: signature ?? `tools.${path}(input: {\n q: string,\n}): Promise`, +}) + +const lookup = entry( + "orders.lookup", + "Look up an order by ID", + "tools.orders.lookup(input: {\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}>", +) + +const render = (entries: ReadonlyArray, budget?: number) => + CodeModeInstructions.render(CodeModeCatalog.summarize(entries, budget)) + +const update = ( + previous: ReadonlyArray, + current: ReadonlyArray, + budget?: number, +) => + CodeModeInstructions.update(CodeModeCatalog.summarize(previous, budget), CodeModeCatalog.summarize(current, budget)) + +describe("CodeModeCatalog.summarize", () => { + test("retains namespace inventory without retaining tools outside the inline budget", () => { + const catalog = CodeModeCatalog.summarize( + Array.from({ length: 10_000 }, (_, index) => entry(`bulk.tool${index}`, `Tool ${index}`)), + 0, + ) + expect(catalog).toEqual({ + total: 10_000, + shown: 0, + namespaces: [{ name: "bulk", count: 10_000, entries: [] }], + }) + }) + + test("retains every namespace when no full tool listing fits", () => { + const catalog = CodeModeCatalog.summarize( + [entry("alpha.one", "One"), entry("beta.two", "Two"), entry("gamma.three", "Three")], + 0, + ) + expect(catalog.namespaces.map((namespace) => namespace.name)).toEqual(["alpha", "beta", "gamma"]) + expect(catalog.namespaces.every((namespace) => namespace.entries.length === 0)).toBe(true) + }) + + test("retains only the rendered portion of inline descriptions", () => { + const catalog = CodeModeCatalog.summarize([entry("alpha.one", `Summary\n${"detail".repeat(10_000)}`)]) + expect(catalog.namespaces[0]?.entries[0]?.line).toEndWith("// Summary") + }) + + test("limits inline descriptions to 120 characters", () => { + const catalog = CodeModeCatalog.summarize([entry("alpha.one", "x".repeat(121))]) + const description = catalog.namespaces[0]?.entries[0]?.line.split(" // ")[1] + expect(description).toHaveLength(120) + expect(description).toEndWith("...") + }) +}) + +describe("CodeModeInstructions.render", () => { + test("inlines complete catalogs without search guidance", () => { + const instructions = render([lookup]) + expect(instructions).toContain("## Available tools") + expect(instructions).toContain("- orders (1 tool)") + expect(instructions).toContain(` - ${lookup.signature} // Look up an order by ID`) + expect(instructions).not.toContain("## Search") + expect(instructions).toContain("Do not infer or normalize tool names") + expect(instructions).toContain('`tools.["tool-name"](input)`') + }) + + test("describes the runtime and execution lifecycle concisely", () => { + const instructions = render([lookup]) + expect(instructions).toContain("Run JavaScript to orchestrate tool calls and compose their results.") + expect(instructions).toContain("Imports, direct filesystem access, and timers are unavailable.") + expect(instructions).toContain("Do not use `fetch`; all external access goes through `tools`.") + expect(instructions).toContain( + "Prefer an explicit `return`; if omitted, the final top-level expression becomes the result.", + ) + expect(instructions).toContain("any calls still pending when execution ends are interrupted") + expect(instructions).toContain("Run independent calls concurrently with `Promise.all`.") + }) + + test("adds search guidance when the catalog exceeds the budget", () => { + const partial = render([lookup], 0) + expect(partial).toContain("## Available tools") + expect(partial).toContain("- orders (1 tool, none shown)") + expect(partial).toContain("## Search") + expect(partial).toContain("Only some tool signatures are shown.") + expect(partial).toContain("- search(input: {") + expect(partial).toContain(" limit?: number,\n offset?: number,") + expect(partial).toContain("or returned by `search`") + expect(partial).not.toContain("tools.orders.lookup(input:") + }) + + test("budgets signatures round-robin so every namespace remains visible", () => { + const cheapAlpha = entry("alpha.cheap", "Cheap") + const cheapBeta = entry("beta.cheap", "Cheap") + const expensive = entry( + "alpha.expensive", + "Expensive", + `tools.alpha.expensive(input: {\n aVeryLongParameterName: string,\n anotherEvenLongerParameterName: number,\n yetAnotherExtremelyVerboseParameterName: string,\n}): Promise`, + ) + // Round 1 places alpha.cheap and beta.cheap; in round 2 alpha.expensive does not fit, + // which marks only alpha done - it must NOT prevent other namespaces from inlining. + const instructions = render([cheapAlpha, expensive, cheapBeta], 40) + expect(instructions).toContain("## Search") + expect(instructions).toContain("- alpha (2 tools, 1 shown)") + expect(instructions).toContain(` - ${cheapAlpha.signature} // Cheap`) + expect(instructions).not.toContain("tools.alpha.expensive(") + expect(instructions).toContain("- beta (1 tool)") + expect(instructions).toContain(` - ${cheapBeta.signature} // Cheap`) + }) + + test("charges inline JSDoc in signatures against the catalog token budget", () => { + const documented = entry( + "records.lookup", + "Look up a record", + `tools.records.lookup(input: {\n /** ${"A detailed identifier description. ".repeat(20).trim()} */\n id: string,\n}): Promise`, + ) + const instructions = render([documented], 40) + expect(instructions).toContain("- records (1 tool, none shown)") + expect(instructions).not.toContain("tools.records.lookup(input:") + }) + + test("renders only the no-tools notice for an empty catalog", () => { + expect(render([])).toBe("No tools are currently available.") + }) +}) + +describe("CodeModeInstructions.update", () => { + const echo = entry("notes.echo", "Echo text") + + test("renders additions, changes, and removals as a compact semantic delta", () => { + const changed = { ...echo, signature: "tools.notes.echo(input: {\n text: string,\n}): Promise" } + const added = entry("notes.list", "List notes") + const text = update([echo, lookup], [changed, added]) + expect(text).toContain("The Code Mode tool catalog has changed.") + expect(text).toContain(`New tools are available in addition to those previously listed:\n - ${added.signature}`) + expect(text).toContain( + `Changed tool listings supersede the previously listed ones:\n - ${changed.signature} // Echo text`, + ) + expect(text).toContain("The following tools are no longer available and must not be called: tools.orders.lookup.") + expect(text).not.toContain("## Available tools") + }) + + test("names removed tools with exact callable expressions including bracket notation", () => { + const dashed = entry("context7.resolve-library-id", "Resolve a library ID") + const text = update([echo, dashed], [echo]) + expect(text).toContain( + 'The following tools are no longer available and must not be called: tools.context7["resolve-library-id"].', + ) + }) + + test("restates the full catalog when the rendering mode crosses full and compact", () => { + const wide = Array.from({ length: 40 }, (_, index) => entry(`bulk.tool${index}`, `Tool ${index}`)) + const text = update([echo], [echo, ...wide], 30) + expect(text).toContain( + "The Code Mode tool catalog has changed. This catalog supersedes the previous Code Mode tool catalog.", + ) + expect(text).toContain("## Search") + expect(text).toContain("## Available tools") + }) + + test("falls back to full replacement when the delta is larger than the catalog", () => { + const previous = Array.from({ length: 200 }, (_, index) => entry(`bulk.tool${index}`, `Tool ${index}`)) + const text = update([...previous, echo], [echo]) + expect(text).toContain("This catalog supersedes the previous Code Mode tool catalog.") + expect(text).toContain("## Available tools") + expect(text).not.toContain("## Search") + expect(text).not.toContain("must not be called") + }) + + test("renders namespace-only deltas without persisting hidden tool entries", () => { + const alpha = Array.from({ length: 10 }, (_, index) => entry(`alpha.tool${index}`, `Tool ${index}`)) + const text = update(alpha, [...alpha, entry("alpha.tool10", "Tool 10")], 0) + expect(text).toContain("`alpha` now has 11 tools") + expect(text).toContain("search them again before relying on previous results") + expect(text).not.toContain("tools.alpha.tool10(input:") + expect(text).not.toContain("## Available tools") + }) +}) diff --git a/packages/core/test/codemode/instructions.test.ts b/packages/core/test/codemode/instructions.test.ts index 6c8bc7907691..953445508948 100644 --- a/packages/core/test/codemode/instructions.test.ts +++ b/packages/core/test/codemode/instructions.test.ts @@ -1,5 +1,6 @@ import { describe, expect } from "bun:test" import { CodeMode } from "@opencode-ai/core/codemode" +import { CodeModeCatalog } from "@opencode-ai/core/codemode/catalog" import { CodeModeInstructions } from "@opencode-ai/core/codemode/instructions" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Tool } from "@opencode-ai/core/tool/tool" @@ -7,8 +8,45 @@ import { Effect, Schema } from "effect" import { it } from "../lib/effect" import { readInitial, readUpdate } from "../lib/instructions" +const echo: CodeModeCatalog.Entry = { + path: "notes.echo", + description: "Echo text", + signature: "tools.notes.echo(input: {\n text: string,\n}): Promise", +} + +const lookup: CodeModeCatalog.Entry = { + path: "orders.lookup", + description: "Look up an order", + signature: "tools.orders.lookup(input: {\n id: string,\n}): Promise", +} + describe("CodeModeInstructions", () => { - it.effect("treats equivalent registration orders as an instruction no-op", () => { + it.effect("renders the initial catalog, semantic deltas, and removal", () => + Effect.gen(function* () { + const initialized = yield* readInitial(CodeModeInstructions.make([echo])) + expect(initialized.text).toContain("## Available tools") + expect(initialized.text).not.toContain("## Search") + expect(initialized.text).toContain(` - ${echo.signature} // Echo text`) + + const added = yield* readUpdate(CodeModeInstructions.make([echo, lookup]), initialized) + expect(added.text).toContain("The Code Mode tool catalog has changed.") + expect(added.text).toContain("New tools are available in addition to those previously listed:") + expect(added.text).toContain(` - ${lookup.signature} // Look up an order`) + expect(added.text).not.toContain("## Available tools") + + const removed = yield* readUpdate(CodeModeInstructions.make([echo]), { values: added.values }) + expect(removed.text).toBe( + "The Code Mode tool catalog has changed.\n\n" + + "The following tools are no longer available and must not be called: tools.orders.lookup.", + ) + + expect(yield* readUpdate(CodeModeInstructions.make(), initialized)).toMatchObject({ + text: "Code Mode tools are no longer available. Do not use any previously listed Code Mode tools.", + }) + }), + ) + + it.effect("stores a canonical sorted snapshot so registration order does not churn history", () => { const alpha = Tool.make({ description: "Alpha tool", input: Schema.Struct({}), @@ -21,46 +59,25 @@ describe("CodeModeInstructions", () => { output: Schema.String, execute: () => Effect.succeed({ output: "zeta" }), }) - const codeModeLayer = AppNodeBuilder.build(CodeMode.node) + const layer = AppNodeBuilder.build(CodeMode.node) return Effect.gen(function* () { const codeMode = yield* CodeMode.Service const initialized = yield* Effect.scoped( Effect.gen(function* () { yield* codeMode.register(Tool.registrationEntries({ zeta, alpha }, { namespace: "tools" })) - const materialization = yield* codeMode.materialize() - return yield* readInitial(CodeModeInstructions.make(materialization.instructions)) + return yield* readInitial(CodeModeInstructions.make((yield* codeMode.materialize()).catalog)) }), ) const reordered = yield* Effect.scoped( Effect.gen(function* () { yield* codeMode.register(Tool.registrationEntries({ alpha, zeta }, { namespace: "tools" })) - const materialization = yield* codeMode.materialize() - return yield* readUpdate(CodeModeInstructions.make(materialization.instructions), initialized) + return yield* readUpdate(CodeModeInstructions.make((yield* codeMode.materialize()).catalog), initialized) }), ) expect(reordered.changed).toBe(false) expect(reordered.text).toBe("") - }).pipe(Effect.provide(codeModeLayer)) - }) - - it.effect("renders catalog changes and removal", () => { - let catalog: string | undefined = "Initial Code Mode catalog" - - return Effect.gen(function* () { - const initialized = yield* readInitial(CodeModeInstructions.make(catalog)) - expect(initialized.text).toBe("Initial Code Mode catalog") - - catalog = "Updated Code Mode catalog" - expect(yield* readUpdate(CodeModeInstructions.make(catalog), initialized)).toMatchObject({ - text: "The Code Mode tool catalog has changed. This catalog supersedes the previous Code Mode tool catalog.\n\nUpdated Code Mode catalog", - }) - - catalog = undefined - expect(yield* readUpdate(CodeModeInstructions.make(catalog), initialized)).toMatchObject({ - text: "Code Mode tools are no longer available. Do not use any previously listed Code Mode tools.", - }) - }) + }).pipe(Effect.provide(layer)) }) }) diff --git a/packages/core/test/session-generate.test.ts b/packages/core/test/session-generate.test.ts index 20b0ca07fe36..a6910f826f70 100644 --- a/packages/core/test/session-generate.test.ts +++ b/packages/core/test/session-generate.test.ts @@ -59,7 +59,11 @@ const client = Layer.mock(LLMClient.Service)({ LLMEvent.textStart({ id: "generate" }), LLMEvent.textDelta({ id: "generate", text: "Transient answer" }), LLMEvent.textEnd({ id: "generate" }), - LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" }, usage: { inputTokens: 100, outputTokens: 10 } }), + LLMEvent.stepFinish({ + index: 0, + reason: { normalized: "stop" }, + usage: { inputTokens: 100, outputTokens: 10 }, + }), LLMEvent.finish({ reason: { normalized: "stop" } }), ]) if (!response) throw new Error("Incomplete generate response") @@ -97,7 +101,13 @@ const plugins = Layer.mock(PluginSupervisor.Service, { flush: Effect.void }) const tools = Layer.mock(ToolRegistry.Service, { snapshot: () => Effect.succeed({ - codeModeInstructions: "Captured Code Mode catalog", + codeModeCatalog: [ + { + path: "captured.lookup", + description: "Captured Code Mode catalog", + signature: "tools.captured.lookup(input: {}): Promise", + }, + ], definitions: [ToolDefinition.make({ name: "lookup", description: "Lookup", inputSchema: { type: "object" } })], execute: () => Effect.die(new Error("unused")), }), @@ -293,7 +303,7 @@ it.effect("generates from fresh settled Session context without durable mutation ) expect(instructionUpdates).toHaveLength(1) expect(instructionUpdates?.[0]).toContain("Changed context") - expect(instructionUpdates?.[0]).toContain("Captured Code Mode catalog") + expect(instructionUpdates?.[0]).toContain("tools.captured.lookup(input: {}): Promise") expect(userTexts(requests[0])).toEqual(["Existing durable context", "Summarize privately"]) expect( requests[0]?.messages.flatMap((message) => diff --git a/packages/core/test/session-runner-tool-registry.test.ts b/packages/core/test/session-runner-tool-registry.test.ts index 7ba1322002d3..57e494783d91 100644 --- a/packages/core/test/session-runner-tool-registry.test.ts +++ b/packages/core/test/session-runner-tool-registry.test.ts @@ -533,7 +533,7 @@ describe("ToolRegistry", () => { .pipe(Scope.provide(scope)) const toolSet = yield* service.snapshot() const execute = toolSet.definitions.find((tool) => tool.name === "execute") - expect(toolSet.codeModeInstructions).toContain("tools.echo") + expect(toolSet.codeModeCatalog?.[0]?.signature).toContain("tools.echo") expect(execute?.description).toContain("confined Code Mode runtime") expect(execute?.description).not.toContain("Echo text") yield* Scope.close(scope, Exit.void) diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index a6f6d0c43e94..b4a937b8b510 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -844,12 +844,19 @@ describe("SessionRunnerLLM", () => { output: Schema.String, execute: () => Effect.sync(() => executed.push(name)).pipe(Effect.as({ output: name })), }) + const catalog = (name: string) => [ + { + path: `catalog.${name.toLowerCase()}`, + description: `Code Mode catalog ${name}`, + signature: `tools.catalog.${name.toLowerCase()}(input: {}): Promise`, + }, + ] const session = yield* setup codeModeMaterializations = [ - { instructions: "Code Mode catalog A", tool: execute("A") }, - { instructions: "Code Mode catalog B", tool: execute("B") }, - { instructions: "Code Mode catalog C", tool: execute("C") }, - { instructions: "Code Mode catalog D", tool: execute("D") }, + { catalog: catalog("A"), tool: execute("A") }, + { catalog: catalog("B"), tool: execute("B") }, + { catalog: catalog("C"), tool: execute("C") }, + { catalog: catalog("D"), tool: execute("D") }, ] yield* admit(session, "Use Code Mode") responses = [reply.tool("call-execute", "execute", {}), reply.stop()] @@ -1036,9 +1043,7 @@ describe("SessionRunnerLLM", () => { input: Schema.Struct({}), output: Schema.Struct({ value: Schema.String }), execute: () => - Effect.sync(() => executions.push("advertised")).pipe( - Effect.as({ output: { value: "advertised" } }), - ), + Effect.sync(() => executions.push("advertised")).pipe(Effect.as({ output: { value: "advertised" } })), }), }, { codemode: false }, @@ -1067,9 +1072,7 @@ describe("SessionRunnerLLM", () => { input: Schema.Struct({}), output: Schema.Struct({ value: Schema.String }), execute: () => - Effect.sync(() => executions.push("replacement")).pipe( - Effect.as({ output: { value: "replacement" } }), - ), + Effect.sync(() => executions.push("replacement")).pipe(Effect.as({ output: { value: "replacement" } })), }), }, { codemode: false }, From 5aa276c117a94a368868931eed967001c3aaa2f9 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 24 Jul 2026 12:11:31 -0400 Subject: [PATCH 086/150] refactor(core): mint assistant message identity before the step runs (#38717) --- packages/core/src/session/runner/llm.ts | 33 ++++++++++++------- .../src/session/runner/publish-llm-event.ts | 11 +++---- packages/core/src/session/runner/retry.ts | 5 ++- .../test/session-runner-tool-events.test.ts | 1 + 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 94d5c7580eec..7c96a1853d89 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -30,7 +30,7 @@ import { SessionUsage } from "../usage" /** How one model call ended: settled, awaiting a scheduled retry, or restarted by compaction. */ type CallOutcome = Data.TaggedEnum<{ Completed: { readonly needsContinuation: boolean; readonly step: number } - Retry: { readonly step: number; readonly assistantMessageID: SessionMessage.ID } + Retry: { readonly step: number } Restart: { readonly step: number; readonly recoveredOverflow: boolean } }> const CallOutcome = Data.taggedEnum() @@ -120,20 +120,26 @@ const layer = Layer.effect( promotable: SessionPending.Promotable, step: number, ) { - const retry = yield* Schedule.toStepWithSleep(SessionRunnerRetry.schedule(events, sessionID)) + // Minting message identity before any attempt lets retries resume the same durable + // message. A compaction restart re-mints: the old message is stranded behind the new + // compaction boundary, so the rebuilt step needs identity inside the new epoch. + let assistantMessageID = SessionMessage.ID.create() + const retry = yield* Schedule.toStepWithSleep( + SessionRunnerRetry.schedule(events, sessionID, () => assistantMessageID), + ) /** - * Consumes one retry allowance: sleeps the scheduled backoff and reports what the next - * attempt should reuse, or publishes Step.Failed and fails once attempts are exhausted. - * The step loop performs the retry itself on the next iteration. + * Consumes one retry allowance: sleeps the scheduled backoff, or publishes + * Step.Failed and fails once attempts are exhausted. The step loop performs + * the retry itself on the next iteration. */ const waitForRetry = (failure: SessionRunnerRetry.RetryableFailure) => retry(failure).pipe( - Effect.as(CallOutcome.Retry({ step: failure.step, assistantMessageID: failure.assistantMessageID })), + Effect.as(CallOutcome.Retry({ step: failure.step })), Pull.catchDone(() => events .publish(SessionEvent.Step.Failed, { sessionID, - assistantMessageID: failure.assistantMessageID, + assistantMessageID, error: failure.error, }) .pipe(Effect.andThen(Effect.fail(failure.cause))), @@ -141,7 +147,6 @@ const layer = Layer.effect( ) let currentPromotable: SessionPending.Promotable | undefined = promotable let currentStep = step - let assistantMessageID: SessionMessage.ID | undefined // Overflow recovery is one-shot: a call after recovery must not recover another overflow. let recoverOverflow = true while (true) { @@ -153,8 +158,10 @@ const layer = Layer.effect( assistantMessageID, ).pipe(Effect.catchTag("SessionRunner.RetryableFailure", waitForRetry)) if (outcome._tag === "Completed") return { needsContinuation: outcome.needsContinuation, step: outcome.step } - if (outcome._tag === "Retry") assistantMessageID = outcome.assistantMessageID - if (outcome._tag === "Restart" && outcome.recoveredOverflow) recoverOverflow = false + if (outcome._tag === "Restart") { + if (outcome.recoveredOverflow) recoverOverflow = false + assistantMessageID = SessionMessage.ID.create() + } // Neither a retry nor a compaction restart re-promotes input. currentPromotable = undefined currentStep = outcome.step @@ -170,7 +177,7 @@ const layer = Layer.effect( promotable: SessionPending.Promotable | undefined, step: number, recoverOverflow: boolean, - assistantMessageID?: SessionMessage.ID, + assistantMessageID: SessionMessage.ID, ) { const selected = yield* context.select(sessionID) // Establish what the model knows before admitting what the user said, so @@ -318,9 +325,11 @@ const layer = Layer.effect( if (llmFailure && !publisher.hasProviderError()) { const error = toSessionError(llmFailure) if (SessionRunnerRetry.isRetryable(llmFailure) && !publisher.hasRetryEvidence()) { + // RetryScheduled and Step.Failed fold onto an existing assistant message, so + // Step.Started must be durable before the failure escapes. + yield* serialized(publisher.startAssistant()) return yield* new SessionRunnerRetry.RetryableFailure({ cause: llmFailure, - assistantMessageID: yield* publisher.startAssistant(), error, step: currentStep, }) diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts index 06fedf6fcf45..bb00c540c657 100644 --- a/packages/core/src/session/runner/publish-llm-event.ts +++ b/packages/core/src/session/runner/publish-llm-event.ts @@ -20,7 +20,7 @@ type Input = { readonly model: ModelV2.Ref readonly providerMetadataKey: string readonly snapshot?: Snapshot.ID - readonly assistantMessageID?: SessionMessage.ID + readonly assistantMessageID: SessionMessage.ID } const record = (value: unknown): Record => @@ -50,7 +50,7 @@ export const createLLMEventPublisher = (events: Pick() const failureSnapshot = (tool: { readonly progress?: ToolRegistry.Progress }) => tool.progress === undefined ? {} : { metadata: tool.progress } - let assistantMessageID = input.assistantMessageID + const assistantMessageID = input.assistantMessageID let stepStarted = false let stepFailed = false let providerFailed = false @@ -64,8 +64,7 @@ export const createLLMEventPublisher = (events: Pick - assistantMessageID === undefined - ? Effect.die(new Error("Tool event before assistant step start")) - : Effect.succeed(assistantMessageID) + stepStarted ? Effect.succeed(assistantMessageID) : Effect.die(new Error("Tool event before assistant step start")) const providerState = (metadata: ProviderMetadata | undefined) => metadata?.[input.providerMetadataKey] const fragments = ( name: string, diff --git a/packages/core/src/session/runner/retry.ts b/packages/core/src/session/runner/retry.ts index 94401a35d68b..de94d36ec6b7 100644 --- a/packages/core/src/session/runner/retry.ts +++ b/packages/core/src/session/runner/retry.ts @@ -10,7 +10,6 @@ import { SessionSchema } from "../schema" export class RetryableFailure extends Data.TaggedError("SessionRunner.RetryableFailure")<{ readonly cause: LLMError - readonly assistantMessageID: SessionMessage.ID readonly error: SessionError.Error readonly step: number }> {} @@ -42,7 +41,7 @@ const retryAfter = (failure: RetryableFailure) => { return undefined } -export const schedule = (events: EventV2.Interface, sessionID: SessionSchema.ID) => +export const schedule = (events: EventV2.Interface, sessionID: SessionSchema.ID, assistantMessageID: () => SessionMessage.ID) => Schedule.max([Schedule.exponential("2 seconds"), Schedule.recurs(4)]).pipe( Schedule.setInputType(), Schedule.modifyDelay(({ input: failure, duration: delay }) => { @@ -52,7 +51,7 @@ export const schedule = (events: EventV2.Interface, sessionID: SessionSchema.ID) Schedule.tap((metadata) => events.publish(SessionEvent.RetryScheduled, { sessionID, - assistantMessageID: metadata.input.assistantMessageID, + assistantMessageID: assistantMessageID(), attempt: metadata.attempt + 1, at: metadata.now + Duration.toMillis(metadata.duration), error: metadata.input.error, diff --git a/packages/core/test/session-runner-tool-events.test.ts b/packages/core/test/session-runner-tool-events.test.ts index 4d6dc3c44007..ac90c1dd4f20 100644 --- a/packages/core/test/session-runner-tool-events.test.ts +++ b/packages/core/test/session-runner-tool-events.test.ts @@ -45,6 +45,7 @@ const capture = (providerMetadataKey = "anthropic", options?: { readonly interru providerID: ProviderV2.ID.opencode, }, providerMetadataKey, + assistantMessageID: SessionMessage.ID.create(), }), } } From 4f201f87a90b1f0fa99cfdc98cae2cea20b37178 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 24 Jul 2026 11:15:23 -0500 Subject: [PATCH 087/150] fix(ai): align Bedrock stream handling (#38712) --- packages/ai/src/protocols/bedrock-converse.ts | 39 ++-- .../ai/src/protocols/bedrock-event-stream.ts | 18 +- .../ai/test/provider/bedrock-converse.test.ts | 170 +++++++++++++++++- 3 files changed, 208 insertions(+), 19 deletions(-) diff --git a/packages/ai/src/protocols/bedrock-converse.ts b/packages/ai/src/protocols/bedrock-converse.ts index 18de26420594..6dda74d4171c 100644 --- a/packages/ai/src/protocols/bedrock-converse.ts +++ b/packages/ai/src/protocols/bedrock-converse.ts @@ -155,6 +155,12 @@ const BedrockUsageSchema = Schema.Struct({ }) type BedrockUsageSchema = Schema.Schema.Type +const BedrockStreamException = Schema.Struct({ + message: Schema.optional(Schema.String), + originalMessage: Schema.optional(Schema.String), + originalStatusCode: Schema.optional(Schema.Number), +}) + // Streaming event shape — the AWS event stream wraps each JSON payload by its // `:event-type` header (e.g. `messageStart`, `contentBlockDelta`). We // reconstruct that wrapping in `decodeFrames` below so the event schema can @@ -184,6 +190,9 @@ const BedrockEvent = Schema.Struct({ signature: Schema.optional(Schema.String), // Blob fields in Bedrock's JSON event stream are base64 strings. redactedContent: Schema.optional(Schema.String), + // Vercel's Bedrock provider exposes the same delta under + // Anthropic's shorter `data` spelling. + data: Schema.optional(Schema.String), }), ), }), @@ -203,11 +212,11 @@ const BedrockEvent = Schema.Struct({ metrics: Schema.optional(Schema.Unknown), }), ), - internalServerException: Schema.optional(Schema.Struct({ message: Schema.String })), - modelStreamErrorException: Schema.optional(Schema.Struct({ message: Schema.String })), - validationException: Schema.optional(Schema.Struct({ message: Schema.String })), - throttlingException: Schema.optional(Schema.Struct({ message: Schema.String })), - serviceUnavailableException: Schema.optional(Schema.Struct({ message: Schema.String })), + internalServerException: Schema.optional(BedrockStreamException), + modelStreamErrorException: Schema.optional(BedrockStreamException), + validationException: Schema.optional(BedrockStreamException), + throttlingException: Schema.optional(BedrockStreamException), + serviceUnavailableException: Schema.optional(BedrockStreamException), }) type BedrockEvent = Schema.Schema.Type @@ -531,14 +540,20 @@ const step = (state: ParserState, event: BedrockEvent) => const index = event.contentBlockDelta.contentBlockIndex const reasoning = event.contentBlockDelta.delta.reasoningContent const events: LLMEvent[] = [] - const lifecycle = reasoning.text - ? Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${index}`, reasoning.text) - : reasoning.redactedContent !== undefined - ? Lifecycle.reasoningStart( + const redactedData = reasoning.redactedContent ?? reasoning.data + const providerMetadata = reasoning.signature + ? bedrockMetadata({ signature: reasoning.signature }) + : redactedData !== undefined + ? bedrockMetadata({ redactedData }) + : undefined + const lifecycle = + reasoning.text !== undefined || providerMetadata !== undefined + ? Lifecycle.reasoningDelta( state.lifecycle, events, `reasoning-${index}`, - bedrockMetadata({ redactedData: reasoning.redactedContent }), + reasoning.text ?? "", + providerMetadata, ) : state.lifecycle return [ @@ -618,7 +633,7 @@ const step = (state: ParserState, event: BedrockEvent) => } if (event.metadata) { - const usage = mapUsage(event.metadata.usage) + const usage = mapUsage(event.metadata.usage) ?? state.pendingFinish?.usage return [ { ...state, @@ -645,7 +660,7 @@ const step = (state: ParserState, event: BedrockEvent) => module: ADAPTER, method: "stream", reason: classifyProviderFailure({ - message: exception[1]?.message ?? "Bedrock Converse stream error", + message: exception[1]?.message ?? exception[1]?.originalMessage ?? "Bedrock Converse stream error", code: exception[0], }), }) diff --git a/packages/ai/src/protocols/bedrock-event-stream.ts b/packages/ai/src/protocols/bedrock-event-stream.ts index 0312ea7d57dc..6d5b58ad8e53 100644 --- a/packages/ai/src/protocols/bedrock-event-stream.ts +++ b/packages/ai/src/protocols/bedrock-event-stream.ts @@ -53,8 +53,22 @@ const consumeFrames = (route: string) => (state: FrameBufferState, chunk: Uint8A }) cursor = { buffer: cursor.buffer, offset: cursor.offset + totalLength } - if (decoded.headers[":message-type"]?.value !== "event") continue - const eventType = decoded.headers[":event-type"]?.value + const messageType = decoded.headers[":message-type"]?.value + if (messageType === "error") { + const code = decoded.headers[":error-code"]?.value + const message = decoded.headers[":error-message"]?.value + return yield* ProviderShared.eventError( + route, + [code, message].filter((value): value is string => typeof value === "string").join(": ") || + "Bedrock Converse event-stream error", + ) + } + const eventType = + messageType === "event" + ? decoded.headers[":event-type"]?.value + : messageType === "exception" + ? decoded.headers[":exception-type"]?.value + : undefined if (typeof eventType !== "string") continue const payload = utf8.decode(decoded.body) if (!payload) continue diff --git a/packages/ai/test/provider/bedrock-converse.test.ts b/packages/ai/test/provider/bedrock-converse.test.ts index 05c0c669fc1f..86f2240488c3 100644 --- a/packages/ai/test/provider/bedrock-converse.test.ts +++ b/packages/ai/test/provider/bedrock-converse.test.ts @@ -43,6 +43,26 @@ const eventFrame = (type: string, payload: object) => body: utf8Encoder.encode(JSON.stringify(payload)), }) +const exceptionFrame = (type: string, payload: object) => + codec.encode({ + headers: { + ":message-type": { type: "string", value: "exception" }, + ":exception-type": { type: "string", value: type }, + ":content-type": { type: "string", value: "application/json" }, + }, + body: utf8Encoder.encode(JSON.stringify(payload)), + }) + +const errorFrame = (code: string, message: string) => + codec.encode({ + headers: { + ":message-type": { type: "string", value: "error" }, + ":error-code": { type: "string", value: code }, + ":error-message": { type: "string", value: message }, + }, + body: new Uint8Array(), + }) + const concat = (frames: ReadonlyArray) => { const total = frames.reduce((sum, frame) => sum + frame.length, 0) const out = new Uint8Array(total) @@ -363,6 +383,19 @@ describe("Bedrock Converse route", () => { }), ) + it.effect("preserves usage across later metadata events without usage", () => + Effect.gen(function* () { + const body = eventStreamBody( + ["messageStop", { stopReason: "end_turn" }], + ["metadata", { usage: { inputTokens: 5, outputTokens: 2, totalTokens: 7 } }], + ["metadata", { metrics: { latencyMs: 100 } }], + ) + const response = yield* LLMClient.generate(baseRequest).pipe(Effect.provide(fixedBytes(body))) + + expect(response.usage).toMatchObject({ inputTokens: 5, outputTokens: 2, totalTokens: 7 }) + }), + ) + it.effect("assembles streamed tool call input", () => Effect.gen(function* () { const body = eventStreamBody( @@ -478,6 +511,95 @@ describe("Bedrock Converse route", () => { }), ) + it.effect("preserves reasoning signatures when contentBlockStop is missing", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(baseRequest).pipe( + Effect.provide( + fixedBytes( + eventStreamBody( + ["messageStart", { role: "assistant" }], + [ + "contentBlockDelta", + { contentBlockIndex: 0, delta: { reasoningContent: { text: "Let me think." } } }, + ], + [ + "contentBlockDelta", + { contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } }, + ], + ["messageStop", { stopReason: "end_turn" }], + ), + ), + ), + ) + + expect(response.events.find((event) => event.type === "reasoning-delta" && event.text === "")).toEqual({ + type: "reasoning-delta", + id: "reasoning-0", + text: "", + providerMetadata: { bedrock: { signature: "sig_1" } }, + }) + expect(response.message.content).toEqual([ + { + type: "reasoning", + text: "Let me think.", + providerMetadata: { bedrock: { signature: "sig_1" } }, + }, + ]) + + const prepared = yield* LLMClient.prepare( + LLM.request({ model, messages: [response.message], cache: "none" }), + ) + expect(prepared.body.messages).toEqual([ + { + role: "assistant", + content: [{ reasoningContent: { reasoningText: { text: "Let me think.", signature: "sig_1" } } }], + }, + ]) + }), + ) + + it.effect("preserves signature-only reasoning blocks", () => + Effect.gen(function* () { + const body = eventStreamBody( + ["messageStart", { role: "assistant" }], + [ + "contentBlockDelta", + { contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } }, + ], + ["contentBlockStop", { contentBlockIndex: 0 }], + ["messageStop", { stopReason: "end_turn" }], + ) + const response = yield* LLMClient.generate(baseRequest).pipe(Effect.provide(fixedBytes(body))) + + expect(response.message.content).toEqual([ + { type: "reasoning", text: "", providerMetadata: { bedrock: { signature: "sig_1" } } }, + ]) + }), + ) + + it.effect("accepts Vercel-compatible redacted reasoning data deltas", () => + Effect.gen(function* () { + const redactedData = "cmVkYWN0ZWQtdGhpbmtpbmc=" + const body = eventStreamBody( + ["messageStart", { role: "assistant" }], + ["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { data: redactedData } } }], + ["contentBlockStop", { contentBlockIndex: 0 }], + ["messageStop", { stopReason: "end_turn" }], + ) + const response = yield* LLMClient.generate(baseRequest).pipe(Effect.provide(fixedBytes(body))) + + expect(response.events.find((event) => event.type === "reasoning-delta" && event.text === "")).toEqual({ + type: "reasoning-delta", + id: "reasoning-0", + text: "", + providerMetadata: { bedrock: { redactedData } }, + }) + expect(response.message.content).toEqual([ + { type: "reasoning", text: "", providerMetadata: { bedrock: { redactedData } } }, + ]) + }), + ) + it.effect("round-trips streamed redacted reasoning with tool use into a continuation request", () => Effect.gen(function* () { // Bedrock represents redactedContent blobs as base64 strings on its JSON @@ -511,6 +633,12 @@ describe("Bedrock Converse route", () => { ), ), ) + expect(response.events.find((event) => event.type === "reasoning-delta" && event.text === "")).toEqual({ + type: "reasoning-delta", + id: "reasoning-0", + text: "", + providerMetadata: { bedrock: { redactedData } }, + }) const prepared = yield* LLMClient.prepare( LLM.request({ model, @@ -543,10 +671,10 @@ describe("Bedrock Converse route", () => { it.effect("classifies throttlingException as a rate limit", () => Effect.gen(function* () { - const body = eventStreamBody( - ["messageStart", { role: "assistant" }], - ["throttlingException", { message: "Slow down" }], - ) + const body = concat([ + eventFrame("messageStart", { role: "assistant" }), + exceptionFrame("throttlingException", { message: "Slow down" }), + ]) const error = yield* LLMClient.generate(baseRequest).pipe(Effect.provide(fixedBytes(body)), Effect.flip) expect(error.reason).toMatchObject({ _tag: "RateLimit", message: "Slow down" }) @@ -557,7 +685,7 @@ describe("Bedrock Converse route", () => { Effect.gen(function* () { const error = yield* LLMClient.generate(baseRequest).pipe( Effect.provide( - fixedBytes(eventStreamBody(["validationException", { message: "Input is too long for requested model" }])), + fixedBytes(exceptionFrame("validationException", { message: "Input is too long for requested model" })), ), Effect.flip, ) @@ -570,6 +698,38 @@ describe("Bedrock Converse route", () => { }), ) + it.effect("uses originalMessage from model stream exception frames", () => + Effect.gen(function* () { + const error = yield* LLMClient.generate(baseRequest).pipe( + Effect.provide( + fixedBytes( + exceptionFrame("modelStreamErrorException", { + originalMessage: "Upstream model failed", + originalStatusCode: 500, + }), + ), + ), + Effect.flip, + ) + + expect(error.reason).toMatchObject({ _tag: "ProviderInternal", message: "Upstream model failed" }) + }), + ) + + it.effect("fails unmodeled AWS event-stream errors", () => + Effect.gen(function* () { + const error = yield* LLMClient.generate(baseRequest).pipe( + Effect.provide(fixedBytes(errorFrame("BadStream", "Stream failed"))), + Effect.flip, + ) + + expect(error.reason).toMatchObject({ + _tag: "InvalidProviderOutput", + message: "BadStream: Stream failed", + }) + }), + ) + it.effect("rejects requests with no auth path", () => Effect.gen(function* () { const unsignedModel = AmazonBedrock.configure({ From 7ab3dd04ad6b224a0e605022bc57a9fc4fe41a59 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 24 Jul 2026 12:55:43 -0400 Subject: [PATCH 088/150] refactor(core): unify tool fiber bookkeeping into one owned structure (#38719) --- packages/core/src/session/runner/llm.ts | 26 +++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 7c96a1853d89..ed358e666668 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -1,6 +1,6 @@ export * as SessionRunnerLLM from "./llm" -import { LLMClient, LLMError, LLMEvent, isContextOverflowFailure, type ProviderErrorEvent } from "@opencode-ai/ai" +import { LLMClient, LLMError, LLMEvent, isContextOverflowFailure, type ProviderErrorEvent, type ToolCall } from "@opencode-ai/ai" import { Cause, Data, Effect, Exit, Fiber, FiberSet, Layer, Option, Pull, Schedule, Semaphore, Stream } from "effect" import { Database } from "../../database/database" import { EventV2 } from "../../event" @@ -203,8 +203,9 @@ const layer = Layer.effect( context: loaded, step: currentStep, }) - const toolFibers = yield* FiberSet.make() - const ownedToolFibers: Array> = [] + // Every local tool call forked here is owned until it reaches one durable settlement. + const toolRuns: Array<{ readonly call: ToolCall; readonly fiber: Fiber.Fiber }> = [] + const interruptTools = Effect.suspend(() => Fiber.interruptAll(toolRuns.map((run) => run.fiber))) let needsContinuation = false const startSnapshot = yield* snapshots.capture() const publisher = createLLMEventPublisher(events, { @@ -277,8 +278,9 @@ const layer = Layer.effect( // continuation depends only on remaining Step allowance. if (!prepared.stepLimitReached) needsContinuation = true const assistantMessageID = yield* publisher.assistantMessageID(event.id) - ownedToolFibers.push( - yield* Effect.uninterruptibleMask((restore) => + toolRuns.push({ + call: event, + fiber: yield* Effect.uninterruptibleMask((restore) => restore( prepared.executeTool({ sessionID: session.id, @@ -290,8 +292,8 @@ const layer = Layer.effect( ).pipe( Effect.flatMap((execution) => serialized(publisher.toolExecution(event.id, event.name, execution))), ), - ).pipe(FiberSet.run(toolFibers)), - ) + ).pipe(Effect.forkScoped), + }) }), ), Effect.ensuring(serialized(publisher.flush())), @@ -339,13 +341,13 @@ const layer = Layer.effect( // Provider error events only arrive from the stream, so the flag is final here. const providerFailed = publisher.hasProviderError() - // Settle every owned tool fiber. FiberSet.join returns on the first failure, so retain - // the individual fibers and await all exits before publishing the terminal step event. - if (streamInterrupted) yield* FiberSet.clear(toolFibers) + // Settle every owned tool run: await all exits, not just the first failure, + // before publishing the terminal step event. + if (streamInterrupted) yield* interruptTools const settled = yield* restore( - Effect.forEach(ownedToolFibers, Fiber.await, { concurrency: "unbounded" }), + Effect.forEach(toolRuns, (run) => Fiber.await(run.fiber), { concurrency: "unbounded" }), ).pipe(Effect.exit) - if (settled._tag === "Failure") yield* FiberSet.clear(toolFibers) + if (settled._tag === "Failure") yield* interruptTools const tools = classifyToolExits(settled) if (tools.declined || streamInterrupted || tools.interrupted) { From 68ef893818d43f248fd72028f7d51d6770cd47a8 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 24 Jul 2026 12:58:50 -0400 Subject: [PATCH 089/150] fix(core): start all suspended sessions promptly (#38720) --- .../core/src/session/execution/restart.ts | 3 +-- packages/core/test/session-execution.test.ts | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/core/src/session/execution/restart.ts b/packages/core/src/session/execution/restart.ts index 985c46a556ea..d7133bb7d25f 100644 --- a/packages/core/src/session/execution/restart.ts +++ b/packages/core/src/session/execution/restart.ts @@ -40,8 +40,7 @@ export const layer = Layer.effect( // Drain failures are already logged and durably recorded by the execution layer. yield* Effect.ignore(execution.resume(sessionID)) }), - // Each suspension is consumed atomically right before its drain; at most four drains run at once. - { concurrency: 4, discard: true }, + { concurrency: "unbounded", discard: true }, ) }), }) diff --git a/packages/core/test/session-execution.test.ts b/packages/core/test/session-execution.test.ts index 192b88eac871..9ff3cc0c54d6 100644 --- a/packages/core/test/session-execution.test.ts +++ b/packages/core/test/session-execution.test.ts @@ -105,6 +105,31 @@ describe("SessionExecution lifecycle", () => { }), ) + it.effect("starts every suspended execution without waiting for earlier drains to finish", () => + Effect.gen(function* () { + const database = yield* Database.Service + const sessionIDs = Array.from({ length: 5 }, (_, index) => SessionV2.ID.make(`ses_resume_concurrent_${index}`)) + yield* seedSessions(database, sessionIDs, { time_suspended: Date.now() }) + + const fourStarted = yield* Deferred.make() + const started: SessionV2.ID[] = [] + const scope = yield* Scope.make() + yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void)) + const context = yield* buildExecution(scope, ({ sessionID }) => + Effect.sync(() => { + started.push(sessionID) + if (started.length === 4) Deferred.doneUnsafe(fourStarted, Effect.void) + }).pipe(Effect.andThen(Effect.never)), + ) + const execution = Context.get(context, SessionExecution.Service) + const restart = Context.get(context, SessionRestart.Service) + yield* restart.resumeSuspendedSessions.pipe(Effect.forkIn(scope)) + yield* Deferred.await(fourStarted) + + expect([...(yield* execution.active)].toSorted()).toEqual(sessionIDs.toSorted()) + }), + ) + it.effect("resumes each suspended Session at most once", () => Effect.gen(function* () { const database = yield* Database.Service @@ -115,9 +140,11 @@ describe("SessionExecution lifecycle", () => { const drained: string[] = [] const scope = yield* Scope.make() const context = yield* buildExecution(scope, ({ sessionID }) => Effect.sync(() => void drained.push(sessionID))) + const execution = Context.get(context, SessionExecution.Service) const restart = Context.get(context, SessionRestart.Service) yield* restart.resumeSuspendedSessions + yield* Effect.forEach([first, second], execution.awaitIdle, { discard: true }) expect(drained.toSorted()).toEqual([first, second]) expect(yield* suspensions(database)).toEqual({ [first]: false, [second]: false }) From 2200d100d071d20fc53a550ca97c970ab7c99e1c Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 24 Jul 2026 13:32:14 -0400 Subject: [PATCH 090/150] refactor(core): name the unsettled-tool sweep scope and untangle hosted settlement (#38724) --- packages/core/src/session/runner/llm.ts | 37 ++++++------------- .../src/session/runner/publish-llm-event.ts | 4 +- 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index ed358e666668..0762e5f2465f 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -364,32 +364,17 @@ const layer = Layer.effect( // these sweeps only close calls that could not produce a truthful settlement. if (providerFailed) yield* serialized(publisher.failUnsettledTools({ type: "aborted", message: "Tool execution interrupted" })) - if (llmFailure && !providerFailed) - yield* serialized( - publisher.failUnsettledTools( - { - type: "tool.result-missing", - message: "Provider did not return a tool result", - }, - true, - ), - ) - const hostedResultMissing = - stream._tag === "Success" && !providerFailed - ? yield* serialized( - publisher.failUnsettledTools( - { type: "tool.result-missing", message: "Provider did not return a tool result" }, - true, - ), - ) - : false - if (hostedResultMissing && !publisher.stepSettlement()) - yield* serialized( - publisher.failAssistant({ - type: "tool.result-missing", - message: "Provider did not return a tool result", - }), - ) + const resultMissing = { + type: "tool.result-missing", + message: "Provider did not return a tool result", + } as const + if (llmFailure && !providerFailed) yield* serialized(publisher.failUnsettledTools(resultMissing, "hosted")) + // A clean stream that still left hosted calls unresolved fails the step itself. + if (stream._tag === "Success" && !providerFailed) { + const hostedResultMissing = yield* serialized(publisher.failUnsettledTools(resultMissing, "hosted")) + if (hostedResultMissing && !publisher.stepSettlement()) + yield* serialized(publisher.failAssistant(resultMissing)) + } const stepFailure = publisher.stepFailure() const stepSettlement = publisher.stepSettlement() diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts index bb00c540c657..cefe60da8695 100644 --- a/packages/core/src/session/runner/publish-llm-event.ts +++ b/packages/core/src/session/runner/publish-llm-event.ts @@ -277,9 +277,9 @@ export const createLLMEventPublisher = (events: Pick { From 9b640cf97d52ae02bd442abbfd027b94f205177e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:33:28 -0500 Subject: [PATCH 091/150] test(core): remove flaky npm install test (#38729) Co-authored-by: Aiden Cline --- packages/core/test/npm.test.ts | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/packages/core/test/npm.test.ts b/packages/core/test/npm.test.ts index b2235d5b7afd..3dae4f434bed 100644 --- a/packages/core/test/npm.test.ts +++ b/packages/core/test/npm.test.ts @@ -64,29 +64,3 @@ describe("Npm.add", () => { expect(entries.fallback.entrypoint).toEndWith("/index.js") }) }) - -describe("Npm.install", () => { - test("respects omit from project .npmrc", async () => { - await using tmp = await tmpdir() - - await writePackage(tmp.path, { - name: "fixture", - dependencies: { - "prod-pkg": "file:./prod-pkg", - }, - devDependencies: { - "dev-pkg": "file:./dev-pkg", - }, - }) - await Bun.write(path.join(tmp.path, ".npmrc"), "omit=dev\n") - await fs.mkdir(path.join(tmp.path, "prod-pkg")) - await fs.mkdir(path.join(tmp.path, "dev-pkg")) - await writePackage(path.join(tmp.path, "prod-pkg"), { name: "prod-pkg" }) - await writePackage(path.join(tmp.path, "dev-pkg"), { name: "dev-pkg" }) - - await Npm.install(tmp.path) - - await expect(fs.stat(path.join(tmp.path, "node_modules", "prod-pkg"))).resolves.toBeDefined() - await expect(fs.stat(path.join(tmp.path, "node_modules", "dev-pkg"))).rejects.toThrow() - }) -}) From 993f046dd990baf667093c3eebb8eb4e187a33ed Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 24 Jul 2026 14:02:35 -0400 Subject: [PATCH 092/150] fix(ai): layer prompt cache breakpoints (#38725) --- .changeset/quick-caches-roll.md | 5 ++ packages/ai/README.md | 10 ++- packages/ai/src/cache-policy.ts | 86 +++++++++++++------ packages/ai/src/schema/options.ts | 10 +-- packages/ai/test/cache-policy.test.ts | 76 ++++++++++++++-- ...g-tool-turn-inside-the-cache-lookback.json | 54 ++++++++++++ .../anthropic-messages-cache.recorded.test.ts | 59 ++++++++++++- .../ai/test/provider/bedrock-converse.test.ts | 1 + 8 files changed, 258 insertions(+), 43 deletions(-) create mode 100644 .changeset/quick-caches-roll.md create mode 100644 packages/ai/test/fixtures/recordings/anthropic-messages-cache/keeps-a-long-tool-turn-inside-the-cache-lookback.json diff --git a/.changeset/quick-caches-roll.md b/.changeset/quick-caches-roll.md new file mode 100644 index 000000000000..3ab9e9b740a9 --- /dev/null +++ b/.changeset/quick-caches-roll.md @@ -0,0 +1,5 @@ +--- +"@opencode-ai/ai": patch +--- + +Improve Anthropic and Bedrock prompt reuse with layered cache breakpoints that roll through long tool loops. diff --git a/packages/ai/README.md b/packages/ai/README.md index 597ff8eaab5b..377e09306fde 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -207,7 +207,9 @@ Prompt caching is **on by default**. Every `LLMRequest` resolves to `cache: "aut ### Auto placement -`"auto"` places three breakpoints — last tool definition, last system part, latest user message. The last-user-message boundary is the load-bearing detail: in a tool-use loop, a single user turn expands into many assistant/tool round-trips, all sharing that prefix. Caching at that boundary lets every intra-turn API call hit. +`"auto"` places up to four breakpoints — the last tool definition, the first system part, the last system part when distinct, and the final message boundary. These expose successively larger reusable prefixes for tools, the base agent, project instructions, and the active conversation. The rolling final-message boundary is the load-bearing detail in tool loops: it advances on every request so the previous cache entry stays within Anthropic's 20-block lookback. + +Tools precede every system and conversation block in the provider prefix, so tool definitions must remain byte-stable and deterministically ordered for downstream breakpoints to remain reusable. The math justifies the default: Anthropic's 5-minute cache write is 1.25× base, read is 0.1×, so a single reuse within 5 minutes already wins. One-shot completions below the per-model minimum-cacheable-token threshold silently no-op on the wire, so the worst case is harmless. @@ -235,7 +237,7 @@ cache: { ### Manual hints -Inline `CacheHint` on any text / system / tool / tool-result part overrides automatic placement. The auto policy preserves manual hints; it only fills gaps. +Inline `CacheHint` on any text / system / tool / tool-result part overrides automatic placement. The auto policy preserves manual hints, counts them against Anthropic and Bedrock's four-breakpoint limit, and only fills the remaining slots. ```ts LLM.request({ @@ -251,8 +253,8 @@ LLM.request({ | Protocol | `cache: "auto"` | | ----------------------- | ------------------------------------------------------------------------- | -| Anthropic Messages | emits up to 3 `cache_control` markers (4-breakpoint cap enforced) | -| Bedrock Converse | emits up to 3 `cachePoint` blocks (4-breakpoint cap enforced) | +| Anthropic Messages | emits up to 4 `cache_control` markers (4-breakpoint cap enforced) | +| Bedrock Converse | emits up to 4 `cachePoint` blocks (4-breakpoint cap enforced) | | OpenAI Chat / Responses | no-op (implicit caching above 1024 tokens) | | Gemini | no-op (implicit caching on 2.5+; explicit `CachedContent` is out-of-band) | diff --git a/packages/ai/src/cache-policy.ts b/packages/ai/src/cache-policy.ts index 60f96dc69aaa..7679c7cf5437 100644 --- a/packages/ai/src/cache-policy.ts +++ b/packages/ai/src/cache-policy.ts @@ -2,32 +2,31 @@ // the policy designates. Runs once at compile time, before the per-protocol // body builder, so the existing inline-hint lowering path handles the rest. // -// The default `"auto"` shape places one breakpoint at the last tool definition, -// one at the last system part, and one at the latest user message. This -// matches what production agent harnesses (LangChain's caching middleware, -// kern-ai's 10x cost-reduction playbook) converge on for tool-use loops: the -// latest user message stays put while a single turn explodes into many -// assistant/tool round-trips, so caching at that boundary lets every -// intra-turn API call hit the prefix. +// The default `"auto"` shape places breakpoints at the last tool definition, +// the first and last distinct system parts, and the conversation tail. This +// exposes reusable tool, base-agent, project, and session prefixes while +// advancing the tail after each tool result keeps the previous cache entry +// within Anthropic's 20-block lookback during long agent turns. // -// Manual `cache: CacheHint` placements on individual parts are preserved — -// this function only fills gaps the caller left empty. +// Manual `cache: CacheHint` placements on individual parts are preserved and +// count against the four-breakpoint budget; auto only fills remaining slots. import { CacheHint, type CachePolicy, type CachePolicyObject } from "./schema/options" import { LLMRequest, Message, ToolDefinition, type ContentPart } from "./schema/messages" const AUTO: CachePolicyObject = { tools: true, system: true, - messages: "latest-user-message", + messages: { tail: 1 }, } const NONE: CachePolicyObject = {} +const BREAKPOINT_CAP = 4 // Resolution rules: // - undefined → "auto" — caching is on by default. The math favors it: // Anthropic 5m-cache write is 1.25x base, read is 0.1x, // so a single reuse within 5 minutes already wins. -// - "auto" → tools + system + latest user msg. +// - "auto" → tools + first/last system + final message boundary. // - "none" → no auto placement; manual `CacheHint`s still flow. // - object form → exactly what the caller asked for. const resolve = (policy: CachePolicy | undefined): CachePolicyObject => { @@ -44,18 +43,32 @@ const RESPECTS_INLINE_HINTS = new Set(["anthropic-messages", "bedrock-converse"] const makeHint = (ttlSeconds: number | undefined): CacheHint => ttlSeconds !== undefined ? new CacheHint({ type: "ephemeral", ttlSeconds }) : new CacheHint({ type: "ephemeral" }) -const markLastTool = (tools: ReadonlyArray, hint: CacheHint): ReadonlyArray => { +interface Budget { + remaining: number +} + +const markLastTool = ( + tools: ReadonlyArray, + hint: CacheHint, + budget: Budget, +): ReadonlyArray => { if (tools.length === 0) return tools const last = tools.length - 1 - if (tools[last]!.cache) return tools + if (tools[last]!.cache || budget.remaining === 0) return tools + budget.remaining -= 1 return tools.map((tool, i) => (i === last ? new ToolDefinition({ ...tool, cache: hint }) : tool)) } -const markLastSystem = (system: LLMRequest["system"], hint: CacheHint): LLMRequest["system"] => { +const markSystemBoundaries = (system: LLMRequest["system"], hint: CacheHint, budget: Budget): LLMRequest["system"] => { if (system.length === 0) return system - const last = system.length - 1 - if (system[last]!.cache) return system - return system.map((part, i) => (i === last ? { ...part, cache: hint } : part)) + let changed = false + const next = system.map((part, index) => { + if ((index !== 0 && index !== system.length - 1) || part.cache || budget.remaining === 0) return part + budget.remaining -= 1 + changed = true + return { ...part, cache: hint } + }) + return changed ? next : system } const lastIndexOfRole = (messages: ReadonlyArray, role: Message["role"]): number => @@ -64,14 +77,20 @@ const lastIndexOfRole = (messages: ReadonlyArray, role: Message["role"] // Mark the last text part of `messages[index]`. If no text part exists, mark // the last content part regardless of type — that's the breakpoint position // in tool-result-only messages too. -const markMessageAt = (messages: ReadonlyArray, index: number, hint: CacheHint): ReadonlyArray => { +const markMessageAt = ( + messages: ReadonlyArray, + index: number, + hint: CacheHint, + budget: Budget, +): ReadonlyArray => { if (index < 0 || index >= messages.length) return messages const target = messages[index]! if (target.content.length === 0) return messages const lastTextIndex = target.content.findLastIndex((part) => part.type === "text") const markAt = lastTextIndex >= 0 ? lastTextIndex : target.content.length - 1 const existing = target.content[markAt]! - if ("cache" in existing && existing.cache) return messages + if (("cache" in existing && existing.cache) || budget.remaining === 0) return messages + budget.remaining -= 1 const nextContent = target.content.map((part, i) => (i === markAt ? ({ ...part, cache: hint } as ContentPart) : part)) const next = new Message({ ...target, content: nextContent }) // Single pass over `messages`, substituting the one updated entry. Long @@ -86,25 +105,42 @@ const markMessages = ( messages: ReadonlyArray, strategy: NonNullable, hint: CacheHint, + budget: Budget, ): ReadonlyArray => { if (messages.length === 0) return messages - if (strategy === "latest-user-message") return markMessageAt(messages, lastIndexOfRole(messages, "user"), hint) - if (strategy === "latest-assistant") return markMessageAt(messages, lastIndexOfRole(messages, "assistant"), hint) + if (strategy === "latest-user-message") + return markMessageAt(messages, lastIndexOfRole(messages, "user"), hint, budget) + if (strategy === "latest-assistant") + return markMessageAt(messages, lastIndexOfRole(messages, "assistant"), hint, budget) const start = Math.max(0, messages.length - strategy.tail) let next = messages - for (let i = start; i < messages.length; i++) next = markMessageAt(next, i, hint) + for (let i = start; i < messages.length; i++) next = markMessageAt(next, i, hint, budget) return next } +const countHints = (request: LLMRequest) => + request.tools.reduce((count, tool) => count + (tool.cache === undefined ? 0 : 1), 0) + + request.system.reduce((count, part) => count + (part.cache === undefined ? 0 : 1), 0) + + request.messages.reduce( + (count, message) => + count + + message.content.reduce( + (contentCount, part) => contentCount + ("cache" in part && part.cache !== undefined ? 1 : 0), + 0, + ), + 0, + ) + export const applyCachePolicy = (request: LLMRequest): LLMRequest => { if (!RESPECTS_INLINE_HINTS.has(request.model.route.id)) return request const policy = resolve(request.cache) if (!policy.tools && !policy.system && !policy.messages) return request const hint = makeHint(policy.ttlSeconds) - const tools = policy.tools ? markLastTool(request.tools, hint) : request.tools - const system = policy.system ? markLastSystem(request.system, hint) : request.system - const messages = policy.messages ? markMessages(request.messages, policy.messages, hint) : request.messages + const budget = { remaining: Math.max(0, BREAKPOINT_CAP - countHints(request)) } + const tools = policy.tools ? markLastTool(request.tools, hint, budget) : request.tools + const system = policy.system ? markSystemBoundaries(request.system, hint, budget) : request.system + const messages = policy.messages ? markMessages(request.messages, policy.messages, hint, budget) : request.messages if (tools === request.tools && system === request.system && messages === request.messages) return request return LLMRequest.update(request, { tools, system, messages }) diff --git a/packages/ai/src/schema/options.ts b/packages/ai/src/schema/options.ts index b7ef93252789..c80bce0fda43 100644 --- a/packages/ai/src/schema/options.ts +++ b/packages/ai/src/schema/options.ts @@ -251,11 +251,11 @@ export class CacheHint extends Schema.Class("LLM.CacheHint")({ // Auto-placement policy for prompt caching. The protocol-neutral lowering step // reads this and injects `CacheHint`s at the configured boundaries; the // per-protocol body builders then translate those hints into wire markers as -// usual. `"auto"` is the recommended default for agent loops — it places one -// breakpoint at the last tool definition, one at the last system part, and one -// at the latest user message. The combination of provider invalidation -// hierarchy (tools → system → messages) and Anthropic/Bedrock's 20-block -// lookback means three trailing breakpoints reliably cover the static prefix. +// usual. `"auto"` is the recommended default for agent loops — it places +// breakpoints at the last tool definition, the first and last distinct system +// parts, and the conversation tail. The rolling message breakpoint keeps a +// prior cache entry within Anthropic/Bedrock's 20-block lookback during long +// tool loops. // // Pass `"none"` to opt out entirely (the legacy behavior). Pass the granular // object form to override individual choices. diff --git a/packages/ai/test/cache-policy.test.ts b/packages/ai/test/cache-policy.test.ts index a126d9502c5e..8e2535b54baf 100644 --- a/packages/ai/test/cache-policy.test.ts +++ b/packages/ai/test/cache-policy.test.ts @@ -39,8 +39,8 @@ describe("applyCachePolicy", () => { }), ) - // No explicit cache field → auto policy fires → last system part + latest - // user message both get cache_control markers. + // A single system block is both the first and last boundary, so the auto + // policy deduplicates it and still marks the conversation tail. expect(prepared.body).toMatchObject({ system: [{ type: "text", text: "You are concise.", cache_control: { type: "ephemeral" } }], messages: [{ role: "user", content: [{ type: "text", text: "hi", cache_control: { type: "ephemeral" } }] }], @@ -48,12 +48,15 @@ describe("applyCachePolicy", () => { }), ) - it.effect("'auto' marks the last tool, last system part, and latest user message on Anthropic", () => + it.effect("'auto' marks the last tool, first and last system parts, and final message boundary on Anthropic", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( LLM.request({ model: anthropicModel, - system: "Sys A", + system: [ + { type: "text", text: "Base agent" }, + { type: "text", text: "Project instructions" }, + ], tools: [{ name: "t1", description: "t1", inputSchema: { type: "object", properties: {} } }], messages: [ Message.user("first user"), @@ -66,7 +69,10 @@ describe("applyCachePolicy", () => { expect(prepared.body).toMatchObject({ tools: [{ name: "t1", cache_control: { type: "ephemeral" } }], - system: [{ type: "text", text: "Sys A", cache_control: { type: "ephemeral" } }], + system: [ + { type: "text", text: "Base agent", cache_control: { type: "ephemeral" } }, + { type: "text", text: "Project instructions", cache_control: { type: "ephemeral" } }, + ], messages: [ { role: "user", content: [{ type: "text", text: "first user" }] }, { role: "assistant", content: [{ type: "text", text: "assistant reply" }] }, @@ -120,7 +126,10 @@ describe("applyCachePolicy", () => { const prepared = yield* LLMClient.prepare( LLM.request({ model: bedrockModel, - system: "Sys", + system: [ + { type: "text", text: "Base agent" }, + { type: "text", text: "Project instructions" }, + ], tools: [{ name: "t1", description: "t1", inputSchema: { type: "object", properties: {} } }], messages: [Message.user("first user"), Message.assistant("reply"), Message.user("latest user")], cache: "auto", @@ -131,7 +140,12 @@ describe("applyCachePolicy", () => { toolConfig: { tools: [{ toolSpec: { name: "t1" } }, { cachePoint: { type: "default" } }], }, - system: [{ text: "Sys" }, { cachePoint: { type: "default" } }], + system: [ + { text: "Base agent" }, + { cachePoint: { type: "default" } }, + { text: "Project instructions" }, + { cachePoint: { type: "default" } }, + ], messages: [ { role: "user", content: [{ text: "first user" }] }, { role: "assistant", content: [{ text: "reply" }] }, @@ -193,9 +207,55 @@ describe("applyCachePolicy", () => { }), ) - const body = prepared.body as { system: Array<{ text: string; cache_control?: unknown }> } + const body = prepared.body as { + system: Array<{ text: string; cache_control?: unknown }> + messages: Array<{ content: Array<{ cache_control?: unknown }> }> + } expect(body.system[0]?.cache_control).toEqual({ type: "ephemeral", ttl: "1h" }) expect(body.system[1]?.cache_control).toEqual({ type: "ephemeral" }) + expect(body.messages[0]?.content[0]?.cache_control).toEqual({ type: "ephemeral" }) + }), + ) + + it.effect("auto policy stays within the four-breakpoint cap when preserving manual hints", () => + Effect.gen(function* () { + const request = LLM.request({ + model: anthropicModel, + system: [ + { type: "text", text: "Base agent" }, + { + type: "text", + text: "Manual context", + cache: new CacheHint({ type: "ephemeral", ttlSeconds: 3600 }), + }, + { type: "text", text: "Project instructions" }, + ], + tools: [{ name: "t1", description: "t1", inputSchema: { type: "object", properties: {} } }], + prompt: "hi", + cache: "auto", + }) + const applied = applyCachePolicy(request) + expect(applied.tools[0]?.cache).toBeDefined() + expect(applied.system.map((part) => part.cache !== undefined)).toEqual([true, true, true]) + const tail = applied.messages[0]!.content[0]! + expect("cache" in tail ? tail.cache : undefined).toBeUndefined() + expect(applyCachePolicy(applied)).toBe(applied) + + const prepared = yield* LLMClient.prepare(request) + + const body = prepared.body as { + tools: Array<{ cache_control?: unknown }> + system: Array<{ cache_control?: unknown }> + messages: Array<{ content: Array<{ cache_control?: unknown }> }> + } + const marked = [ + ...body.tools.map((tool) => tool.cache_control), + ...body.system.map((part) => part.cache_control), + ...body.messages.flatMap((message) => message.content.map((part) => part.cache_control)), + ].filter((cache) => cache !== undefined) + expect(marked).toHaveLength(4) + expect(body.system[1]?.cache_control).toEqual({ type: "ephemeral", ttl: "1h" }) + expect(body.messages[0]?.content[0]?.cache_control).toBeUndefined() }), ) diff --git a/packages/ai/test/fixtures/recordings/anthropic-messages-cache/keeps-a-long-tool-turn-inside-the-cache-lookback.json b/packages/ai/test/fixtures/recordings/anthropic-messages-cache/keeps-a-long-tool-turn-inside-the-cache-lookback.json new file mode 100644 index 000000000000..cf349fb8526b --- /dev/null +++ b/packages/ai/test/fixtures/recordings/anthropic-messages-cache/keeps-a-long-tool-turn-inside-the-cache-lookback.json @@ -0,0 +1,54 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:anthropic-messages-cache", + "provider:anthropic", + "protocol:anthropic-messages", + "cache", + "tool" + ], + "name": "anthropic-messages-cache/keeps-a-long-tool-turn-inside-the-cache-lookback", + "recordedAt": "2026-07-24T16:22:29.494Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.anthropic.com/v1/messages", + "headers": { + "anthropic-version": "2023-06-01", + "content-type": "application/json" + }, + "body": "{\"model\":\"claude-haiku-4-5-20251001\",\"system\":[{\"type\":\"text\",\"text\":\"You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. \",\"cache_control\":{\"type\":\"ephemeral\"}}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Run the fixture lookups.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_0\",\"name\":\"lookup\",\"input\":{\"index\":0}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_0\",\"content\":\"\\\"Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_1\",\"name\":\"lookup\",\"input\":{\"index\":1}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_1\",\"content\":\"\\\"Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_2\",\"name\":\"lookup\",\"input\":{\"index\":2}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_2\",\"content\":\"\\\"Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_3\",\"name\":\"lookup\",\"input\":{\"index\":3}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_3\",\"content\":\"\\\"Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_4\",\"name\":\"lookup\",\"input\":{\"index\":4}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_4\",\"content\":\"\\\"Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_5\",\"name\":\"lookup\",\"input\":{\"index\":5}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_5\",\"content\":\"\\\"Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_6\",\"name\":\"lookup\",\"input\":{\"index\":6}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_6\",\"content\":\"\\\"Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_7\",\"name\":\"lookup\",\"input\":{\"index\":7}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_7\",\"content\":\"\\\"Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_8\",\"name\":\"lookup\",\"input\":{\"index\":8}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_8\",\"content\":\"\\\"Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_9\",\"name\":\"lookup\",\"input\":{\"index\":9}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_9\",\"content\":\"\\\"Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_10\",\"name\":\"lookup\",\"input\":{\"index\":10}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_10\",\"content\":\"\\\"Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. \\\"\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"tools\":[{\"name\":\"lookup\",\"description\":\"Look up a fixture value.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"index\":{\"type\":\"number\"}},\"required\":[\"index\"],\"additionalProperties\":false},\"cache_control\":{\"type\":\"ephemeral\"}}],\"stream\":true,\"max_tokens\":16,\"temperature\":0}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_011CdMBzDZ4qqmkyuCMbZ81z\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":5,\"cache_creation_input_tokens\":12288,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":12288,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":2,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Fixture\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" lookups complete. Results:\\n\\n- Index 0: Fixture\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"max_tokens\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":5,\"cache_creation_input_tokens\":12288,\"cache_read_input_tokens\":0,\"output_tokens\":16} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n" + } + }, + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.anthropic.com/v1/messages", + "headers": { + "anthropic-version": "2023-06-01", + "content-type": "application/json" + }, + "body": "{\"model\":\"claude-haiku-4-5-20251001\",\"system\":[{\"type\":\"text\",\"text\":\"You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. \",\"cache_control\":{\"type\":\"ephemeral\"}}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Run the fixture lookups.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_0\",\"name\":\"lookup\",\"input\":{\"index\":0}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_0\",\"content\":\"\\\"Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. Fixture result 0. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_1\",\"name\":\"lookup\",\"input\":{\"index\":1}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_1\",\"content\":\"\\\"Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. Fixture result 1. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_2\",\"name\":\"lookup\",\"input\":{\"index\":2}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_2\",\"content\":\"\\\"Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. Fixture result 2. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_3\",\"name\":\"lookup\",\"input\":{\"index\":3}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_3\",\"content\":\"\\\"Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. Fixture result 3. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_4\",\"name\":\"lookup\",\"input\":{\"index\":4}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_4\",\"content\":\"\\\"Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. Fixture result 4. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_5\",\"name\":\"lookup\",\"input\":{\"index\":5}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_5\",\"content\":\"\\\"Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. Fixture result 5. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_6\",\"name\":\"lookup\",\"input\":{\"index\":6}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_6\",\"content\":\"\\\"Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. Fixture result 6. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_7\",\"name\":\"lookup\",\"input\":{\"index\":7}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_7\",\"content\":\"\\\"Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. Fixture result 7. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_8\",\"name\":\"lookup\",\"input\":{\"index\":8}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_8\",\"content\":\"\\\"Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. Fixture result 8. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_9\",\"name\":\"lookup\",\"input\":{\"index\":9}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_9\",\"content\":\"\\\"Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. Fixture result 9. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"lookup_10\",\"name\":\"lookup\",\"input\":{\"index\":10}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"lookup_10\",\"content\":\"\\\"Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. Fixture result 10. \\\"\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"The fixture lookups are complete.\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Reply exactly: OK\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"tools\":[{\"name\":\"lookup\",\"description\":\"Look up a fixture value.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"index\":{\"type\":\"number\"}},\"required\":[\"index\"],\"additionalProperties\":false},\"cache_control\":{\"type\":\"ephemeral\"}}],\"stream\":true,\"max_tokens\":16,\"temperature\":0}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_011CdMBzKTWjSsCrp7u47Xem\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":3,\"cache_creation_input_tokens\":19,\"cache_read_input_tokens\":12288,\"cache_creation\":{\"ephemeral_5m_input_tokens\":19,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"OK\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":3,\"cache_creation_input_tokens\":19,\"cache_read_input_tokens\":12288,\"output_tokens\":4} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n" + } + } + ] +} diff --git a/packages/ai/test/provider/anthropic-messages-cache.recorded.test.ts b/packages/ai/test/provider/anthropic-messages-cache.recorded.test.ts index 910c13fbb22a..c6b9814e3c5d 100644 --- a/packages/ai/test/provider/anthropic-messages-cache.recorded.test.ts +++ b/packages/ai/test/provider/anthropic-messages-cache.recorded.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" -import { CacheHint, LLM } from "../../src" +import { CacheHint, LLM, LLMRequest, Message, ToolCallPart, ToolDefinition } from "../../src" import { LLMClient } from "../../src/route" import * as Anthropic from "../../src/providers/anthropic" import { LARGE_CACHEABLE_SYSTEM } from "../recorded-scenarios" @@ -24,6 +24,39 @@ const cacheRequest = LLM.request({ generation: { maxTokens: 16, temperature: 0 }, }) +const lookup = ToolDefinition.make({ + name: "lookup", + description: "Look up a fixture value.", + inputSchema: { + type: "object", + properties: { index: { type: "number" } }, + required: ["index"], + additionalProperties: false, + }, +}) +const longToolTurn = [ + Message.user("Run the fixture lookups."), + ...Array.from({ length: 11 }, (_, index) => { + const id = `lookup_${index}` + return [ + Message.assistant(ToolCallPart.make({ id, name: lookup.name, input: { index } })), + Message.tool({ + id, + name: lookup.name, + result: `Fixture result ${index}. `.repeat(80), + }), + ] + }).flat(), +] +const longToolTurnRequest = LLM.request({ + id: "recorded_anthropic_cache_long_tool_turn", + model, + system: LARGE_CACHEABLE_SYSTEM, + messages: longToolTurn, + tools: [lookup], + generation: { maxTokens: 16, temperature: 0 }, +}) + const recorded = recordedTests({ prefix: "anthropic-messages-cache", provider: "anthropic", @@ -50,4 +83,28 @@ describe("Anthropic Messages cache recorded", () => { expect(second.usage?.cacheReadInputTokens ?? 0).toBeGreaterThan(0) }), ) + + recorded.effect.with("keeps a long tool turn inside the cache lookback", { tags: ["cache", "tool"] }, () => + Effect.gen(function* () { + const first = yield* LLMClient.generate(longToolTurnRequest) + const firstRead = first.usage?.cacheReadInputTokens ?? 0 + const firstWrite = first.usage?.cacheWriteInputTokens ?? 0 + const firstCached = firstRead + firstWrite + // The prefix may already be warm when recording, so either a read or a + // write establishes that Anthropic recognized the cache boundary. + expect(firstCached).toBeGreaterThan(0) + + const second = yield* LLMClient.generate( + LLMRequest.update(longToolTurnRequest, { + messages: [ + ...longToolTurn, + Message.assistant("The fixture lookups are complete."), + Message.user("Reply exactly: OK"), + ], + }), + ) + expect(second.usage?.cacheReadInputTokens ?? 0).toBeGreaterThanOrEqual(firstCached) + expect(second.usage?.cacheWriteInputTokens ?? 0).toBeLessThan(firstCached) + }), + ) }) diff --git a/packages/ai/test/provider/bedrock-converse.test.ts b/packages/ai/test/provider/bedrock-converse.test.ts index 86f2240488c3..5cc8af1378a7 100644 --- a/packages/ai/test/provider/bedrock-converse.test.ts +++ b/packages/ai/test/provider/bedrock-converse.test.ts @@ -939,6 +939,7 @@ describe("Bedrock Converse route", () => { const prepared = yield* LLMClient.prepare( LLM.request({ model, + cache: "none", messages: [ Message.assistant([ToolCallPart.make({ id: "call_1", name: "read", input: { path: "report.pdf" } })]), Message.tool({ From 5ae2d6d3f64289d8e623e05e812aa96afd1391c5 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 24 Jul 2026 14:18:02 -0400 Subject: [PATCH 093/150] refactor(core): settle declined tool calls durably with typed reasons (#38734) --- packages/core/src/permission.ts | 5 ++ packages/core/src/session/model-request.ts | 32 +++++++-- packages/core/src/session/runner/llm.ts | 72 ++++++++++++++----- .../src/session/runner/publish-llm-event.ts | 27 ++++--- packages/core/src/tool/AGENTS.md | 2 +- packages/core/src/tool/question.ts | 3 + packages/core/test/session-runner.test.ts | 4 +- 7 files changed, 109 insertions(+), 36 deletions(-) diff --git a/packages/core/src/permission.ts b/packages/core/src/permission.ts index 1a93f4f58da7..8b276833dd38 100644 --- a/packages/core/src/permission.ts +++ b/packages/core/src/permission.ts @@ -218,6 +218,11 @@ const layer = Layer.effect( if (result.effect === "allow") return const item = yield* create(request(input), input.agent) return yield* restore(Deferred.await(item.deferred)).pipe( + // Deliberate defect tunnel: leaves wrap execution in blanket `mapError`, which + // must not convert a user's decline into model-facing tool output. The decline + // resurfaces as a typed failure at SessionModelRequest.executeTool. A decline + // WITH feedback (CorrectedError) intentionally stays typed so the leaf can turn + // it into ToolFailure and the model continues. Effect.catchTag("PermissionV2.DeclinedError", (error) => Effect.die(error)), Effect.ensuring( Effect.sync(() => { diff --git a/packages/core/src/session/model-request.ts b/packages/core/src/session/model-request.ts index af9ac71a8646..b62fdc8f3f70 100644 --- a/packages/core/src/session/model-request.ts +++ b/packages/core/src/session/model-request.ts @@ -2,11 +2,14 @@ export * as SessionModelRequest from "./model-request" import { LLM, Message, SystemPart, type LLMRequest, type ToolContent } from "@opencode-ai/ai" import { SessionError } from "@opencode-ai/schema/session-error" -import { Context, Effect, Layer } from "effect" +import { Cause, Context, Effect, Layer, Result } from "effect" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { App } from "../app" import { ModelV2 } from "../model" +import { PermissionV2 } from "../permission" import { PluginHooks } from "../plugin/hooks" +import { QuestionTool } from "../tool/question" +import { ToolOutputStore } from "../tool-output-store" import { ToolRegistry } from "../tool/registry" import { SessionContext } from "./context" import { SessionModelHeaders } from "./model-headers" @@ -14,13 +17,32 @@ import { MAX_STEPS_PROMPT } from "./runner/max-steps" import PROMPT_DEFAULT from "./runner/prompt/base.txt" import { toLLMMessages } from "./runner/to-llm-message" +/** Failures a prepared execution can surface: infrastructure errors plus user declines resurfaced from the defect tunnel. */ +export type ExecuteError = ToolOutputStore.Error | PermissionV2.DeclinedError | QuestionTool.CancelledError + +// User declines dive under the leaves' blanket `mapError` as defects (the deliberate +// tunnel entered in PermissionV2.assert and the question tool), so a user's "no" can +// never become model-facing tool output. They resurface as typed failures exactly once, +// here at the seam the runner executes through. +const declineDefect = (cause: Cause.Cause) => { + const decline = cause.reasons.flatMap((reason) => + Cause.isDieReason(reason) && + (reason.defect instanceof PermissionV2.DeclinedError || reason.defect instanceof QuestionTool.CancelledError) + ? [reason.defect] + : [], + )[0] + return decline ? Result.succeed(decline) : Result.fail(cause) +} + interface Prepared { readonly request: LLMRequest /** * One request-scoped execution operation. Unknown, hook-removed, and * step-limit-violating calls fail individually through the same seam. */ - readonly executeTool: ToolRegistry.ToolSet["execute"] + readonly executeTool: ( + input: ToolRegistry.ExecuteInput, + ) => Effect.Effect /** True when this request is the final Step; violating calls are rejected and no continuation follows. */ readonly stepLimitReached: boolean } @@ -134,7 +156,7 @@ export const layer = Layer.effect( tools: hookedTools, toolChoice: stepLimitReached ? "none" : undefined, }) - const executeTool: ToolRegistry.ToolSet["execute"] = (executeInput) => { + const executeTool: Prepared["executeTool"] = (executeInput) => { if (stepLimitReached) return Effect.succeed({ status: "error", @@ -145,7 +167,9 @@ export const layer = Layer.effect( status: "error", error: { type: "tool.unknown", message: `Tool is not available for this request: ${executeInput.call.name}` }, }) - return toolSet.execute(executeInput) + return toolSet + .execute(executeInput) + .pipe(Effect.catchCauseFilter(declineDefect, (decline) => Effect.fail(decline))) } return { request, diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 0762e5f2465f..9f0fce8942b3 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -36,27 +36,44 @@ type CallOutcome = Data.TaggedEnum<{ const CallOutcome = Data.taggedEnum() // Declining an interactive prompt halts the drain instead of becoming model-facing tool output. -const isUserDeclined = (cause: Cause.Cause) => - cause.reasons.some( - (reason) => - Cause.isDieReason(reason) && - (reason.defect instanceof PermissionV2.DeclinedError || reason.defect instanceof QuestionTool.CancelledError), - ) +const isDecline = ( + error: SessionModelRequest.ExecuteError, +): error is PermissionV2.DeclinedError | QuestionTool.CancelledError => + error._tag === "PermissionV2.DeclinedError" || error._tag === "QuestionTool.CancelledError" /** - * Classifies how the owned tool fibers ended. Interrupts and interactive declines abort - * the step; a defect from a tool implementation becomes a failed tool call the model can - * read; a typed infrastructure failure must fail the assistant and then the drain. + * Classifies how the owned tool fibers ended. Interrupts abort the step; a user decline + * settles its own call and then aborts the step; a defect from a tool implementation + * becomes a failed tool call the model can read; a typed infrastructure failure must + * fail the assistant and then the drain. */ -const classifyToolExits = (settled: Exit.Exit>, never>) => { +const classifyToolExits = ( + settled: Exit.Exit>, never>, + calls: ReadonlyArray, +) => { + // Exits align with calls by construction: one owned fiber per accepted local call. + const exits = settled._tag === "Success" ? settled.value : [] + const declines = exits.flatMap((exit, index) => + exit._tag === "Failure" + ? exit.cause.reasons.flatMap((reason) => + Cause.isFailReason(reason) && isDecline(reason.error) ? [{ call: calls[index], reason: reason.error }] : [], + ) + : [], + ) const causes = - settled._tag === "Failure" - ? [settled.cause] - : settled.value.flatMap((exit) => (exit._tag === "Failure" ? [exit.cause] : [])) - const failure = causes.find((cause) => !Cause.hasInterrupts(cause) && !isUserDeclined(cause)) + settled._tag === "Failure" ? [settled.cause] : exits.flatMap((exit) => (exit._tag === "Failure" ? [exit.cause] : [])) + // The first non-interrupt, non-decline failure, rebuilt without decline reasons so the + // drain's error channel never carries a decline. + const failure = causes.flatMap((cause) => { + if (Cause.hasInterrupts(cause)) return [] + const reasons = cause.reasons.flatMap((reason): Array> => + Cause.isFailReason(reason) ? (isDecline(reason.error) ? [] : [Cause.makeFailReason(reason.error)]) : [reason], + ) + return reasons.length > 0 ? [Cause.fromReasons(reasons)] : [] + })[0] return { interrupted: causes.some(Cause.hasInterrupts), - declined: causes.some(isUserDeclined), + declines, failure, infraError: failure === undefined ? undefined : Option.getOrUndefined(Cause.findErrorOption(failure)), } @@ -204,7 +221,10 @@ const layer = Layer.effect( step: currentStep, }) // Every local tool call forked here is owned until it reaches one durable settlement. - const toolRuns: Array<{ readonly call: ToolCall; readonly fiber: Fiber.Fiber }> = [] + const toolRuns: Array<{ + readonly call: ToolCall + readonly fiber: Fiber.Fiber + }> = [] const interruptTools = Effect.suspend(() => Fiber.interruptAll(toolRuns.map((run) => run.fiber))) let needsContinuation = false const startSnapshot = yield* snapshots.capture() @@ -348,9 +368,23 @@ const layer = Layer.effect( Effect.forEach(toolRuns, (run) => Fiber.await(run.fiber), { concurrency: "unbounded" }), ).pipe(Effect.exit) if (settled._tag === "Failure") yield* interruptTools - const tools = classifyToolExits(settled) + const tools = classifyToolExits( + settled, + toolRuns.map((run) => run.call), + ) - if (tools.declined || streamInterrupted || tools.interrupted) { + // A declined call settles durably with its reason before the generic sweeps. + for (const decline of tools.declines) + yield* serialized( + publisher.failTool(decline.call.id, { + type: "aborted", + message: + decline.reason._tag === "QuestionTool.CancelledError" + ? decline.reason.message + : "The user declined this tool call", + }), + ) + if (tools.declines.length > 0 || streamInterrupted || tools.interrupted) { yield* serialized(publisher.failUnsettledTools({ type: "aborted", message: "Tool execution interrupted" })) yield* serialized(publisher.failAssistant({ type: "aborted", message: "Step interrupted" })) } @@ -390,7 +424,7 @@ const layer = Layer.effect( } if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause) - if (tools.declined) return yield* Effect.interrupt + if (tools.declines.length > 0) return yield* Effect.interrupt if ((tools.interrupted || tools.infraError !== undefined) && tools.failure) return yield* Effect.failCause(tools.failure) if (tools.interrupted && settled._tag === "Failure") return yield* Effect.failCause(settled.cause) diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts index cefe60da8695..cee1380048dc 100644 --- a/packages/core/src/session/runner/publish-llm-event.ts +++ b/packages/core/src/session/runner/publish-llm-event.ts @@ -232,21 +232,27 @@ export const createLLMEventPublisher = (events: Pick providerFailed, diff --git a/packages/core/src/tool/AGENTS.md b/packages/core/src/tool/AGENTS.md index 7c1e3f150699..2c1e9292bc09 100644 --- a/packages/core/src/tool/AGENTS.md +++ b/packages/core/src/tool/AGENTS.md @@ -24,7 +24,7 @@ const source = { } ``` -Leaves own resolution, permission, and side-effect ordering. Translate only expected typed errors into `ToolFailure`; do not use `catchCause`, because interruption and defects must survive. +Leaves own resolution, permission, and side-effect ordering. Translate only expected typed errors into `ToolFailure`; do not use `catchCause`, because interruption and defects must survive. User declines from `PermissionV2.assert` and question dismissals travel as defects beneath leaf `mapError` blankets and resurface as typed failures at `SessionModelRequest.executeTool`; leaves must never catch or convert them. A decline with feedback (`PermissionV2.CorrectedError`) stays typed so the leaf converts it into `ToolFailure` and the model continues. ## Registration diff --git a/packages/core/src/tool/question.ts b/packages/core/src/tool/question.ts index cdb80a0000f0..7a19f711c8d7 100644 --- a/packages/core/src/tool/question.ts +++ b/packages/core/src/tool/question.ts @@ -91,6 +91,9 @@ export const Plugin = { .pipe(Effect.orDie), ), Effect.flatMap((state) => { + // Deliberate defect tunnel (see PermissionV2.assert): a dismissal must dodge + // leaf `mapError` blankets so it never becomes model-facing tool output; it + // resurfaces as a typed failure at SessionModelRequest.executeTool. if (state.status === "cancelled") return Effect.die(new CancelledError()) const output = { answers: input.questions.map((_, index): QuestionV2.Answer => { diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index b4a937b8b510..882308604605 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -3569,7 +3569,7 @@ describe("SessionRunnerLLM", () => { { type: "tool", id: "call-declined", - state: { status: "error", error: { message: "Tool execution interrupted" } }, + state: { status: "error", error: { type: "aborted", message: "The user declined this tool call" } }, }, ], }, @@ -3721,7 +3721,7 @@ describe("SessionRunnerLLM", () => { { type: "tool", id: "call-question", - state: { status: "error", error: { type: "aborted", message: "Tool execution interrupted" } }, + state: { status: "error", error: { type: "aborted", message: "The user dismissed this question" } }, }, ], }, From 423fad730c99333fd3cdc1b00a7369f687494227 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:24:23 -0500 Subject: [PATCH 094/150] fix(core): authorize external glob paths (#38714) --- packages/core/src/tool/glob.ts | 22 +++-- packages/core/test/tool-search.test.ts | 112 +++++++++++++++++++++---- 2 files changed, 115 insertions(+), 19 deletions(-) diff --git a/packages/core/src/tool/glob.ts b/packages/core/src/tool/glob.ts index 3da274fcf10b..edcf98d7f1db 100644 --- a/packages/core/src/tool/glob.ts +++ b/packages/core/src/tool/glob.ts @@ -7,6 +7,7 @@ import path from "path" import { FileSystem } from "../filesystem" import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "../location" +import { LocationMutation } from "../location-mutation" import { Ripgrep } from "../ripgrep" import { RelativePath } from "../schema" import { PermissionV2 } from "../permission" @@ -45,6 +46,7 @@ export const Plugin = { const fs = yield* FSUtil.Service const ripgrep = yield* Ripgrep.Service const location = yield* Location.Service + const mutation = yield* LocationMutation.Service const permission = yield* PermissionV2.Service yield* ctx.tool @@ -58,6 +60,16 @@ export const Plugin = { output: Output, execute: (input, context) => Effect.gen(function* () { + const source = { type: "tool" as const, messageID: context.messageID, callID: context.callID } + const target = yield* mutation.resolve({ path: input.path ?? ".", kind: "directory" }) + const external = target.externalDirectory + if (external) + yield* permission.assert({ + ...LocationMutation.externalDirectoryPermission(external), + sessionID: context.sessionID, + agent: context.agent, + source, + }) yield* permission.assert({ action: name, resources: [input.pattern], @@ -69,20 +81,20 @@ export const Plugin = { }, sessionID: context.sessionID, agent: context.agent, - source: { type: "tool", messageID: context.messageID, callID: context.callID }, + source, }) - const cwd = path.resolve(location.directory, input.path ?? ".") yield* fs - .stat(cwd) + .stat(target.canonical) .pipe( Effect.catchReason("PlatformError", "NotFound", () => Effect.fail(new ToolFailure({ message: `Search path does not exist: ${input.path ?? "."}` })), ), ) + const root = path.resolve(location.directory, input.path ?? ".") const limit = input.limit ?? FileSystem.DEFAULT_SEARCH_LIMIT const entries = yield* ripgrep .glob({ - cwd, + cwd: target.canonical, pattern: input.pattern, limit: limit + 1, }) @@ -91,7 +103,7 @@ export const Plugin = { result.map((entry) => FileSystem.Entry.make({ ...entry, - path: RelativePath.make(path.relative(location.directory, path.resolve(cwd, entry.path))), + path: RelativePath.make(path.relative(location.directory, path.resolve(root, entry.path))), }), ), ), diff --git a/packages/core/test/tool-search.test.ts b/packages/core/test/tool-search.test.ts index 7f924a94dd14..022599e88412 100644 --- a/packages/core/test/tool-search.test.ts +++ b/packages/core/test/tool-search.test.ts @@ -8,6 +8,7 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { FileSystem } from "@opencode-ai/core/filesystem" import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "@opencode-ai/core/location" +import { LocationMutation } from "@opencode-ai/core/location-mutation" import { PermissionV2 } from "@opencode-ai/core/permission" import { Ripgrep } from "@opencode-ai/core/ripgrep" import { AbsolutePath } from "@opencode-ai/core/schema" @@ -24,27 +25,27 @@ import { executeTool, registerToolPlugin, toolIdentity } from "./lib/tool" const globToolNode = makeLocationNode({ name: "test/glob-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(GlobTool.Plugin)), - deps: [ToolRegistry.toolsNode, FSUtil.node, Ripgrep.node, Location.node, PermissionV2.node], + deps: [ + ToolRegistry.toolsNode, + FSUtil.node, + Ripgrep.node, + Location.node, + LocationMutation.node, + PermissionV2.node, + ], }) const grepToolNode = makeLocationNode({ name: "test/grep-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(GrepTool.Plugin)), deps: [ToolRegistry.toolsNode, FSUtil.node, Ripgrep.node, Location.node, PermissionV2.node], }) -const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ - assert: () => Effect.void, - ask: () => Effect.die("unused"), - reply: () => Effect.die("unused"), - get: () => Effect.die("unused"), - forSession: () => Effect.die("unused"), - list: () => Effect.die("unused"), - }), -) const sessionID = SessionV2.ID.make("ses_search_tool_test") -const withTools = (directory: string, body: (registry: ToolRegistry.Interface) => Effect.Effect) => +const withTools = ( + directory: string, + body: (registry: ToolRegistry.Interface) => Effect.Effect, + assertions?: PermissionV2.AssertInput[], +) => Effect.gen(function* () { return yield* body(yield* ToolRegistry.Service) }).pipe( @@ -54,7 +55,23 @@ const withTools = (directory: string, body: (registry: ToolRegistry.Int Location.node, Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))), ], - [PermissionV2.node, permission], + [ + PermissionV2.node, + Layer.succeed( + PermissionV2.Service, + PermissionV2.Service.of({ + assert: (input) => + Effect.sync(() => { + assertions?.push(input) + }), + ask: () => Effect.die("unused"), + reply: () => Effect.die("unused"), + get: () => Effect.die("unused"), + forSession: () => Effect.die("unused"), + list: () => Effect.die("unused"), + }), + ), + ], [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], ]), ), @@ -125,4 +142,71 @@ describe("search tools", () => { ), ) } + + it.live("requires external_directory approval for an explicit external glob path", () => + Effect.acquireUseRelease( + Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), + ([active, outside]) => { + const assertions: PermissionV2.AssertInput[] = [] + return Effect.promise(() => fs.writeFile(path.join(outside.path, "outside.txt"), "outside\n")).pipe( + Effect.andThen( + withTools( + active.path, + (registry) => executeTool(registry, call("glob", { path: outside.path, pattern: "*.txt" })), + assertions, + ), + ), + Effect.tap((result) => + Effect.sync(() => { + expect(result.status).toBe("completed") + expect(assertions.map((input) => input.action)).toEqual(["external_directory", "glob"]) + expect(assertions[0]?.resources).toEqual([ + path.join(outside.path, "*").replaceAll("\\", "/"), + ]) + }), + ), + ) + }, + ([active, outside]) => + Effect.promise(() => + Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined), + ), + ), + ) + + it.live("globs through an in-location external symlink without external approval", () => + Effect.acquireUseRelease( + Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), + ([active, outside]) => { + if (process.platform === "win32") return Effect.void + const assertions: PermissionV2.AssertInput[] = [] + return Effect.promise(async () => { + await fs.writeFile(path.join(outside.path, "outside.txt"), "outside\n") + await fs.symlink(outside.path, path.join(active.path, "linked")) + }).pipe( + Effect.andThen( + withTools( + active.path, + (registry) => executeTool(registry, call("glob", { path: "linked", pattern: "*.txt" })), + assertions, + ), + ), + Effect.tap((result) => + Effect.sync(() => { + expect(result.status).toBe("completed") + expect(assertions.map((input) => input.action)).toEqual(["glob"]) + expect(result).toMatchObject({ + output: [{ path: path.join("linked", "outside.txt"), type: "file" }], + content: [{ type: "text", text: path.join(active.path, "linked", "outside.txt") }], + }) + }), + ), + ) + }, + ([active, outside]) => + Effect.promise(() => + Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined), + ), + ), + ) }) From b09a066fb5f9f49db9f1c6539babbd9f8593b760 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 24 Jul 2026 14:26:30 -0400 Subject: [PATCH 095/150] fix(ai): report OpenAI cache writes (#38735) --- .changeset/bright-sols-write.md | 5 +++++ packages/ai/src/protocols/open-responses.ts | 15 +++++++++++---- packages/ai/src/protocols/openai-chat.ts | 11 +++++++---- packages/ai/test/provider/openai-chat.test.ts | 7 ++++--- .../ai/test/provider/openai-responses.test.ts | 7 ++++--- 5 files changed, 31 insertions(+), 14 deletions(-) create mode 100644 .changeset/bright-sols-write.md diff --git a/.changeset/bright-sols-write.md b/.changeset/bright-sols-write.md new file mode 100644 index 000000000000..b008ad247789 --- /dev/null +++ b/.changeset/bright-sols-write.md @@ -0,0 +1,5 @@ +--- +"@opencode-ai/ai": patch +--- + +Report OpenAI prompt cache write tokens in normalized usage. diff --git a/packages/ai/src/protocols/open-responses.ts b/packages/ai/src/protocols/open-responses.ts index 4cd545731965..249f6a93b43d 100644 --- a/packages/ai/src/protocols/open-responses.ts +++ b/packages/ai/src/protocols/open-responses.ts @@ -167,7 +167,12 @@ export type OpenResponsesBody = Schema.Schema.Type const OpenResponsesUsage = Schema.Struct({ input_tokens: Schema.optional(Schema.Number), - input_tokens_details: optionalNull(Schema.Struct({ cached_tokens: Schema.optional(Schema.Number) })), + input_tokens_details: optionalNull( + Schema.Struct({ + cached_tokens: Schema.optional(Schema.Number), + cache_write_tokens: Schema.optional(Schema.Number), + }), + ), output_tokens: Schema.optional(Schema.Number), output_tokens_details: optionalNull(Schema.Struct({ reasoning_tokens: Schema.optional(Schema.Number) })), total_tokens: Schema.optional(Schema.Number), @@ -540,19 +545,21 @@ export const fromRequest = Effect.fn("OpenResponses.fromRequest")(function* ( // Stream Parsing // ============================================================================= // Responses APIs report `input_tokens` (inclusive total) with a -// `cached_tokens` subset, and `output_tokens` (inclusive total) with a -// `reasoning_tokens` subset. Pass the totals through and derive the +// cached-read and cache-write subsets, and `output_tokens` (inclusive total) +// with a `reasoning_tokens` subset. Pass the totals through and derive the // non-cached breakdown. const mapUsage = (usage: OpenResponsesUsage | null | undefined, providerMetadataKey: string) => { if (!usage) return undefined const cached = usage.input_tokens_details?.cached_tokens + const cacheWrite = usage.input_tokens_details?.cache_write_tokens const reasoning = usage.output_tokens_details?.reasoning_tokens - const nonCached = ProviderShared.subtractTokens(usage.input_tokens, cached) + const nonCached = ProviderShared.subtractTokens(usage.input_tokens, ProviderShared.sumTokens(cached, cacheWrite)) return new Usage({ inputTokens: usage.input_tokens, outputTokens: usage.output_tokens, nonCachedInputTokens: nonCached, cacheReadInputTokens: cached, + cacheWriteInputTokens: cacheWrite, reasoningTokens: reasoning, totalTokens: ProviderShared.totalTokens(usage.input_tokens, usage.output_tokens, usage.total_tokens), providerMetadata: { [providerMetadataKey]: usage }, diff --git a/packages/ai/src/protocols/openai-chat.ts b/packages/ai/src/protocols/openai-chat.ts index 0e327f2abf20..e130f0bdfe4d 100644 --- a/packages/ai/src/protocols/openai-chat.ts +++ b/packages/ai/src/protocols/openai-chat.ts @@ -131,6 +131,7 @@ const OpenAIChatUsage = Schema.Struct({ prompt_tokens_details: optionalNull( Schema.Struct({ cached_tokens: Schema.optional(Schema.Number), + cache_write_tokens: Schema.optional(Schema.Number), }), ), completion_tokens_details: optionalNull( @@ -453,20 +454,22 @@ const mapFinishReason = (reason: string | null | undefined): FinishReason => { } // OpenAI Chat reports `prompt_tokens` (inclusive total) with a -// `cached_tokens` subset, and `completion_tokens` (inclusive total) with -// a `reasoning_tokens` subset. We pass the inclusive totals through and -// derive the non-cached breakdown so the `LLM.Usage` contract is +// cached-read and cache-write subsets, and `completion_tokens` (inclusive +// total) with a `reasoning_tokens` subset. We pass the inclusive totals +// through and derive the non-cached breakdown so the `LLM.Usage` contract is // satisfied on both sides. const mapUsage = (usage: OpenAIChatEvent["usage"]): Usage | undefined => { if (!usage) return undefined const cached = usage.prompt_tokens_details?.cached_tokens + const cacheWrite = usage.prompt_tokens_details?.cache_write_tokens const reasoning = usage.completion_tokens_details?.reasoning_tokens - const nonCached = ProviderShared.subtractTokens(usage.prompt_tokens, cached) + const nonCached = ProviderShared.subtractTokens(usage.prompt_tokens, ProviderShared.sumTokens(cached, cacheWrite)) return new Usage({ inputTokens: usage.prompt_tokens, outputTokens: usage.completion_tokens, nonCachedInputTokens: nonCached, cacheReadInputTokens: cached, + cacheWriteInputTokens: cacheWrite, reasoningTokens: reasoning, totalTokens: ProviderShared.totalTokens(usage.prompt_tokens, usage.completion_tokens, usage.total_tokens), providerMetadata: { openai: usage }, diff --git a/packages/ai/test/provider/openai-chat.test.ts b/packages/ai/test/provider/openai-chat.test.ts index 926ea30dba02..57ae8e865305 100644 --- a/packages/ai/test/provider/openai-chat.test.ts +++ b/packages/ai/test/provider/openai-chat.test.ts @@ -550,7 +550,7 @@ describe("OpenAI Chat route", () => { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7, - prompt_tokens_details: { cached_tokens: 1 }, + prompt_tokens_details: { cached_tokens: 1, cache_write_tokens: 2 }, completion_tokens_details: { reasoning_tokens: 0 }, }), ) @@ -558,8 +558,9 @@ describe("OpenAI Chat route", () => { const usage = new Usage({ inputTokens: 5, outputTokens: 2, - nonCachedInputTokens: 4, + nonCachedInputTokens: 2, cacheReadInputTokens: 1, + cacheWriteInputTokens: 2, reasoningTokens: 0, totalTokens: 7, providerMetadata: { @@ -567,7 +568,7 @@ describe("OpenAI Chat route", () => { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7, - prompt_tokens_details: { cached_tokens: 1 }, + prompt_tokens_details: { cached_tokens: 1, cache_write_tokens: 2 }, completion_tokens_details: { reasoning_tokens: 0 }, }, }, diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index cf66e5cbd587..43d19ed42ec2 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -832,7 +832,7 @@ describe("OpenAI Responses route", () => { input_tokens: 5, output_tokens: 2, total_tokens: 7, - input_tokens_details: { cached_tokens: 1 }, + input_tokens_details: { cached_tokens: 1, cache_write_tokens: 2 }, output_tokens_details: { reasoning_tokens: 0 }, }, }, @@ -842,8 +842,9 @@ describe("OpenAI Responses route", () => { const usage = new Usage({ inputTokens: 5, outputTokens: 2, - nonCachedInputTokens: 4, + nonCachedInputTokens: 2, cacheReadInputTokens: 1, + cacheWriteInputTokens: 2, reasoningTokens: 0, totalTokens: 7, providerMetadata: { @@ -851,7 +852,7 @@ describe("OpenAI Responses route", () => { input_tokens: 5, output_tokens: 2, total_tokens: 7, - input_tokens_details: { cached_tokens: 1 }, + input_tokens_details: { cached_tokens: 1, cache_write_tokens: 2 }, output_tokens_details: { reasoning_tokens: 0 }, }, }, From ee5460a152405b01fd61df3bd85c991ff57c1781 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:32:54 -0500 Subject: [PATCH 096/150] fix(codemode): report interrupted tool calls (#38741) --- packages/codemode/src/codemode.ts | 2 +- packages/codemode/src/tool-runtime.ts | 34 ++++----- packages/codemode/test/codemode.test.ts | 91 ++++++++++++++++++++++++- 3 files changed, 106 insertions(+), 21 deletions(-) diff --git a/packages/codemode/src/codemode.ts b/packages/codemode/src/codemode.ts index 786b8b4f4ea8..0d557169d833 100644 --- a/packages/codemode/src/codemode.ts +++ b/packages/codemode/src/codemode.ts @@ -40,7 +40,7 @@ export type ExecuteOptions = {}> = { limits?: ExecutionLimits /** Observes decoded tool input immediately before tool execution. */ onToolCallStart?: (call: ToolRuntime.ToolCallStarted) => Effect.Effect> - /** Observes each admitted tool call as it settles, with outcome and duration. */ + /** Observes each admitted tool call as it succeeds, fails, or is interrupted. */ onToolCallEnd?: (call: ToolRuntime.ToolCallEnded) => Effect.Effect> } diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index e39c30e38a93..f1e189f7335f 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -1,4 +1,4 @@ -import { Cause, Effect, Schema } from "effect" +import { Cause, Effect, Exit, Schema } from "effect" import { ToolError, toolError } from "./tool-error.js" import { decodeInput as decodeToolInput, @@ -52,7 +52,7 @@ export type ToolCallEnded = { readonly name: string readonly input: unknown readonly durationMs: number - readonly outcome: "success" | "failure" + readonly outcome: "success" | "failure" | "interrupted" readonly message?: string } @@ -495,22 +495,19 @@ export const make = ( const root = toolTrie(tools) const searchTool = makeSearchTool(searchIndex) - // End hooks observe settled success or failure; interruption emits neither outcome. const observeEnd = (effect: Effect.Effect, call: ToolCallStarted): Effect.Effect => { const onEnd = hooks?.onToolCallEnd if (onEnd === undefined) return effect const startedAt = Date.now() return effect.pipe( - Effect.tap(() => onEnd({ ...call, durationMs: Date.now() - startedAt, outcome: "success" })), - Effect.tapError((error) => { + Effect.onExit((exit) => { + const durationMs = Date.now() - startedAt + if (Exit.isSuccess(exit)) return onEnd({ ...call, durationMs, outcome: "success" }) + if (Cause.hasInterruptsOnly(exit.cause)) return onEnd({ ...call, durationMs, outcome: "interrupted" }) + const error = Cause.squash(exit.cause) const message = error instanceof ToolError || error instanceof ToolRuntimeError ? error.message : "Tool execution failed" - return onEnd({ - ...call, - durationMs: Date.now() - startedAt, - outcome: "failure", - message, - }) + return onEnd({ ...call, durationMs, outcome: "failure", message }) }), ) } @@ -528,12 +525,6 @@ export const make = ( calls.push(call) } - const recordAndObserve = (name: string, input: unknown) => - Effect.sync(() => { - recordCall({ name }) - return calls.length - 1 - }).pipe(Effect.tap((index) => hooks?.onToolCallStart?.({ index, name, input }) ?? Effect.void)) - const executeTool = (name: string, tool: Tool, externalArgs: Array) => Effect.gen(function* () { if (externalArgs.length !== 1) @@ -547,9 +538,14 @@ export const make = ( name === "search" ? [] : ["The signature may have changed. Use search to get the current signature."], ), }) - const index = yield* recordAndObserve(name, input) + const index = yield* Effect.sync(() => { + recordCall({ name }) + return calls.length - 1 + }) + const call = { index, name, input } return yield* observeEnd( Effect.gen(function* () { + if (hooks?.onToolCallStart !== undefined) yield* hooks.onToolCallStart(call) const raw = yield* runHost(Effect.suspend(() => tool.execute(input))) const result = yield* Effect.try({ try: () => decodeToolOutput(tool, raw), @@ -557,7 +553,7 @@ export const make = ( }) return yield* decodeOutput(result, name) }), - { index, name, input }, + call, ) }) diff --git a/packages/codemode/test/codemode.test.ts b/packages/codemode/test/codemode.test.ts index 86b1e076f5e8..cac1d4602492 100644 --- a/packages/codemode/test/codemode.test.ts +++ b/packages/codemode/test/codemode.test.ts @@ -189,7 +189,12 @@ describe("CodeMode tool-call observation", () => { description: "Look up a value", input: Schema.Struct({ query: Schema.String }), output: Schema.String, - execute: ({ query }) => (query === "boom" ? Effect.fail(toolError("Lookup refused")) : Effect.succeed(query)), + execute: ({ query }) => + query === "boom" + ? Effect.fail(toolError("Lookup refused")) + : query === "defect" + ? Effect.die("broken") + : Effect.succeed(query), }) const runtime = CodeMode.make({ @@ -215,14 +220,98 @@ describe("CodeMode tool-call observation", () => { expect(success.ok).toBe(true) const failure = await Effect.runPromise(runtime.execute(`return await tools.context.lookup({ query: "boom" })`)) expect(failure.ok).toBe(false) + const defect = await Effect.runPromise(runtime.execute(`return await tools.context.lookup({ query: "defect" })`)) + expect(defect.ok).toBe(false) expect(events).toStrictEqual([ { phase: "start", index: 0, name: "context.lookup" }, { phase: "end", index: 0, name: "context.lookup", outcome: "success" }, { phase: "start", index: 0, name: "context.lookup" }, { phase: "end", index: 0, name: "context.lookup", outcome: "failure", message: "Lookup refused" }, + { phase: "start", index: 0, name: "context.lookup" }, + { phase: "end", index: 0, name: "context.lookup", outcome: "failure", message: "Tool execution failed" }, ]) }) + + test("observes interrupted calls", async () => { + const events: Array = [] + const call = Tool.make({ + description: "Interrupt", + input: Schema.Struct({}), + output: Schema.String, + execute: () => Effect.interrupt, + }) + const exit = await Effect.runPromiseExit( + CodeMode.make({ + tools: { host: { call } }, + onToolCallStart: () => Effect.sync(() => events.push("start")), + onToolCallEnd: (call) => Effect.sync(() => events.push(`end:${call.outcome}`)), + }).execute("return await tools.host.call({})"), + ) + + expect(exit._tag).toBe("Failure") + expect(events).toEqual(["start", "end:interrupted"]) + }) + + test("observes running calls interrupted during completion", async () => { + const events: Array = [] + const call = Tool.make({ + description: "Pending", + input: Schema.Struct({}), + output: Schema.String, + execute: () => Effect.never, + }) + const result = await Effect.runPromise( + CodeMode.make({ + tools: { host: { call } }, + onToolCallStart: () => Effect.sync(() => events.push("start")), + onToolCallEnd: (call) => Effect.sync(() => events.push(`end:${call.outcome}`)), + }).execute('tools.host.call({}); return "done"'), + ) + + expect(result).toMatchObject({ ok: true, value: "done" }) + expect(events).toEqual(["start", "end:interrupted"]) + }) + + test("ends calls interrupted during start observation", async () => { + const events: Array = [] + const call = Tool.make({ + description: "Unused", + input: Schema.Struct({}), + output: Schema.String, + execute: () => Effect.succeed("unused"), + }) + const exit = await Effect.runPromiseExit( + CodeMode.make({ + tools: { host: { call } }, + onToolCallStart: () => Effect.interrupt, + onToolCallEnd: (call) => Effect.sync(() => events.push(call.outcome)), + }).execute("return await tools.host.call({})"), + ) + + expect(exit._tag).toBe("Failure") + expect(events).toEqual(["interrupted"]) + }) + + test("observes calls interrupted by the execution timeout", async () => { + const outcomes: Array = [] + const call = Tool.make({ + description: "Pending", + input: Schema.Struct({}), + output: Schema.String, + execute: () => Effect.never, + }) + const result = await Effect.runPromise( + CodeMode.make({ + tools: { host: { call } }, + limits: { timeoutMs: 10 }, + onToolCallEnd: (call) => Effect.sync(() => outcomes.push(call.outcome)), + }).execute("return await tools.host.call({})"), + ) + + expect(result).toMatchObject({ ok: false, error: { kind: "TimeoutExceeded" } }) + expect(outcomes).toEqual(["interrupted"]) + }) }) describe("CodeMode console capture", () => { From 3193f3aa955ab1c53a6588c888087d9b763283f6 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 24 Jul 2026 14:35:13 -0400 Subject: [PATCH 097/150] fix(tui): flag likely cache busts accurately (#38727) --- packages/tui/src/routes/session/index.tsx | 32 ++++--- packages/tui/src/routes/session/rows.ts | 31 +++++-- .../tui/test/cli/tui/session-rows.test.ts | 88 ++++++++++++++++++- 3 files changed, 131 insertions(+), 20 deletions(-) diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index d4f9f2f00a91..a771c8c018f5 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -81,7 +81,15 @@ import { PluginSlot } from "../../plugin/context" import { Keymap, type KeymapCommand } from "../../context/keymap" import { usePathFormatter } from "../../context/path-format" import { useLocation } from "../../context/location" -import { createSessionRows, messageBoundaryIDs, resolvePart, type PartRef, type SessionRow } from "./rows" +import { + cacheReuseDrop, + createSessionRows, + messageBoundaryIDs, + resolvePart, + type CacheUsage, + type PartRef, + type SessionRow, +} from "./rows" import { switchLabel } from "../../util/model" import { findMessageBoundary, messageNavigationSlack } from "./message-navigation" import { stringWidth } from "../../util/string-width" @@ -1079,7 +1087,7 @@ function SessionRowView(props: SessionRowViewProps) { {(row) => ( )} @@ -1091,13 +1099,13 @@ function SessionRowView(props: SessionRowViewProps) { function TurnTokenUsage(props: { messageIDs: string[] - previousCacheRead?: number + previousCache?: CacheUsage message: (messageID: string) => SessionMessageInfo | undefined }) { const config = useConfig() const { themeV2 } = useTheme() const steps = createMemo(() => { - let previousCacheRead = props.previousCacheRead + let previousCache = props.previousCache return props.messageIDs.flatMap((messageID) => { const message = props.message(messageID) if (message?.type !== "assistant" || !message.tokens) return [] @@ -1109,18 +1117,16 @@ function TurnTokenUsage(props: { message.tokens.cache.write if (total === 0) return [] const newTokens = total - message.tokens.cache.read - const cacheBust = - previousCacheRead !== undefined && message.tokens.cache.read < previousCacheRead - ? previousCacheRead - message.tokens.cache.read - : undefined - previousCacheRead = message.tokens.cache.read + const currentCache = { read: message.tokens.cache.read, model: message.model } + const reuseDrop = cacheReuseDrop(previousCache, currentCache) + previousCache = currentCache return [ { finish: message.finish === "tool-calls" ? "tool-call" : (message.finish ?? "unknown"), newTokens, cached: message.tokens.cache.read, total, - cacheBust, + reuseDrop, }, ] }) @@ -1165,9 +1171,9 @@ function TurnTokenUsage(props: { {" "} {item.total.toLocaleString().padStart(columns().total)} - - - ! Cache bust: {item.cacheBust?.toLocaleString()} fewer cached tokens than the previous step + + + ! Likely cache bust: {item.reuseDrop?.toLocaleString()} fewer cached tokens than the previous step diff --git a/packages/tui/src/routes/session/rows.ts b/packages/tui/src/routes/session/rows.ts index c6b0d744c91d..3c2018401190 100644 --- a/packages/tui/src/routes/session/rows.ts +++ b/packages/tui/src/routes/session/rows.ts @@ -10,6 +10,11 @@ export type PartRef = { partID: string } +export type CacheUsage = { + read: number + model: SessionMessageAssistant["model"] +} + export type SessionRow = | { type: "message"; messageID: string } | { type: "compaction-queued"; inputID: string } @@ -28,7 +33,7 @@ export type SessionRow = completed: boolean } | { type: "assistant-footer"; messageID: string } - | { type: "turn-usage"; messageIDs: string[]; previousCacheRead?: number } + | { type: "turn-usage"; messageIDs: string[]; previousCache?: CacheUsage } export function createSessionRows(sessionID: Accessor) { const data = useData() @@ -280,7 +285,7 @@ export function reduceSessionRows( const pendingCompactions = messages.filter((message) => message.type === "compaction" && message.status === "running") const pending = new Set([...pendingCompactions.map((message) => message.id), ...inputs]) const usage = turnTokens - ? { steps: [] as SessionMessageAssistant[], previousTurnCacheRead: undefined as number | undefined } + ? { steps: [] as SessionMessageAssistant[], previousTurnCache: undefined as CacheUsage | undefined } : undefined return [ ...messages.filter((message) => !pending.has(message.id)), @@ -289,6 +294,8 @@ export function reduceSessionRows( ].reduce((rows, message) => { if (message.type !== "assistant") { if (message.type === "synthetic" && !message.description?.trim()) return rows + if (message.type === "compaction" && message.status === "completed" && usage) + usage.previousTurnCache = undefined if (!pending.has(message.id)) completePrevious(rows) rows.push({ type: "message", messageID: message.id }) return rows @@ -312,11 +319,9 @@ export function reduceSessionRows( rows.push({ type: "turn-usage", messageIDs: stepsWithUsage.map((step) => step.id), - ...(usage.previousTurnCacheRead === undefined - ? {} - : { previousCacheRead: usage.previousTurnCacheRead }), + ...(usage.previousTurnCache === undefined ? {} : { previousCache: usage.previousTurnCache }), }) - usage.previousTurnCacheRead = last.tokens.cache.read + usage.previousTurnCache = { read: last.tokens.cache.read, model: last.model } } usage.steps.length = 0 } @@ -324,6 +329,20 @@ export function reduceSessionRows( }, []) } +export function cacheReuseDrop(previous: CacheUsage | undefined, current: CacheUsage) { + if (previous === undefined) return + if ( + previous.model.providerID !== current.model.providerID || + previous.model.id !== current.model.id || + previous.model.variant !== current.model.variant + ) + return + const drop = previous.read - current.read + // OpenAI cache reads can move between one and two 1,024-token buckets without a material loss of reuse. + if (current.model.providerID === "openai" && drop >= 1_024 && drop <= 2_048) return + return drop > 0 ? drop : undefined +} + function hasTokenUsage( message: SessionMessageAssistant, ): message is SessionMessageAssistant & { tokens: NonNullable } { diff --git a/packages/tui/test/cli/tui/session-rows.test.ts b/packages/tui/test/cli/tui/session-rows.test.ts index c84237a3b768..4e7f87152db9 100644 --- a/packages/tui/test/cli/tui/session-rows.test.ts +++ b/packages/tui/test/cli/tui/session-rows.test.ts @@ -1,6 +1,92 @@ import { expect, test } from "bun:test" import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client" -import { messageBoundaryIDs, reduceSessionRows } from "../../../src/routes/session/rows" +import { cacheReuseDrop, messageBoundaryIDs, reduceSessionRows } from "../../../src/routes/session/rows" + +test("filters OpenAI cache quantization from cache reuse drops", () => { + const openai = { id: "gpt", providerID: "openai" } + expect(cacheReuseDrop(undefined, { read: 10_000, model: openai })).toBeUndefined() + expect(cacheReuseDrop({ read: 10_000, model: openai }, { read: 11_000, model: openai })).toBeUndefined() + expect(cacheReuseDrop({ read: 10_000, model: openai }, { read: 8_977, model: openai })).toBe(1_023) + expect(cacheReuseDrop({ read: 10_000, model: openai }, { read: 8_976, model: openai })).toBeUndefined() + expect(cacheReuseDrop({ read: 10_000, model: openai }, { read: 8_500, model: openai })).toBeUndefined() + expect(cacheReuseDrop({ read: 10_000, model: openai }, { read: 7_952, model: openai })).toBeUndefined() + expect(cacheReuseDrop({ read: 10_000, model: openai }, { read: 7_951, model: openai })).toBe(2_049) +}) + +test("compares cache reuse only for the same model", () => { + const previous = { read: 10_000, model: { id: "claude", providerID: "anthropic" } } + expect(cacheReuseDrop(previous, { read: 8_976, model: { id: "gpt", providerID: "openai" } })).toBeUndefined() + expect(cacheReuseDrop(previous, { read: 8_976, model: { id: "claude", providerID: "anthropic" } })).toBe(1_024) + expect( + cacheReuseDrop( + { read: 10_000, model: { id: "gpt", providerID: "openai", variant: "low" } }, + { read: 8_976, model: { id: "gpt", providerID: "openai", variant: "high" } }, + ), + ).toBeUndefined() +}) + +test("carries model identity with the cross-turn cache baseline", () => { + const first = assistant("assistant-1", []) + first.model = { id: "claude", providerID: "anthropic" } + first.finish = "stop" + first.tokens = { input: 1, output: 0, reasoning: 0, cache: { read: 10_000, write: 0 } } + const second = assistant("assistant-2", []) + second.model = { id: "gpt", providerID: "openai" } + second.finish = "stop" + second.tokens = { input: 1, output: 0, reasoning: 0, cache: { read: 8_976, write: 0 } } + + const rows = reduceSessionRows( + [ + { type: "user", id: "user-1", text: "First", time: { created: 0 } }, + first, + { type: "user", id: "user-2", text: "Second", time: { created: 2 } }, + second, + ], + new Set(), + true, + ).filter((row) => row.type === "turn-usage") + + expect(rows).toEqual([ + { type: "turn-usage", messageIDs: ["assistant-1"] }, + { + type: "turn-usage", + messageIDs: ["assistant-2"], + previousCache: { read: 10_000, model: { id: "claude", providerID: "anthropic" } }, + }, + ]) +}) + +test("resets the cross-turn cache baseline after compaction", () => { + const first = assistant("assistant-1", []) + first.finish = "stop" + first.tokens = { input: 1, output: 0, reasoning: 0, cache: { read: 370_176, write: 0 } } + const second = assistant("assistant-2", []) + second.finish = "stop" + second.tokens = { input: 1, output: 0, reasoning: 0, cache: { read: 13_824, write: 0 } } + + const rows = reduceSessionRows( + [ + first, + { + type: "compaction", + id: "compaction-1", + status: "completed", + reason: "auto", + summary: "Compacted context", + recent: "", + time: { created: 2 }, + }, + second, + ], + new Set(), + true, + ).filter((row) => row.type === "turn-usage") + + expect(rows).toEqual([ + { type: "turn-usage", messageIDs: ["assistant-1"] }, + { type: "turn-usage", messageIDs: ["assistant-2"] }, + ]) +}) test("assigns assistant boundaries to the first rendered row instead of the first text row", () => { const messages: SessionMessageInfo[] = [ From d66d0cb9042182472dc4007f5e280d0858c09500 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:44:59 -0500 Subject: [PATCH 098/150] fix(core): clarify Code Mode tool availability (#38745) --- packages/core/src/codemode/instructions.ts | 2 ++ packages/core/test/codemode/catalog.test.ts | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/core/src/codemode/instructions.ts b/packages/core/src/codemode/instructions.ts index 66e5150dc536..3a546d79c2d1 100644 --- a/packages/core/src/codemode/instructions.ts +++ b/packages/core/src/codemode/instructions.ts @@ -8,6 +8,8 @@ import { CodeModeCatalog } from "./catalog" // prettier-ignore const prompt = (hasMoreTools: boolean) => `Run JavaScript to orchestrate tool calls and compose their results. Imports, direct filesystem access, and timers are unavailable. Do not use \`fetch\`; all external access goes through \`tools\`. +Inside Code Mode, \`tools\` contains only the tools shown below${hasMoreTools ? " or returned by `search`" : ""}; surrounding top-level agent tools are not available and must not be called from the code. + Prefer an explicit \`return\`; if omitted, the final top-level expression becomes the result. Await tool calls before returning; any calls still pending when execution ends are interrupted. Run independent calls concurrently with \`Promise.all\`. Do not infer or normalize tool names; use only the exact signatures shown below${hasMoreTools ? " or returned by `search`" : ""}, preserving bracket notation such as \`tools.["tool-name"](input)\`.${hasMoreTools ? ` diff --git a/packages/core/test/codemode/catalog.test.ts b/packages/core/test/codemode/catalog.test.ts index 176fe1c6350e..6831b46ad56c 100644 --- a/packages/core/test/codemode/catalog.test.ts +++ b/packages/core/test/codemode/catalog.test.ts @@ -68,6 +68,9 @@ describe("CodeModeInstructions.render", () => { expect(instructions).not.toContain("## Search") expect(instructions).toContain("Do not infer or normalize tool names") expect(instructions).toContain('`tools.["tool-name"](input)`') + expect(instructions).toContain( + "`tools` contains only the tools shown below; surrounding top-level agent tools are not available and must not be called from the code.", + ) }) test("describes the runtime and execution lifecycle concisely", () => { @@ -88,6 +91,9 @@ describe("CodeModeInstructions.render", () => { expect(partial).toContain("- orders (1 tool, none shown)") expect(partial).toContain("## Search") expect(partial).toContain("Only some tool signatures are shown.") + expect(partial).toContain( + "`tools` contains only the tools shown below or returned by `search`; surrounding top-level agent tools are not available and must not be called from the code.", + ) expect(partial).toContain("- search(input: {") expect(partial).toContain(" limit?: number,\n offset?: number,") expect(partial).toContain("or returned by `search`") @@ -169,7 +175,7 @@ describe("CodeModeInstructions.update", () => { expect(text).toContain("This catalog supersedes the previous Code Mode tool catalog.") expect(text).toContain("## Available tools") expect(text).not.toContain("## Search") - expect(text).not.toContain("must not be called") + expect(text).not.toContain("The following tools are no longer available") }) test("renders namespace-only deltas without persisting hidden tool entries", () => { From 49bec25ae59d72406affc263de549809cd5ef56c Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:23:35 -0500 Subject: [PATCH 099/150] fix(ai): align Anthropic stream handling (#38733) --- .../ai/src/protocols/anthropic-messages.ts | 89 ++++++++--- packages/ai/src/protocols/utils/lifecycle.ts | 15 +- .../test/provider/anthropic-messages.test.ts | 139 ++++++++++++++++++ packages/ai/test/tool-runtime.test.ts | 3 + 4 files changed, 217 insertions(+), 29 deletions(-) diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index b3a35f986e86..2812e0136f77 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -9,6 +9,7 @@ import { LLMEvent, Usage, type CacheHint, + type FinishReasonDetails, type FinishReason, type JsonSchema, type LLMRequest, @@ -288,7 +289,12 @@ type AnthropicEvent = Schema.Schema.Type interface ParserState { readonly tools: ToolStream.State + readonly reasoningSignatures: Readonly> readonly usage?: Usage + readonly pendingFinish?: { + readonly reason: FinishReasonDetails + readonly providerMetadata?: ProviderMetadata + } readonly lifecycle: Lifecycle.State } @@ -763,6 +769,10 @@ const onContentBlockStart = (state: ParserState, event: AnthropicEvent): StepRes tools: ToolStream.start(state.tools, event.index, { id: block.id ?? String(event.index), name: block.name ?? "", + input: + block.input !== undefined && (!ProviderShared.isRecord(block.input) || Object.keys(block.input).length > 0) + ? ProviderShared.encodeJson(block.input) + : undefined, providerExecuted: block.type === "server_tool_use", }), }, @@ -777,20 +787,31 @@ const onContentBlockStart = (state: ParserState, event: AnthropicEvent): StepRes ] } - if (block.type === "text" && block.text) { + if (block.type === "text" && block.text !== undefined) { const events: LLMEvent[] = [] + const id = `text-${event.index ?? 0}` + const lifecycle = Lifecycle.textStart(state.lifecycle, events, id) return [ - { ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, `text-${event.index ?? 0}`, block.text) }, + { ...state, lifecycle: block.text ? Lifecycle.textDelta(lifecycle, events, id, block.text) : lifecycle }, events, ] } - if (block.type === "thinking" && block.thinking) { + if (block.type === "thinking" && block.thinking !== undefined) { const events: LLMEvent[] = [] + const id = `reasoning-${event.index ?? 0}` + const providerMetadata = block.signature === undefined ? undefined : anthropicMetadata({ signature: block.signature }) + const lifecycle = Lifecycle.reasoningStart(state.lifecycle, events, id, providerMetadata) return [ { ...state, - lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${event.index ?? 0}`, block.thinking), + lifecycle: block.thinking + ? Lifecycle.reasoningDelta(lifecycle, events, id, block.thinking, providerMetadata) + : lifecycle, + reasoningSignatures: + event.index === undefined || block.signature === undefined + ? state.reasoningSignatures + : { ...state.reasoningSignatures, [event.index]: block.signature }, }, events, ] @@ -799,7 +820,7 @@ const onContentBlockStart = (state: ParserState, event: AnthropicEvent): StepRes // Redacted thinking surfaces as an empty reasoning part carrying the opaque // payload as `redactedData` metadata (same model as Vercel's // @ai-sdk/anthropic). The existing content_block_stop closes the part. - if (block.type === "redacted_thinking" && block.data) { + if (block.type === "redacted_thinking" && block.data !== undefined) { const events: LLMEvent[] = [] return [ { @@ -847,18 +868,13 @@ const onContentBlockDelta = Effect.fn("AnthropicMessages.onContentBlockDelta")(f } if (delta?.type === "signature_delta" && delta.signature) { - const events: LLMEvent[] = [] + const index = event.index ?? 0 return [ { ...state, - lifecycle: Lifecycle.reasoningEnd( - state.lifecycle, - events, - `reasoning-${event.index ?? 0}`, - anthropicMetadata({ signature: delta.signature }), - ), + reasoningSignatures: { ...state.reasoningSignatures, [index]: delta.signature }, }, - events, + NO_EVENTS, ] satisfies StepResult } @@ -889,31 +905,53 @@ const onContentBlockStop = Effect.fn("AnthropicMessages.onContentBlockStop")(fun const result = yield* ToolStream.finish(ADAPTER, state.tools, event.index) const events: LLMEvent[] = [] const resultEvents = result.events ?? [] + const signature = state.reasoningSignatures[event.index] const lifecycle = resultEvents.length ? Lifecycle.stepStart(state.lifecycle, events) : Lifecycle.reasoningEnd( Lifecycle.textEnd(state.lifecycle, events, `text-${event.index}`), events, `reasoning-${event.index}`, + signature === undefined ? undefined : anthropicMetadata({ signature }), ) events.push(...resultEvents) - return [{ ...state, lifecycle, tools: result.tools }, events] satisfies StepResult + const reasoningSignatures = { ...state.reasoningSignatures } + delete reasoningSignatures[event.index] + return [{ ...state, lifecycle, tools: result.tools, reasoningSignatures }, events] satisfies StepResult }) const onMessageDelta = (state: ParserState, event: AnthropicEvent): StepResult => { const usage = mergeUsage(state.usage, mapUsage(event.usage)) + return [ + { + ...state, + usage, + pendingFinish: { + reason: { + normalized: mapFinishReason(event.delta?.stop_reason), + raw: event.delta?.stop_reason ?? undefined, + }, + providerMetadata: + event.delta?.stop_sequence === null || event.delta?.stop_sequence === undefined + ? undefined + : anthropicMetadata({ stopSequence: event.delta.stop_sequence }), + }, + }, + NO_EVENTS, + ] +} + +const onMessageStop = (state: ParserState): StepResult => { const events: LLMEvent[] = [] const lifecycle = Lifecycle.finish(state.lifecycle, events, { - reason: { - normalized: mapFinishReason(event.delta?.stop_reason), - raw: event.delta?.stop_reason ?? undefined, + reason: state.pendingFinish?.reason ?? { + normalized: "unknown", + raw: undefined, }, - usage, - providerMetadata: event.delta?.stop_sequence - ? anthropicMetadata({ stopSequence: event.delta.stop_sequence }) - : undefined, + usage: state.usage, + providerMetadata: state.pendingFinish?.providerMetadata, }) - return [{ ...state, lifecycle, usage }, events] + return [{ ...state, lifecycle }, events] } // Prefix `error.type` so overloads, rate limits, and quota errors are visible @@ -938,6 +976,7 @@ const step = (state: ParserState, event: AnthropicEvent) => { if (event.type === "content_block_delta") return onContentBlockDelta(state, event) if (event.type === "content_block_stop") return onContentBlockStop(state, event) if (event.type === "message_delta") return Effect.succeed(onMessageDelta(state, event)) + if (event.type === "message_stop") return Effect.succeed(onMessageStop(state)) if (event.type === "error") return onError(event) return Effect.succeed([state, NO_EVENTS]) } @@ -958,7 +997,11 @@ export const protocol = Protocol.make({ }, stream: { event: Protocol.jsonEvent(AnthropicEvent), - initial: () => ({ tools: ToolStream.empty(), lifecycle: Lifecycle.initial() }), + initial: () => ({ + tools: ToolStream.empty(), + reasoningSignatures: {}, + lifecycle: Lifecycle.initial(), + }), step, }, }) diff --git a/packages/ai/src/protocols/utils/lifecycle.ts b/packages/ai/src/protocols/utils/lifecycle.ts index 761cff3690f6..1ee8b608f672 100644 --- a/packages/ai/src/protocols/utils/lifecycle.ts +++ b/packages/ai/src/protocols/utils/lifecycle.ts @@ -14,16 +14,19 @@ export const stepStart = (state: State, events: LLMEvent[]): State => { return { ...state, stepStarted: true } } -export const textDelta = (state: State, events: LLMEvent[], id: string, text: string): State => { +export const textStart = (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata): State => { + if (state.text.has(id)) return state const stepped = stepStart(state, events) - if (stepped.text.has(id)) { - events.push(LLMEvent.textDelta({ id, text })) - return stepped - } - events.push(LLMEvent.textStart({ id }), LLMEvent.textDelta({ id, text })) + events.push(LLMEvent.textStart({ id, providerMetadata })) return { ...stepped, text: new Set([...stepped.text, id]) } } +export const textDelta = (state: State, events: LLMEvent[], id: string, text: string): State => { + const started = textStart(state, events, id) + events.push(LLMEvent.textDelta({ id, text })) + return started +} + export const reasoningStart = ( state: State, events: LLMEvent[], diff --git a/packages/ai/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts index acdab6a7b0c2..004a378bcff2 100644 --- a/packages/ai/test/provider/anthropic-messages.test.ts +++ b/packages/ai/test/provider/anthropic-messages.test.ts @@ -506,6 +506,7 @@ describe("Anthropic Messages route", () => { expect(response.events.find((event) => event.type === "reasoning-end")).toMatchObject({ providerMetadata: { anthropic: { signature: "sig_1" } }, }) + expect(response.events.find((event) => event.type === "reasoning-delta" && event.text === "")).toBeUndefined() expect(response.message.content).toEqual([ { type: "text", text: "Hello!" }, { type: "reasoning", text: "thinking", providerMetadata: { anthropic: { signature: "sig_1" } } }, @@ -518,6 +519,139 @@ describe("Anthropic Messages route", () => { }), ) + it.effect("requires message_stop before completing a streamed message", () => + Effect.gen(function* () { + const error = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { type: "message_start", message: { usage: { input_tokens: 5 } } }, + { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } }, + { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Hello" } }, + { type: "content_block_stop", index: 0 }, + { type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } }, + ), + ), + ), + Effect.flip, + ) + + expect(error.reason).toMatchObject({ + _tag: "InvalidProviderOutput", + message: "Provider stream ended without a terminal finish event", + }) + }), + ) + + it.effect("round-trips omitted thinking carried only by a signature delta", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { type: "message_start", message: { usage: { input_tokens: 5 } } }, + { + type: "content_block_start", + index: 0, + content_block: { type: "thinking", thinking: "", signature: "" }, + }, + { type: "content_block_delta", index: 0, delta: { type: "signature_delta", signature: "sig_1" } }, + { type: "content_block_stop", index: 0 }, + { type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } }, + { type: "message_stop" }, + ), + ), + ), + ) + + expect(response.message.content).toEqual([ + { type: "reasoning", text: "", providerMetadata: { anthropic: { signature: "sig_1" } } }, + ]) + + const prepared = yield* LLMClient.prepare( + LLM.request({ model, messages: [response.message], cache: "none" }), + ) + expect(prepared.body.messages).toEqual([ + { role: "assistant", content: [{ type: "thinking", thinking: "", signature: "sig_1" }] }, + ]) + }), + ) + + it.effect("retains a thinking signature supplied in content_block_start", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { type: "message_start", message: { usage: { input_tokens: 5 } } }, + { + type: "content_block_start", + index: 0, + content_block: { type: "thinking", thinking: "", signature: "sig_1" }, + }, + { type: "content_block_stop", index: 0 }, + { type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } }, + { type: "message_stop" }, + ), + ), + ), + ) + + expect(response.message.content).toEqual([ + { type: "reasoning", text: "", providerMetadata: { anthropic: { signature: "sig_1" } } }, + ]) + expect(response.events.find((event) => event.type === "reasoning-end")).toMatchObject({ + providerMetadata: { anthropic: { signature: "sig_1" } }, + }) + }), + ) + + it.effect("retains complete tool input from content_block_start", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { type: "message_start", message: { usage: { input_tokens: 5 } } }, + { + type: "content_block_start", + index: 0, + content_block: { type: "tool_use", id: "call_1", name: "lookup", input: { query: "weather" } }, + }, + { type: "content_block_stop", index: 0 }, + { type: "message_delta", delta: { stop_reason: "tool_use" }, usage: { output_tokens: 1 } }, + { type: "message_stop" }, + ), + ), + ), + ) + + expect(response.toolCalls).toMatchObject([ + { id: "call_1", name: "lookup", input: { query: "weather" } }, + ]) + }), + ) + + it.effect("retains empty text blocks", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { type: "message_start", message: { usage: { input_tokens: 5 } } }, + { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } }, + { type: "content_block_stop", index: 0 }, + { type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } }, + { type: "message_stop" }, + ), + ), + ), + ) + + expect(response.message.content).toEqual([{ type: "text", text: "" }]) + }), + ) + it.effect("parses redacted thinking into empty reasoning with redactedData metadata", () => Effect.gen(function* () { const body = sseEvents( @@ -629,6 +763,7 @@ describe("Anthropic Messages route", () => { delta: { stop_reason: "model_context_window_exceeded" }, usage: { output_tokens: 1 }, }, + { type: "message_stop" }, ), ), ), @@ -646,6 +781,7 @@ describe("Anthropic Messages route", () => { sseEvents( { type: "message_start", message: { usage: { input_tokens: 5 } } }, { type: "message_delta", delta: { stop_reason: "pause_turn" }, usage: { output_tokens: 1 } }, + { type: "message_stop" }, ), ), ), @@ -664,6 +800,7 @@ describe("Anthropic Messages route", () => { { type: "content_block_delta", index: 0, delta: { type: "input_json_delta", partial_json: ':"weather"}' } }, { type: "content_block_stop", index: 0 }, { type: "message_delta", delta: { stop_reason: "tool_use" }, usage: { output_tokens: 1 } }, + { type: "message_stop" }, ) const response = yield* LLMClient.generate( LLMRequest.update(request, { @@ -849,6 +986,7 @@ describe("Anthropic Messages route", () => { { type: "content_block_delta", index: 2, delta: { type: "text_delta", text: "Found it." } }, { type: "content_block_stop", index: 2 }, { type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 8 } }, + { type: "message_stop" }, ) const response = yield* LLMClient.generate( LLMRequest.update(request, { @@ -912,6 +1050,7 @@ describe("Anthropic Messages route", () => { }, { type: "content_block_stop", index: 1 }, { type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } }, + { type: "message_stop" }, ) const response = yield* LLMClient.generate( LLMRequest.update(request, { diff --git a/packages/ai/test/tool-runtime.test.ts b/packages/ai/test/tool-runtime.test.ts index aa4c85f6efee..356ee779c8b9 100644 --- a/packages/ai/test/tool-runtime.test.ts +++ b/packages/ai/test/tool-runtime.test.ts @@ -539,6 +539,7 @@ describe("LLMClient tools", () => { }, { type: "content_block_stop", index: 1 }, { type: "message_delta", delta: { stop_reason: "tool_use" }, usage: { output_tokens: 5 } }, + { type: "message_stop" }, ) : sseEvents( { type: "message_start", message: { usage: { input_tokens: 5 } } }, @@ -546,6 +547,7 @@ describe("LLMClient tools", () => { { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Done." } }, { type: "content_block_stop", index: 0 }, { type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } }, + { type: "message_stop" }, ), { headers: { "content-type": "text/event-stream" } }, ) @@ -801,6 +803,7 @@ describe("LLMClient tools", () => { { type: "content_block_delta", index: 2, delta: { type: "text_delta", text: "Done." } }, { type: "content_block_stop", index: 2 }, { type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 8 } }, + { type: "message_stop" }, ), { headers: { "content-type": "text/event-stream" } }, ) From b31747124b529b32670b3c7da0d20cc8ecc17de7 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:46:46 -0500 Subject: [PATCH 100/150] fix(core): stream Code Mode tool progress (#38718) --- packages/core/src/tool/execute.ts | 49 ++++++++++--------- .../test/session-runner-tool-registry.test.ts | 15 ++++-- packages/core/test/tool-execute.test.ts | 45 ++++++++++++++++- packages/tui/src/routes/session/index.tsx | 2 +- 4 files changed, 82 insertions(+), 29 deletions(-) diff --git a/packages/core/src/tool/execute.ts b/packages/core/src/tool/execute.ts index a9d005af4567..ddf7dbe48121 100644 --- a/packages/core/src/tool/execute.ts +++ b/packages/core/src/tool/execute.ts @@ -3,7 +3,7 @@ export type { Registration } from "./tool" import { CodeMode, Tool, toolError } from "@opencode-ai/codemode" import type { ToolContent } from "@opencode-ai/ai" -import { Effect, Ref, Schema } from "effect" +import { Effect, Ref, Schema, Semaphore } from "effect" import { execute, make, toLLMDefinition, type Content, type Metadata, type Registration } from "./tool" const ExecuteFile = Schema.Struct({ @@ -50,12 +50,11 @@ export const create = (registrations: ReadonlyMap) => { const callIndex = yield* Ref.make(0) const files = yield* Ref.make>([]) const calls = yield* Ref.make>([]) - // TODO: Publish live call-list updates once V2 has a generic tool progress API. - const finalCalls = Ref.get(calls).pipe( - Effect.map((items) => - items.map((call) => (call.status === "running" ? { ...call, status: "error" as const } : call)), - ), - ) + const lock = Semaphore.makeUnsafe(1) + const updateCalls = (update: (items: Array) => Array) => + lock.withPermit( + Ref.updateAndGet(calls, update).pipe(Effect.flatMap((toolCalls) => context.progress({ toolCalls }))), + ) const result = yield* runtime( registrations, (name, registration, input) => @@ -66,7 +65,7 @@ export const create = (registrations: ReadonlyMap) => { agent: context.agent, messageID: context.messageID, callID: context.callID, - progress: context.progress, + progress: () => Effect.void, }).pipe(Effect.mapError((failure) => toolError(failure.message, failure))) const outputFileParts = outputFiles(executed.content) if (outputFileParts.length > 0) @@ -74,26 +73,28 @@ export const create = (registrations: ReadonlyMap) => { return executed.output }), { - onToolCallStart: ({ index, name, input }) => - Effect.gen(function* () { - const shown = displayInput(input) - yield* Ref.update(calls, (items) => { - const next = [...items] - next[index] = { tool: name, status: "running", ...(shown ? { input: shown } : {}) } - return next - }) - }), - onToolCallEnd: ({ index, outcome }) => - Ref.update(calls, (items) => { - const current = items[index] - if (!current) return items + onToolCallStart: ({ index, name, input }) => { + const shown = displayInput(input) + return updateCalls((items) => { + const next = [...items] + next[index] = { tool: name, status: "running", ...(shown ? { input: shown } : {}) } + return next + }) + }, + onToolCallEnd: ({ index, name, input, outcome }) => { + const shown = displayInput(input) + return updateCalls((items) => { const next = [...items] - next[index] = { ...current, status: outcome === "success" ? "completed" : "error" } + next[index] = { + ...(items[index] ?? { tool: name, ...(shown ? { input: shown } : {}) }), + status: outcome === "success" ? "completed" : "error", + } return next - }), + }) + }, }, ).execute(code) - const toolCalls = yield* finalCalls + const toolCalls = yield* Ref.get(calls) const collected = (yield* Ref.get(files)) .toSorted((left, right) => left.index - right.index) .flatMap((item) => item.files) diff --git a/packages/core/test/session-runner-tool-registry.test.ts b/packages/core/test/session-runner-tool-registry.test.ts index 57e494783d91..b46f7192457d 100644 --- a/packages/core/test/session-runner-tool-registry.test.ts +++ b/packages/core/test/session-runner-tool-registry.test.ts @@ -515,7 +515,7 @@ describe("ToolRegistry", () => { }), ) - it.effect("executes codemode tools advertised in a model request", () => + it.effect("executes and reports progress for codemode tools advertised in a model request", () => Effect.gen(function* () { const service = yield* ToolRegistry.Service const executed: string[] = [] @@ -526,8 +526,11 @@ describe("ToolRegistry", () => { description: "Echo text", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), - execute: ({ text }) => - Effect.sync(() => executed.push(`old:${text}`)).pipe(Effect.as({ output: { text } })), + execute: ({ text }, context) => + Effect.sync(() => executed.push(`old:${text}`)).pipe( + Effect.andThen(context.progress({ stage: "old" })), + Effect.as({ output: { text } }), + ), }), }) .pipe(Scope.provide(scope)) @@ -546,6 +549,7 @@ describe("ToolRegistry", () => { }), }) + const progress: ToolRegistry.Progress[] = [] const execution = yield* toolSet.execute({ ...call("execute"), call: { @@ -554,10 +558,15 @@ describe("ToolRegistry", () => { name: "execute", input: { code: 'return await tools.echo({ text: "request" })' }, }, + progress: (update) => Effect.sync(() => progress.push(update)), }) expect(execution).toMatchObject({ status: "completed", content: [{ type: "text" }] }) expect(executed).toEqual(["old:request"]) + expect(progress).toEqual([ + { toolCalls: [{ tool: "echo", status: "running", input: { text: "request" } }] }, + { toolCalls: [{ tool: "echo", status: "completed", input: { text: "request" } }] }, + ]) }), ) }) diff --git a/packages/core/test/tool-execute.test.ts b/packages/core/test/tool-execute.test.ts index 84df02243ecb..21498a2fbd36 100644 --- a/packages/core/test/tool-execute.test.ts +++ b/packages/core/test/tool-execute.test.ts @@ -4,7 +4,7 @@ import { Tool } from "@opencode-ai/core/tool/tool" import { Agent } from "@opencode-ai/schema/agent" import { Session } from "@opencode-ai/schema/session" import { SessionMessage } from "@opencode-ai/schema/session-message" -import { Effect, Schema } from "effect" +import { Deferred, Effect, Fiber, Schema } from "effect" const context = { sessionID: Session.ID.make("ses_execute"), @@ -131,3 +131,46 @@ test("execute supports callable namespace tools", async () => { }) expect(result.content).toEqual([{ type: "text", text: '[\n "admin",\n "created"\n]' }]) }) + +test("execute marks every admitted child call failed when interrupted", async () => { + const child = Tool.make({ + description: "Wait forever", + input: Schema.Struct({ id: Schema.Number }), + output: Schema.String, + execute: () => Effect.never, + }) + const execute = ExecuteTool.create(new Map([["wait", { tool: child, name: "wait", permission: "wait" }]])) + const updates: Tool.Metadata[] = [] + + await Effect.runPromise( + Effect.gen(function* () { + const started = yield* Deferred.make() + const fiber = yield* Tool.execute( + execute, + { code: "return await Promise.all([tools.wait({ id: 1 }), tools.wait({ id: 2 })])" }, + { + ...context, + progress: (update) => + Effect.gen(function* () { + updates.push(update) + if (updates.length > 1) return + yield* Deferred.succeed(started, undefined) + yield* Effect.never + }), + }, + ).pipe(Effect.forkChild) + yield* Deferred.await(started) + yield* Effect.yieldNow + yield* Effect.yieldNow + yield* Fiber.interrupt(fiber) + }), + ) + + expect(updates[0]).toEqual({ toolCalls: [{ tool: "wait", status: "running", input: { id: 1 } }] }) + expect(updates.at(-1)).toEqual({ + toolCalls: [ + { tool: "wait", status: "error", input: { id: 1 } }, + { tool: "wait", status: "error", input: { id: 2 } }, + ], + }) +}) diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index a771c8c018f5..46428a1a9349 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -2872,7 +2872,7 @@ function Execute(props: ToolProps) { const isLoading = createMemo(() => props.part.state.status === "streaming" || props.part.state.status === "running") const calls = createMemo(() => executeCalls(props.metadata.toolCalls)) const output = createMemo(() => stripAnsi(props.output?.trim() ?? "")) - const hasRuntimeError = createMemo(() => props.metadata.error === true) + const hasRuntimeError = createMemo(() => props.metadata.error === true || props.part.state.status === "error") const outputPreview = createMemo(() => collapseToolOutput(output(), 4, 4 * Math.max(20, ctx.width - 6)).output) const showOutput = createMemo(() => output() && hasRuntimeError()) const content = createMemo(() => { From d1d97014b4b274293a9940d50570821a7771f14a Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:46:59 -0500 Subject: [PATCH 101/150] refactor(core): move static Code Mode guidance (#38746) --- packages/core/src/codemode/instructions.ts | 10 +++------- packages/core/src/tool/execute.ts | 9 +++++---- packages/core/test/codemode/catalog.test.ts | 18 ++---------------- packages/core/test/tool-execute.test.ts | 12 ++++++++++++ 4 files changed, 22 insertions(+), 27 deletions(-) diff --git a/packages/core/src/codemode/instructions.ts b/packages/core/src/codemode/instructions.ts index 3a546d79c2d1..da12c1c35eb2 100644 --- a/packages/core/src/codemode/instructions.ts +++ b/packages/core/src/codemode/instructions.ts @@ -6,17 +6,13 @@ import { Instructions } from "../instructions/index" import { CodeModeCatalog } from "./catalog" // prettier-ignore -const prompt = (hasMoreTools: boolean) => `Run JavaScript to orchestrate tool calls and compose their results. Imports, direct filesystem access, and timers are unavailable. Do not use \`fetch\`; all external access goes through \`tools\`. +const prompt = (hasMoreTools: boolean) => `The Code Mode tool catalog below is ${hasMoreTools ? "partial" : "complete"}. -Inside Code Mode, \`tools\` contains only the tools shown below${hasMoreTools ? " or returned by `search`" : ""}; surrounding top-level agent tools are not available and must not be called from the code. - -Prefer an explicit \`return\`; if omitted, the final top-level expression becomes the result. Await tool calls before returning; any calls still pending when execution ends are interrupted. Run independent calls concurrently with \`Promise.all\`. - -Do not infer or normalize tool names; use only the exact signatures shown below${hasMoreTools ? " or returned by `search`" : ""}, preserving bracket notation such as \`tools.["tool-name"](input)\`.${hasMoreTools ? ` +Inside Code Mode, \`tools\` contains only the tools shown below${hasMoreTools ? " or returned by `search`" : ""}; surrounding top-level agent tools are not available and must not be called from the code.${hasMoreTools ? ` ## Search -Only some tool signatures are shown. Use \`search\` to discover exact paths and signatures for additional tools: +Use \`search\` to discover exact paths and signatures for additional tools: - ${searchSignature}` : ""} diff --git a/packages/core/src/tool/execute.ts b/packages/core/src/tool/execute.ts index ddf7dbe48121..c1b297156d4f 100644 --- a/packages/core/src/tool/execute.ts +++ b/packages/core/src/tool/execute.ts @@ -34,10 +34,11 @@ type CollectedFiles = { // Invariant model-facing guidance; the changing tool catalog is delivered through Instructions. const description = [ - "Run JavaScript in a confined Code Mode runtime through { code }.", - "Call Code Mode tools through `tools` using the exact paths and signatures from the instructions.", - "Use `search({ query })` to discover exact signatures when needed.", - "Await important calls and use `Promise.all` for independent calls.", + "Run JavaScript to orchestrate tool calls and compose their results through `{ code }` in a confined Code Mode runtime.", + "Imports, direct filesystem access, and timers are unavailable. Do not use `fetch`; all external access goes through `tools`.", + 'Call Code Mode tools through `tools` using only exact paths and signatures from the current catalog or `search`. Do not infer or normalize tool names; preserve bracket notation such as `tools.["tool-name"](input)`.', + "Prefer an explicit `return`; if omitted, the final top-level expression becomes the result.", + "Await every call whose completion matters; pending calls are interrupted when execution ends. Run independent calls concurrently with `Promise.all`.", ].join("\n") export const create = (registrations: ReadonlyMap) => { diff --git a/packages/core/test/codemode/catalog.test.ts b/packages/core/test/codemode/catalog.test.ts index 6831b46ad56c..c880c5d53f21 100644 --- a/packages/core/test/codemode/catalog.test.ts +++ b/packages/core/test/codemode/catalog.test.ts @@ -66,37 +66,23 @@ describe("CodeModeInstructions.render", () => { expect(instructions).toContain("- orders (1 tool)") expect(instructions).toContain(` - ${lookup.signature} // Look up an order by ID`) expect(instructions).not.toContain("## Search") - expect(instructions).toContain("Do not infer or normalize tool names") - expect(instructions).toContain('`tools.["tool-name"](input)`') + expect(instructions).toContain("The Code Mode tool catalog below is complete.") expect(instructions).toContain( "`tools` contains only the tools shown below; surrounding top-level agent tools are not available and must not be called from the code.", ) }) - test("describes the runtime and execution lifecycle concisely", () => { - const instructions = render([lookup]) - expect(instructions).toContain("Run JavaScript to orchestrate tool calls and compose their results.") - expect(instructions).toContain("Imports, direct filesystem access, and timers are unavailable.") - expect(instructions).toContain("Do not use `fetch`; all external access goes through `tools`.") - expect(instructions).toContain( - "Prefer an explicit `return`; if omitted, the final top-level expression becomes the result.", - ) - expect(instructions).toContain("any calls still pending when execution ends are interrupted") - expect(instructions).toContain("Run independent calls concurrently with `Promise.all`.") - }) - test("adds search guidance when the catalog exceeds the budget", () => { const partial = render([lookup], 0) expect(partial).toContain("## Available tools") expect(partial).toContain("- orders (1 tool, none shown)") expect(partial).toContain("## Search") - expect(partial).toContain("Only some tool signatures are shown.") + expect(partial).toContain("The Code Mode tool catalog below is partial.") expect(partial).toContain( "`tools` contains only the tools shown below or returned by `search`; surrounding top-level agent tools are not available and must not be called from the code.", ) expect(partial).toContain("- search(input: {") expect(partial).toContain(" limit?: number,\n offset?: number,") - expect(partial).toContain("or returned by `search`") expect(partial).not.toContain("tools.orders.lookup(input:") }) diff --git a/packages/core/test/tool-execute.test.ts b/packages/core/test/tool-execute.test.ts index 21498a2fbd36..00f6bef32bee 100644 --- a/packages/core/test/tool-execute.test.ts +++ b/packages/core/test/tool-execute.test.ts @@ -14,6 +14,18 @@ const context = { progress: () => Effect.void, } +test("execute describes invariant Code Mode behavior", () => { + expect(ExecuteTool.create(new Map()).description).toBe( + [ + "Run JavaScript to orchestrate tool calls and compose their results through `{ code }` in a confined Code Mode runtime.", + "Imports, direct filesystem access, and timers are unavailable. Do not use `fetch`; all external access goes through `tools`.", + 'Call Code Mode tools through `tools` using only exact paths and signatures from the current catalog or `search`. Do not infer or normalize tool names; preserve bracket notation such as `tools.["tool-name"](input)`.', + "Prefer an explicit `return`; if omitted, the final top-level expression becomes the result.", + "Await every call whose completion matters; pending calls are interrupted when execution ends. Run independent calls concurrently with `Promise.all`.", + ].join("\n"), + ) +}) + test("canonical execution distinguishes declared, model-only, and raw schema outputs", async () => { const declared = Tool.make({ description: "Declared", From 13b6845e7e902308e9dccd2400a13340592d627a Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:02:57 -0500 Subject: [PATCH 102/150] fix(core): scope MCP execute guidance to Code Mode (#38753) --- packages/core/src/mcp/instructions.ts | 40 +++++++++------ packages/core/test/mcp-instructions.test.ts | 54 +++++++++++++++++++++ 2 files changed, 80 insertions(+), 14 deletions(-) diff --git a/packages/core/src/mcp/instructions.ts b/packages/core/src/mcp/instructions.ts index e41a108bc355..d0cf45ec6dc0 100644 --- a/packages/core/src/mcp/instructions.ts +++ b/packages/core/src/mcp/instructions.ts @@ -11,16 +11,20 @@ import { Instructions } from "../instructions/index" const Summary = Schema.Struct({ server: Schema.String, instructions: Schema.String, + codemode: Schema.optionalKey(Schema.Literal(false)), }) type Summary = typeof Summary.Type const entries = (servers: ReadonlyArray) => - servers.flatMap((server) => [ - ` `, - ` Use tools from this server through \`execute\` under \`tools[${JSON.stringify(McpTool.namespace(server.server))}]\`.`, - ...server.instructions.split("\n").map((line) => ` ${line}`), - " ", - ]) + servers.flatMap((server) => { + const result = [` `] + if (server.codemode !== false) + result.push( + ` Use tools from this server through \`execute\` under \`tools[${JSON.stringify(McpTool.namespace(server.server))}]\`.`, + ) + result.push(...server.instructions.split("\n").map((line) => ` ${line}`), " ") + return result + }) const render = (servers: ReadonlyArray) => ["", ...entries(servers), ""].join("\n") @@ -30,7 +34,7 @@ const update = (previous: ReadonlyArray, current: ReadonlyArray server.server, - (before, after) => before.instructions !== after.instructions, + (before, after) => before.instructions !== after.instructions || before.codemode !== after.codemode, ) // Additions and removals render as small deltas; anything else restates the full list. if (diff.changed.length > 0 || (diff.added.length === 0 && diff.removed.length === 0)) @@ -76,21 +80,29 @@ export const layer = Layer.effect( removed: () => "MCP server instructions are no longer available.", }, }) - if (PermissionV2.evaluate("execute", "*", agent.permissions).effect === "deny") - return source(Instructions.removed) const [instructions, tools] = yield* Effect.all([mcp.instructions(), mcp.tools()], { concurrency: "unbounded", }) + const canExecute = PermissionV2.evaluate("execute", "*", agent.permissions).effect !== "deny" // Instructions are useful only when this agent can reach at least one server tool. const visible = instructions - .filter((item) => { + .flatMap((item) => { const owned = tools.filter((tool) => tool.server === item.server) - return owned.some( - (tool) => - PermissionV2.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny", + const codemode = owned[0]?.codemode !== false + if (codemode && !canExecute) return [] + if ( + !owned.some( + (tool) => + PermissionV2.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny", + ) ) + return [] + return [ + codemode + ? { server: item.server, instructions: item.instructions } + : { server: item.server, instructions: item.instructions, codemode: false as const }, + ] }) - .map((item) => ({ server: item.server, instructions: item.instructions })) .toSorted((a, b) => a.server.localeCompare(b.server)) return source(visible.length === 0 ? Instructions.removed : visible) }), diff --git a/packages/core/test/mcp-instructions.test.ts b/packages/core/test/mcp-instructions.test.ts index 14e0e671f090..77b493635482 100644 --- a/packages/core/test/mcp-instructions.test.ts +++ b/packages/core/test/mcp-instructions.test.ts @@ -93,6 +93,60 @@ describe("McpInstructions", () => { ), ) + it.effect("keeps MCP instructions when Code Mode is disabled and execute is denied", () => + Effect.gen(function* () { + const service = yield* McpInstructions.Service + const generation = yield* service + .load(selection([{ action: "execute", resource: "*", effect: "deny" }])) + .pipe(Effect.flatMap(readInitial)) + + expect(generation.text).toBe( + [ + "", + ' ', + " Alpha instructions", + " ", + "", + ].join("\n"), + ) + }).pipe( + Effect.provide( + layer( + () => [instructions("alpha", "Alpha instructions")], + () => [new MCP.Tool({ server: MCP.ServerName.make("alpha"), name: "search", codemode: false })], + ), + ), + ), + ) + + it.effect("restates guidance when Code Mode is disabled for a server", () => { + let tools = [tool("alpha")] + return Effect.gen(function* () { + const service = yield* McpInstructions.Service + const initialized = yield* service.load(selection()).pipe(Effect.flatMap(readInitial)) + + tools = [new MCP.Tool({ server: MCP.ServerName.make("alpha"), name: "search", codemode: false })] + const changed = yield* readUpdate(yield* service.load(selection()), initialized) + expect(changed.text).toBe( + [ + "The available MCP server instructions have changed. This list supersedes the previous one.", + "", + ' ', + " Alpha instructions", + " ", + "", + ].join("\n"), + ) + }).pipe( + Effect.provide( + layer( + () => [instructions("alpha", "Alpha instructions")], + () => tools, + ), + ), + ) + }) + it.effect("renders additions, changes, and removal", () => { let catalog = [instructions("alpha", "Alpha instructions")] const tools = [tool("alpha"), tool("beta")] From c7d7f611462446b28d064e3be5a9de1808ccce4b Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:23:51 -0500 Subject: [PATCH 103/150] fix(ai): preserve Anthropic usage metadata (#38751) --- .../ai/src/protocols/anthropic-messages.ts | 42 +++++++++---- packages/ai/src/schema/events.ts | 6 +- .../test/provider/anthropic-messages.test.ts | 60 +++++++++++++++++++ 3 files changed, 92 insertions(+), 16 deletions(-) diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index 2812e0136f77..316f193d92e6 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -7,6 +7,7 @@ import { Protocol } from "../route/protocol" import { LLMError, LLMEvent, + mergeJsonRecords, Usage, type CacheHint, type FinishReasonDetails, @@ -234,12 +235,27 @@ const AnthropicBodyFields = { export const AnthropicMessagesBody = Schema.Struct(AnthropicBodyFields) export type AnthropicMessagesBody = Schema.Schema.Type -const AnthropicUsage = Schema.Struct({ - input_tokens: Schema.optional(Schema.Number), - output_tokens: Schema.optional(Schema.Number), - cache_creation_input_tokens: optionalNull(Schema.Number), - cache_read_input_tokens: optionalNull(Schema.Number), -}) +const AnthropicUsage = Schema.StructWithRest( + Schema.Struct({ + input_tokens: Schema.optional(Schema.Number), + output_tokens: Schema.optional(Schema.Number), + cache_creation_input_tokens: optionalNull(Schema.Number), + cache_read_input_tokens: optionalNull(Schema.Number), + server_tool_use: optionalNull( + Schema.StructWithRest( + Schema.Struct({ web_search_requests: Schema.optional(Schema.Number) }), + [Schema.Record(Schema.String, Schema.Unknown)], + ), + ), + output_tokens_details: optionalNull( + Schema.StructWithRest( + Schema.Struct({ thinking_tokens: Schema.optional(Schema.Number) }), + [Schema.Record(Schema.String, Schema.Unknown)], + ), + ), + }), + [Schema.Record(Schema.String, Schema.Unknown)], +) type AnthropicUsage = Schema.Schema.Type const AnthropicStreamBlock = Schema.Struct({ @@ -666,9 +682,8 @@ const mapFinishReason = (reason: string | null | undefined): FinishReason => { // `input_tokens` is the *non-cached* count per the Messages API docs, with // cache reads and writes as separate fields. We sum them to derive the // inclusive `inputTokens` the rest of the contract expects. Extended -// thinking tokens are *not* broken out by Anthropic — they're billed as -// part of `output_tokens`, so `reasoningTokens` stays `undefined` and -// `outputTokens` carries the combined total. +// thinking tokens are included in `output_tokens`; newer responses also +// expose that subset through `output_tokens_details.thinking_tokens`. const mapUsage = (usage: AnthropicUsage | undefined): Usage | undefined => { if (!usage) return undefined const nonCached = usage.input_tokens @@ -681,6 +696,7 @@ const mapUsage = (usage: AnthropicUsage | undefined): Usage | undefined => { nonCachedInputTokens: nonCached, cacheReadInputTokens: cacheRead, cacheWriteInputTokens: cacheWrite, + reasoningTokens: usage.output_tokens_details?.thinking_tokens, totalTokens: ProviderShared.totalTokens(inputTokens, usage.output_tokens, undefined), providerMetadata: { anthropic: usage }, }) @@ -699,18 +715,18 @@ const mergeUsage = (left: Usage | undefined, right: Usage | undefined) => { const cacheWriteInputTokens = right.cacheWriteInputTokens ?? left.cacheWriteInputTokens const inputTokens = ProviderShared.sumTokens(nonCachedInputTokens, cacheReadInputTokens, cacheWriteInputTokens) const outputTokens = right.outputTokens ?? left.outputTokens + const reasoningTokens = right.reasoningTokens ?? left.reasoningTokens return new Usage({ inputTokens, outputTokens, nonCachedInputTokens, cacheReadInputTokens, cacheWriteInputTokens, + reasoningTokens, totalTokens: ProviderShared.totalTokens(inputTokens, outputTokens, undefined), providerMetadata: { - anthropic: { - ...left.providerMetadata?.["anthropic"], - ...right.providerMetadata?.["anthropic"], - }, + anthropic: + mergeJsonRecords(left.providerMetadata?.["anthropic"], right.providerMetadata?.["anthropic"]) ?? {}, }, }) } diff --git a/packages/ai/src/schema/events.ts b/packages/ai/src/schema/events.ts index d2195d8c1906..654a892859fd 100644 --- a/packages/ai/src/schema/events.ts +++ b/packages/ai/src/schema/events.ts @@ -40,9 +40,9 @@ import { ProviderFailureClassification } from "./errors" * - Anthropic and Bedrock report the input breakdown natively: Anthropic's * `input_tokens` and Bedrock's `inputTokens` are non-cached only. Their * mappers sum the breakdown to derive the inclusive `inputTokens`. - * Anthropic does *not* break extended-thinking out of `output_tokens`, so - * `reasoningTokens` is `undefined` and `outputTokens` carries the - * combined total — a documented limitation of the Anthropic API. + * Anthropic's `outputTokens` includes extended thinking. Newer responses + * expose that subset as `output_tokens_details.thinking_tokens`, which maps + * to `reasoningTokens`; older responses leave it undefined. * * `providerMetadata` always carries the provider's raw usage payload — * keyed by provider name (`{ openai: ... }`, `{ anthropic: ... }`, etc.) diff --git a/packages/ai/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts index 004a378bcff2..af1b3d99c802 100644 --- a/packages/ai/test/provider/anthropic-messages.test.ts +++ b/packages/ai/test/provider/anthropic-messages.test.ts @@ -543,6 +543,66 @@ describe("Anthropic Messages route", () => { }), ) + it.effect("maps thinking tokens and preserves unknown Anthropic usage fields", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { + type: "message_start", + message: { + usage: { + input_tokens: 5, + cache_read_input_tokens: 2, + service_tier: "standard", + cache_creation: { ephemeral_5m_input_tokens: 1 }, + server_tool_use: { web_search_requests: 1, start_counter: 2 }, + output_tokens_details: { thinking_tokens: 3, start_detail: "preserved" }, + }, + }, + }, + { + type: "message_delta", + delta: { stop_reason: "end_turn" }, + usage: { + output_tokens: 8, + server_tool_use: { web_search_requests: 2, terminal_counter: 3 }, + output_tokens_details: { terminal_detail: "preserved" }, + future_terminal: { requests: 4 }, + }, + }, + { type: "message_stop" }, + ), + ), + ), + ) + + expect(response.usage).toMatchObject({ + inputTokens: 7, + outputTokens: 8, + reasoningTokens: 3, + totalTokens: 15, + providerMetadata: { + anthropic: { + input_tokens: 5, + cache_read_input_tokens: 2, + service_tier: "standard", + cache_creation: { ephemeral_5m_input_tokens: 1 }, + server_tool_use: { web_search_requests: 2, start_counter: 2, terminal_counter: 3 }, + output_tokens: 8, + output_tokens_details: { + thinking_tokens: 3, + start_detail: "preserved", + terminal_detail: "preserved", + }, + future_terminal: { requests: 4 }, + }, + }, + }) + }), + ) + it.effect("round-trips omitted thinking carried only by a signature delta", () => Effect.gen(function* () { const response = yield* LLMClient.generate(request).pipe( From 1291dc1f113c46590bb76676c57021538f9ee40a Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 24 Jul 2026 20:49:55 -0500 Subject: [PATCH 104/150] fix(core): clarify code mode tool boundary (#38785) --- packages/core/src/codemode/instructions.ts | 4 +--- packages/core/src/tool/execute.ts | 3 ++- packages/core/test/codemode/catalog.test.ts | 11 ++++------- packages/core/test/tool-execute.test.ts | 3 ++- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/core/src/codemode/instructions.ts b/packages/core/src/codemode/instructions.ts index da12c1c35eb2..389b8873825e 100644 --- a/packages/core/src/codemode/instructions.ts +++ b/packages/core/src/codemode/instructions.ts @@ -6,9 +6,7 @@ import { Instructions } from "../instructions/index" import { CodeModeCatalog } from "./catalog" // prettier-ignore -const prompt = (hasMoreTools: boolean) => `The Code Mode tool catalog below is ${hasMoreTools ? "partial" : "complete"}. - -Inside Code Mode, \`tools\` contains only the tools shown below${hasMoreTools ? " or returned by `search`" : ""}; surrounding top-level agent tools are not available and must not be called from the code.${hasMoreTools ? ` +const prompt = (hasMoreTools: boolean) => `The Code Mode tool catalog below is ${hasMoreTools ? "partial" : "complete"}.${hasMoreTools ? ` ## Search diff --git a/packages/core/src/tool/execute.ts b/packages/core/src/tool/execute.ts index c1b297156d4f..edb22fa78b71 100644 --- a/packages/core/src/tool/execute.ts +++ b/packages/core/src/tool/execute.ts @@ -36,7 +36,8 @@ type CollectedFiles = { const description = [ "Run JavaScript to orchestrate tool calls and compose their results through `{ code }` in a confined Code Mode runtime.", "Imports, direct filesystem access, and timers are unavailable. Do not use `fetch`; all external access goes through `tools`.", - 'Call Code Mode tools through `tools` using only exact paths and signatures from the current catalog or `search`. Do not infer or normalize tool names; preserve bracket notation such as `tools.["tool-name"](input)`.', + "Within `{ code }`, the only callable tools are those explicitly listed in the Code Mode catalog instructions or returned by `search`. Inside `{ code }`, ignore tools shown outside the Code Mode catalog. They are not available in the Code Mode runtime.", + 'Call tools through `tools` using only exact paths and signatures from the catalog. Do not infer or normalize tool names; preserve bracket notation such as `tools.["tool-name"](input)`.', "Prefer an explicit `return`; if omitted, the final top-level expression becomes the result.", "Await every call whose completion matters; pending calls are interrupted when execution ends. Run independent calls concurrently with `Promise.all`.", ].join("\n") diff --git a/packages/core/test/codemode/catalog.test.ts b/packages/core/test/codemode/catalog.test.ts index c880c5d53f21..e47224ed71a4 100644 --- a/packages/core/test/codemode/catalog.test.ts +++ b/packages/core/test/codemode/catalog.test.ts @@ -67,9 +67,7 @@ describe("CodeModeInstructions.render", () => { expect(instructions).toContain(` - ${lookup.signature} // Look up an order by ID`) expect(instructions).not.toContain("## Search") expect(instructions).toContain("The Code Mode tool catalog below is complete.") - expect(instructions).toContain( - "`tools` contains only the tools shown below; surrounding top-level agent tools are not available and must not be called from the code.", - ) + expect(instructions).not.toContain("surrounding top-level agent tools") }) test("adds search guidance when the catalog exceeds the budget", () => { @@ -78,9 +76,7 @@ describe("CodeModeInstructions.render", () => { expect(partial).toContain("- orders (1 tool, none shown)") expect(partial).toContain("## Search") expect(partial).toContain("The Code Mode tool catalog below is partial.") - expect(partial).toContain( - "`tools` contains only the tools shown below or returned by `search`; surrounding top-level agent tools are not available and must not be called from the code.", - ) + expect(partial).not.toContain("surrounding top-level agent tools") expect(partial).toContain("- search(input: {") expect(partial).toContain(" limit?: number,\n offset?: number,") expect(partial).not.toContain("tools.orders.lookup(input:") @@ -127,7 +123,8 @@ describe("CodeModeInstructions.update", () => { test("renders additions, changes, and removals as a compact semantic delta", () => { const changed = { ...echo, signature: "tools.notes.echo(input: {\n text: string,\n}): Promise" } const added = entry("notes.list", "List notes") - const text = update([echo, lookup], [changed, added]) + const unchanged = Array.from({ length: 5 }, (_, index) => entry(`stable.tool${index}`, `Stable ${index}`)) + const text = update([echo, lookup, ...unchanged], [changed, added, ...unchanged]) expect(text).toContain("The Code Mode tool catalog has changed.") expect(text).toContain(`New tools are available in addition to those previously listed:\n - ${added.signature}`) expect(text).toContain( diff --git a/packages/core/test/tool-execute.test.ts b/packages/core/test/tool-execute.test.ts index 00f6bef32bee..1175c2ea4305 100644 --- a/packages/core/test/tool-execute.test.ts +++ b/packages/core/test/tool-execute.test.ts @@ -19,7 +19,8 @@ test("execute describes invariant Code Mode behavior", () => { [ "Run JavaScript to orchestrate tool calls and compose their results through `{ code }` in a confined Code Mode runtime.", "Imports, direct filesystem access, and timers are unavailable. Do not use `fetch`; all external access goes through `tools`.", - 'Call Code Mode tools through `tools` using only exact paths and signatures from the current catalog or `search`. Do not infer or normalize tool names; preserve bracket notation such as `tools.["tool-name"](input)`.', + "Within `{ code }`, the only callable tools are those explicitly listed in the Code Mode catalog instructions or returned by `search`. Inside `{ code }`, ignore tools shown outside the Code Mode catalog. They are not available in the Code Mode runtime.", + 'Call tools through `tools` using only exact paths and signatures from the catalog. Do not infer or normalize tool names; preserve bracket notation such as `tools.["tool-name"](input)`.', "Prefer an explicit `return`; if omitted, the final top-level expression becomes the result.", "Await every call whose completion matters; pending calls are interrupted when execution ends. Run independent calls concurrently with `Promise.all`.", ].join("\n"), From 454145fe65e9be9bb10e6c16f45a53c36b98ac20 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 02:22:47 +0000 Subject: [PATCH 105/150] fix(tui): preserve workspace while reconnecting (#38788) --- packages/tui/src/component/reconnecting.tsx | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/tui/src/component/reconnecting.tsx b/packages/tui/src/component/reconnecting.tsx index 5c3e7f361827..927c42519c78 100644 --- a/packages/tui/src/component/reconnecting.tsx +++ b/packages/tui/src/component/reconnecting.tsx @@ -1,8 +1,9 @@ +import { RGBA } from "@opentui/core" import { useTheme } from "../context/theme" import { Spinner } from "./spinner" export function Reconnecting() { - const { themeV2 } = useTheme() + const { themeV2 } = useTheme().contextual("elevated") return ( - - Waiting for background service... + + Restarting service... + Your session will resume automatically. ) From 828148909db3e3f3d8acfb5b5d358ea6fadb91c3 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 03:48:42 +0000 Subject: [PATCH 106/150] fix(tui): preserve workspace while reconnecting (#38788) From b2afb355275b362472b187b25f32dd82494c21a9 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 25 Jul 2026 13:43:17 -0400 Subject: [PATCH 107/150] refactor(core): settle steps lock-free by joining tool fibers first (#38743) --- packages/core/src/session/runner/llm.ts | 215 +++++++++--------- .../src/session/runner/publish-llm-event.ts | 73 ++++-- .../test/session-runner-tool-events.test.ts | 4 +- 3 files changed, 162 insertions(+), 130 deletions(-) diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 9f0fce8942b3..da722b449f8f 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -1,7 +1,7 @@ export * as SessionRunnerLLM from "./llm" import { LLMClient, LLMError, LLMEvent, isContextOverflowFailure, type ProviderErrorEvent, type ToolCall } from "@opencode-ai/ai" -import { Cause, Data, Effect, Exit, Fiber, FiberSet, Layer, Option, Pull, Schedule, Semaphore, Stream } from "effect" +import { Cause, Data, Effect, Exit, Fiber, FiberSet, Layer, Option, Pull, Schedule, Stream } from "effect" import { Database } from "../../database/database" import { EventV2 } from "../../event" import { PermissionV2 } from "../../permission" @@ -18,7 +18,7 @@ import { SessionSchema } from "../schema" import { SessionStore } from "../store" import { SessionTitle } from "../title" import { Service } from "./index" -import { createLLMEventPublisher } from "./publish-llm-event" +import { createLLMEventPublisher, type StepRecord } from "./publish-llm-event" import { Snapshot } from "../../snapshot" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { llmClient } from "../../effect/app-node-platform" @@ -70,7 +70,7 @@ const classifyToolExits = ( Cause.isFailReason(reason) ? (isDecline(reason.error) ? [] : [Cause.makeFailReason(reason.error)]) : [reason], ) return reasons.length > 0 ? [Cause.fromReasons(reasons)] : [] - })[0] + }).at(0) return { interrupted: causes.some(Cause.hasInterrupts), declines, @@ -79,6 +79,10 @@ const classifyToolExits = ( } } +const TOOLS_INTERRUPTED = { type: "aborted", message: "Tool execution interrupted" } as const +const STEP_INTERRUPTED = { type: "aborted", message: "Step interrupted" } as const +const RESULT_MISSING = { type: "tool.result-missing", message: "Provider did not return a tool result" } as const + const layer = Layer.effect( Service, Effect.gen(function* () { @@ -226,7 +230,6 @@ const layer = Layer.effect( readonly fiber: Fiber.Fiber }> = [] const interruptTools = Effect.suspend(() => Fiber.interruptAll(toolRuns.map((run) => run.fiber))) - let needsContinuation = false const startSnapshot = yield* snapshots.capture() const publisher = createLLMEventPublisher(events, { sessionID: session.id, @@ -238,15 +241,9 @@ const layer = Layer.effect( snapshot: startSnapshot, assistantMessageID, }) - const publication = Semaphore.makeUnsafe(1) - // Durable publishes are serialized so tool fibers and step settlement never interleave - // mid-event. - const serialized = (effect: Effect.Effect) => publication.withPermit(effect) - const publish = (event: LLMEvent) => serialized(publisher.publish(event)) - - const stepUsage = (settlement: NonNullable>) => ({ - cost: SessionUsage.calculateCost(resolved.cost, settlement.tokens), - tokens: settlement.tokens, + const stepUsage = (finish: NonNullable) => ({ + cost: SessionUsage.calculateCost(resolved.cost, finish.tokens), + tokens: finish.tokens, }) const captureStepEnd = Effect.fnUntraced(function* () { @@ -260,20 +257,26 @@ const layer = Layer.effect( return { snapshot, files } }) - const publishStepEnd = (settlement: NonNullable>) => + const publishStepEnd = (finish: NonNullable) => Effect.gen(function* () { const end = yield* captureStepEnd() - yield* serialized( - events.publish(SessionEvent.Step.Ended, { - sessionID: session.id, - assistantMessageID: yield* publisher.startAssistant(), - finish: settlement.finish, - ...stepUsage(settlement), - ...end, - }), - ) + yield* events.publish(SessionEvent.Step.Ended, { + sessionID: session.id, + assistantMessageID: yield* publisher.startAssistant(), + finish: finish.finish, + ...stepUsage(finish), + ...end, + }) }) + // Concurrent writers, no lock: the provider loop and each tool fiber publish + // durable events unserialized. This is safe because every publisher method commits + // its state marks synchronously before its first await (see publish-llm-event.ts), + // every required event order is per-source (each source is one sequential fiber), + // and a fiber's events are causally after its own Tool.Called: the fork happens + // below that publish. Cross-source order is unconstrained; either interleaving is + // a truthful history of concurrent work. + // // The stream is defined here but runs inside the settlement mask below: publish each // event durably, fork one fiber per local tool call, and hold back a virgin // context-overflow provider error so settlement may recover it via compaction. @@ -283,20 +286,13 @@ const layer = Layer.effect( Effect.gen(function* () { if (overflowFailure || publisher.hasProviderError()) return if (LLMEvent.is.providerError(event)) { - if (isContextOverflowFailure(event) && !publisher.hasRetryEvidence()) { + if (isContextOverflowFailure(event) && !publisher.record().outputStarted) { overflowFailure = event return } } - yield* publish(event) - if (LLMEvent.is.toolInputError(event)) { - if (!prepared.stepLimitReached) needsContinuation = true - return - } + yield* publisher.publish(event) if (event.type !== "tool-call" || event.providerExecuted) return - // Unavailable calls fail individually through the same execution seam; - // continuation depends only on remaining Step allowance. - if (!prepared.stepLimitReached) needsContinuation = true const assistantMessageID = yield* publisher.assistantMessageID(event.id) toolRuns.push({ call: event, @@ -307,20 +303,23 @@ const layer = Layer.effect( agent: agent.id, messageID: assistantMessageID, call: event, - progress: (update) => serialized(publisher.progress(event.id, update)), + // Progress is ephemeral, not durable history: nothing to order. + progress: (update) => publisher.progress(event.id, update), }), ).pipe( - Effect.flatMap((execution) => serialized(publisher.toolExecution(event.id, event.name, execution))), + // The fiber owns its call: it publishes its own completion, masked so a + // finished execution always reaches its durable settlement. + Effect.flatMap((outcome) => publisher.toolExecution(event.id, event.name, outcome)), ), ).pipe(Effect.forkScoped), }) }), ), - Effect.ensuring(serialized(publisher.flush())), + Effect.ensuring(publisher.flush()), ) - // Settle: only the stream itself is interruptible (restore); every line after it is - // protected so a started call always reaches one durable outcome. + // Settle: only the stream and the fiber joins are interruptible (restore); every + // other line is protected so a started call always reaches one durable outcome. return yield* Effect.uninterruptibleMask((restore) => Effect.gen(function* () { const stream = yield* restore(providerStream).pipe(Effect.exit) @@ -329,107 +328,103 @@ const layer = Layer.effect( // away non-interrupt failures, so both interrupt checks stay Cause-based. const streamInterrupted = stream._tag === "Failure" && Cause.hasInterrupts(stream.cause) + // Join every owned tool run first: await all exits, not just the first failure. + // Afterwards no fiber is alive, settlement is the only writer, and the record + // is final. A failed join means the waiting itself was interrupted, so the runs + // we abandoned are interrupted before settlement closes them out. + if (streamInterrupted) yield* interruptTools + const joined = yield* restore( + Effect.forEach(toolRuns, (run) => Fiber.await(run.fiber), { concurrency: "unbounded" }), + ).pipe(Effect.exit) + if (joined._tag === "Failure") yield* interruptTools + const tools = classifyToolExits( + joined, + toolRuns.map((run) => run.call), + ) + // A context overflow before any assistant output is recoverable: compact and // restart the step instead of surfacing the provider error. if ( recoverOverflow && - !publisher.hasRetryEvidence() && + !publisher.record().outputStarted && isContextOverflowFailure(overflowFailure ?? streamFailure) && (yield* restore(compaction.compact(compactionInput))).status === "completed" ) return CallOutcome.Restart({ step: currentStep, recoveredOverflow: true }) - // An unrecovered held-back overflow becomes the step's durable provider error. A - // thrown LLM failure records the assistant failure unless a provider error was - // already recorded from the stream. Terminal publication waits for owned tools. - if (overflowFailure) yield* publish(overflowFailure) + // An unrecovered held-back overflow becomes the step's durable provider error. + if (overflowFailure) yield* publisher.publish(overflowFailure) + // A thrown LLM failure not already recorded as the provider error either + // escapes as a scheduled retry or fails the assistant durably. const llmFailure = streamFailure instanceof LLMError ? streamFailure : undefined - if (llmFailure && !publisher.hasProviderError()) { - const error = toSessionError(llmFailure) - if (SessionRunnerRetry.isRetryable(llmFailure) && !publisher.hasRetryEvidence()) { - // RetryScheduled and Step.Failed fold onto an existing assistant message, so - // Step.Started must be durable before the failure escapes. - yield* serialized(publisher.startAssistant()) - return yield* new SessionRunnerRetry.RetryableFailure({ - cause: llmFailure, - error, - step: currentStep, - }) - } - yield* serialized(publisher.failAssistant(error)) + const llmError = llmFailure && !publisher.record().providerFailed ? toSessionError(llmFailure) : undefined + if (llmFailure && llmError && SessionRunnerRetry.isRetryable(llmFailure) && !publisher.record().outputStarted) { + // RetryScheduled and Step.Failed fold onto an existing assistant message, so + // Step.Started must be durable before the failure escapes. + yield* publisher.startAssistant() + return yield* new SessionRunnerRetry.RetryableFailure({ + cause: llmFailure, + error: llmError, + step: currentStep, + }) } - // Provider error events only arrive from the stream, so the flag is final here. - const providerFailed = publisher.hasProviderError() - - // Settle every owned tool run: await all exits, not just the first failure, - // before publishing the terminal step event. - if (streamInterrupted) yield* interruptTools - const settled = yield* restore( - Effect.forEach(toolRuns, (run) => Fiber.await(run.fiber), { concurrency: "unbounded" }), - ).pipe(Effect.exit) - if (settled._tag === "Failure") yield* interruptTools - const tools = classifyToolExits( - settled, - toolRuns.map((run) => run.call), - ) + if (llmError) yield* publisher.failAssistant(llmError) - // A declined call settles durably with its reason before the generic sweeps. + // Close every unsettled call with the reason it could not settle truthfully, + // and fail the assistant when the step itself cannot complete. A declined call + // settles with its own reason before the generic sweeps. for (const decline of tools.declines) - yield* serialized( - publisher.failTool(decline.call.id, { - type: "aborted", - message: - decline.reason._tag === "QuestionTool.CancelledError" - ? decline.reason.message - : "The user declined this tool call", - }), - ) + yield* publisher.failTool(decline.call.id, { + type: "aborted", + message: + decline.reason._tag === "QuestionTool.CancelledError" + ? decline.reason.message + : "The user declined this tool call", + }) if (tools.declines.length > 0 || streamInterrupted || tools.interrupted) { - yield* serialized(publisher.failUnsettledTools({ type: "aborted", message: "Tool execution interrupted" })) - yield* serialized(publisher.failAssistant({ type: "aborted", message: "Step interrupted" })) + yield* publisher.failUnsettledTools(TOOLS_INTERRUPTED) + yield* publisher.failAssistant(STEP_INTERRUPTED) } if (tools.failure !== undefined) { const error = toSessionError(tools.infraError ?? Cause.squash(tools.failure)) - yield* serialized(publisher.failUnsettledTools(error)) - if (tools.infraError !== undefined) yield* serialized(publisher.failAssistant(error)) + yield* publisher.failUnsettledTools(error) + if (tools.infraError !== undefined) yield* publisher.failAssistant(error) } - - // Fail unresolved calls before the terminal step event. Local calls have joined, so - // these sweeps only close calls that could not produce a truthful settlement. - if (providerFailed) - yield* serialized(publisher.failUnsettledTools({ type: "aborted", message: "Tool execution interrupted" })) - const resultMissing = { - type: "tool.result-missing", - message: "Provider did not return a tool result", - } as const - if (llmFailure && !providerFailed) yield* serialized(publisher.failUnsettledTools(resultMissing, "hosted")) + // Local calls have joined, so the remaining sweeps only close hosted calls the + // provider promised but never resolved. + if (publisher.record().providerFailed) yield* publisher.failUnsettledTools(TOOLS_INTERRUPTED) + if (llmError) yield* publisher.failUnsettledTools(RESULT_MISSING, "hosted") // A clean stream that still left hosted calls unresolved fails the step itself. - if (stream._tag === "Success" && !providerFailed) { - const hostedResultMissing = yield* serialized(publisher.failUnsettledTools(resultMissing, "hosted")) - if (hostedResultMissing && !publisher.stepSettlement()) - yield* serialized(publisher.failAssistant(resultMissing)) + if (stream._tag === "Success" && !publisher.record().providerFailed) { + const hostedResultMissing = yield* publisher.failUnsettledTools(RESULT_MISSING, "hosted") + if (hostedResultMissing && !publisher.record().finish) yield* publisher.failAssistant(RESULT_MISSING) } - const stepFailure = publisher.stepFailure() - const stepSettlement = publisher.stepSettlement() - if (stepSettlement && !stepFailure) yield* publishStepEnd(stepSettlement) - if (stepFailure) { + // One terminal event: Step.Ended on a clean finish, Step.Failed otherwise. + const record = publisher.record() + if (record.finish && !record.failure) yield* publishStepEnd(record.finish) + if (record.failure) { const end = yield* captureStepEnd() - yield* serialized( - publisher.publishStepFailure({ - ...(stepSettlement ? stepUsage(stepSettlement) : {}), - ...end, - }), - ) + yield* publisher.publishStepFailure({ + ...(record.finish ? stepUsage(record.finish) : {}), + ...end, + }) } if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause) if (tools.declines.length > 0) return yield* Effect.interrupt if ((tools.interrupted || tools.infraError !== undefined) && tools.failure) return yield* Effect.failCause(tools.failure) - if (tools.interrupted && settled._tag === "Failure") return yield* Effect.failCause(settled.cause) - if (stepFailure) return yield* new StepFailedError({ error: stepFailure }) - return CallOutcome.Completed({ needsContinuation, step: currentStep }) + if (tools.interrupted && joined._tag === "Failure") return yield* Effect.failCause(joined.cause) + if (record.failure) return yield* new StepFailedError({ error: record.failure }) + return CallOutcome.Completed({ + // A local call or malformed tool input requires another model step, unless + // this step already exhausted the agent's allowance. + needsContinuation: + !prepared.stepLimitReached && + record.calls.some((call) => !call.providerExecuted && (call.called || call.settled)), + step: currentStep, + }) }), ) }, Effect.scoped) diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts index cee1380048dc..fcec89352cfa 100644 --- a/packages/core/src/session/runner/publish-llm-event.ts +++ b/packages/core/src/session/runner/publish-llm-event.ts @@ -23,9 +23,30 @@ type Input = { readonly assistantMessageID: SessionMessage.ID } -const record = (value: unknown): Record => +const asRecord = (value: unknown): Record => typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record) : { value } +/** Immutable fold of the durable facts a step's writer has recorded so far. */ +export interface StepRecord { + /** The model produced visible output this attempt, which bars transparent retries and overflow recovery. */ + readonly outputStarted: boolean + readonly providerFailed: boolean + /** The step's recorded assistant failure, if any. */ + readonly failure?: SessionError.Error + /** Present once the provider finished the step normally. */ + readonly finish?: { + readonly finish: Extract["reason"]["normalized"] + readonly tokens: ReturnType + } + readonly calls: ReadonlyArray<{ + readonly id: string + readonly name: string + readonly called: boolean + readonly settled: boolean + readonly providerExecuted: boolean + }> +} + /** Derives canonical model content from a provider-hosted tool result. */ const hostedContent = (result: ToolResultValue): Tool.NonEmptyContent => { if (result.type === "content") { @@ -35,7 +56,17 @@ const hostedContent = (result: ToolResultValue): Tool.NonEmptyContent => { return [{ type: "text", text: Tool.stringify(result.value) }] } -/** Persist one step without executing tools or starting a continuation step. */ +/** + * Persist one step without executing tools or starting a continuation step. + * + * Concurrency invariant: the provider loop and each owned tool fiber call these methods + * concurrently without a lock. Two rules keep that safe, and every method must preserve + * them. (1) Commit state marks synchronously before the first await: never a yield + * between a check (`tool.settled`, `stepStarted`, ...) and its mark, so check-and-mark + * stays atomic under cooperative scheduling. (2) Never require a cross-source event + * order: each publishing fiber is sequential, so per-source order holds by construction, + * and consumers fold by callID/ordinal rather than global position. + */ export const createLLMEventPublisher = (events: Pick, input: Input) => { const tools = new Map< string, @@ -54,14 +85,9 @@ export const createLLMEventPublisher = (events: Pick["reason"]["normalized"] - readonly tokens: ReturnType - } - | undefined + let stepSettlement: StepRecord["finish"] const startAssistant = Effect.fnUntraced(function* () { if (stepStarted) return assistantMessageID @@ -299,7 +325,7 @@ export const createLLMEventPublisher = (events: Pick providerFailed, - hasRetryEvidence: () => retryEvidence, - stepFailure: () => stepFailure, - stepSettlement: () => stepSettlement, + /** Immutable snapshot of everything recorded for this step so far. */ + record: (): StepRecord => ({ + outputStarted, + providerFailed, + failure: stepFailure, + finish: stepSettlement, + calls: Array.from(tools, ([id, tool]) => ({ + id, + name: tool.name, + called: tool.called, + settled: tool.settled, + providerExecuted: tool.providerExecuted, + })), + }), startAssistant, assistantMessageID: assistantMessageIDForTool, } diff --git a/packages/core/test/session-runner-tool-events.test.ts b/packages/core/test/session-runner-tool-events.test.ts index ac90c1dd4f20..23b607a70e05 100644 --- a/packages/core/test/session-runner-tool-events.test.ts +++ b/packages/core/test/session-runner-tool-events.test.ts @@ -259,7 +259,7 @@ test("step finish records settlement without publishing step ended", async () => await Effect.runPromise(publisher.publish(LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } }))) expect(published.some((event) => event.type === "step.ended.2")).toBe(false) - expect(publisher.stepSettlement()).toMatchObject({ finish: "stop" }) + expect(publisher.record().finish).toMatchObject({ finish: "stop" }) }) test("content-filter finish retains failure evidence until step closeout", async () => { @@ -280,7 +280,7 @@ test("content-filter finish retains failure evidence until step closeout", async ) expect(published.map((event) => event.type)).toEqual(["session.step.started.1"]) - const settlement = publisher.stepSettlement() + const settlement = publisher.record().finish expect(settlement).toMatchObject({ finish: "content-filter", tokens: { input: 8, output: 2, reasoning: 1 }, From 33390cc4573544d05789db1f46f7f2c0f8e676e1 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:01:05 -0500 Subject: [PATCH 108/150] fix(core): keep execute tool cache stable (#38783) --- packages/core/src/codemode.ts | 1 - packages/core/src/codemode/instructions.ts | 26 +++++------ packages/core/test/codemode/catalog.test.ts | 4 +- .../core/test/codemode/instructions.test.ts | 19 ++++++++ packages/core/test/lib/tool.ts | 16 +++++++ packages/core/test/mcp.test.ts | 24 +++++++---- .../core/test/session-runner-recorded.test.ts | 1 + .../test/session-runner-tool-registry.test.ts | 43 ++++++++++++++----- packages/core/test/session-runner.test.ts | 40 +++++++++++++++++ packages/core/test/tool-edit.test.ts | 10 +++-- packages/core/test/tool-patch.test.ts | 2 +- packages/core/test/tool-question.test.ts | 8 +++- packages/core/test/tool-read.test.ts | 8 +++- packages/core/test/tool-webfetch.test.ts | 2 +- packages/core/test/tool-websearch.test.ts | 2 +- packages/core/test/tool-write.test.ts | 2 +- 16 files changed, 163 insertions(+), 45 deletions(-) diff --git a/packages/core/src/codemode.ts b/packages/core/src/codemode.ts index 20dcea1d427d..6d858097f8bc 100644 --- a/packages/core/src/codemode.ts +++ b/packages/core/src/codemode.ts @@ -63,7 +63,6 @@ const layer = Layer.effect( if (rule?.resource === "*" && rule.effect === "deny") continue registrations.set(name, registration) } - if (registrations.size === 0) return {} const executeRule = rules.findLast((rule) => Wildcard.match("execute", rule.action)) if (executeRule?.resource === "*" && executeRule.effect === "deny") return {} return { diff --git a/packages/core/src/codemode/instructions.ts b/packages/core/src/codemode/instructions.ts index 389b8873825e..2561725fe85a 100644 --- a/packages/core/src/codemode/instructions.ts +++ b/packages/core/src/codemode/instructions.ts @@ -17,7 +17,8 @@ Use \`search\` to discover exact paths and signatures for additional tools: ## Available tools` export function render(catalog: CodeModeCatalog.Summary) { - if (catalog.total === 0) return "No tools are currently available." + if (catalog.total === 0) + return "No Code Mode tools are currently available. Do not call `execute` until a later system update announces available tools." const tools = catalog.namespaces.flatMap((namespace) => { const count = namespace.count === 1 ? "1 tool" : `${namespace.count} tools` @@ -36,12 +37,13 @@ ${tools.join("\n")}` } export function update(previous: CodeModeCatalog.Summary, current: CodeModeCatalog.Summary) { - const full = `The Code Mode tool catalog has changed. This catalog supersedes the previous Code Mode tool catalog. + const replacement = `The Code Mode tool catalog has changed. This catalog supersedes the previous Code Mode tool catalog. ${render(current)}` + if (current.total === 0) return replacement const previousComplete = previous.shown === previous.total const currentComplete = current.shown === current.total - if (previousComplete !== currentComplete) return full + if (previousComplete !== currentComplete) return replacement const diff = Instructions.diffByKey( previous.namespaces.flatMap((namespace) => namespace.entries), @@ -52,7 +54,7 @@ ${render(current)}` const entriesChanged = diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0 if (!currentComplete) { - if (entriesChanged) return full + if (entriesChanged) return replacement const namespaces = Instructions.diffByKey( previous.namespaces, current.namespaces, @@ -60,7 +62,7 @@ ${render(current)}` (before, after) => before.count !== after.count, ) const changed = namespaces.added.length > 0 || namespaces.removed.length > 0 || namespaces.changed.length > 0 - if (!changed) return full + if (!changed) return replacement const parts = ["The Code Mode tool catalog has changed."] if (namespaces.added.length > 0) { @@ -85,11 +87,11 @@ ${render(current)}` ) } const delta = parts.join("\n\n") - if (delta.length < full.length) return delta - return full + if (delta.length < replacement.length) return delta + return replacement } - if (!entriesChanged) return full + if (!entriesChanged) return replacement const parts = ["The Code Mode tool catalog has changed."] if (diff.added.length > 0) { parts.push( @@ -115,19 +117,19 @@ ${render(current)}` ) } const delta = parts.join("\n\n") - if (delta.length < full.length) return delta - return full + if (delta.length < replacement.length) return delta + return replacement } const key = Instructions.Key.make("core/codemode") const codec = Schema.toCodecJson(CodeModeCatalog.Summary) export const make = (entries?: ReadonlyArray): Instructions.Instructions => { - const catalog = CodeModeCatalog.summarize(entries ?? []) + const catalog = entries === undefined ? Instructions.removed : CodeModeCatalog.summarize(entries) return Instructions.make({ key, codec, - read: Effect.succeed(catalog.total === 0 ? Instructions.removed : catalog), + read: Effect.succeed(catalog), render: { initial: render, changed: update, diff --git a/packages/core/test/codemode/catalog.test.ts b/packages/core/test/codemode/catalog.test.ts index e47224ed71a4..486ebb36c1b0 100644 --- a/packages/core/test/codemode/catalog.test.ts +++ b/packages/core/test/codemode/catalog.test.ts @@ -113,7 +113,9 @@ describe("CodeModeInstructions.render", () => { }) test("renders only the no-tools notice for an empty catalog", () => { - expect(render([])).toBe("No tools are currently available.") + expect(render([])).toBe( + "No Code Mode tools are currently available. Do not call `execute` until a later system update announces available tools.", + ) }) }) diff --git a/packages/core/test/codemode/instructions.test.ts b/packages/core/test/codemode/instructions.test.ts index 953445508948..8f812241188e 100644 --- a/packages/core/test/codemode/instructions.test.ts +++ b/packages/core/test/codemode/instructions.test.ts @@ -21,6 +21,25 @@ const lookup: CodeModeCatalog.Entry = { } describe("CodeModeInstructions", () => { + it.effect("instructs the model not to call execute while the catalog is empty", () => + Effect.gen(function* () { + const initialized = yield* readInitial(CodeModeInstructions.make([])) + expect(initialized.text).toBe( + "No Code Mode tools are currently available. Do not call `execute` until a later system update announces available tools.", + ) + + const added = yield* readUpdate(CodeModeInstructions.make([echo]), initialized) + expect(added.text).toContain("New tools are available in addition to those previously listed:") + expect(added.text).toContain(echo.signature) + + expect(yield* readUpdate(CodeModeInstructions.make([]), { values: added.values })).toMatchObject({ + text: + "The Code Mode tool catalog has changed. This catalog supersedes the previous Code Mode tool catalog.\n\n" + + "No Code Mode tools are currently available. Do not call `execute` until a later system update announces available tools.", + }) + }), + ) + it.effect("renders the initial catalog, semantic deltas, and removal", () => Effect.gen(function* () { const initialized = yield* readInitial(CodeModeInstructions.make([echo])) diff --git a/packages/core/test/lib/tool.ts b/packages/core/test/lib/tool.ts index 2f41c3b9b47d..34314cdc5899 100644 --- a/packages/core/test/lib/tool.ts +++ b/packages/core/test/lib/tool.ts @@ -32,6 +32,22 @@ export function waitForTool( }) } +export function waitForCodeModeTool( + registry: ToolRegistry.Interface, + path: string, + remaining = 1000, +): Effect.Effect { + return Effect.gen(function* () { + const toolSet = yield* registry.snapshot() + if (toolSet.codeModeCatalog?.some((tool) => tool.path === path)) return toolSet + if (remaining === 0) { + return yield* Effect.fail(new Error(`Timed out waiting for Code Mode tool: ${path}`)) + } + yield* Effect.promise(() => Bun.sleep(1)) + return yield* waitForCodeModeTool(registry, path, remaining - 1) + }) +} + /** * Registers a core tool plugin's tools against the real registry without booting the * full plugin host. Only the tool domain is live; focused tool tests exercise diff --git a/packages/core/test/mcp.test.ts b/packages/core/test/mcp.test.ts index 55933feb1f9f..8b948c9cc9bf 100644 --- a/packages/core/test/mcp.test.ts +++ b/packages/core/test/mcp.test.ts @@ -33,7 +33,7 @@ import { Image } from "@opencode-ai/core/image" import { testEffect } from "./lib/effect" import { imagePassthrough } from "./lib/image" import { location } from "./fixture/location" -import { executeTool, toolDefinitions, toolIdentity, waitForTool } from "./lib/tool" +import { executeTool, toolDefinitions, toolIdentity, waitForCodeModeTool, waitForTool } from "./lib/tool" let assertion: Deferred.Deferred | undefined let decision: Effect.Effect = Effect.void @@ -802,10 +802,16 @@ test("serializes concurrent MCP lifecycle operations", async () => { it.effect("advertises MCP output schemas to Code Mode", () => Effect.gen(function* () { const registry = yield* ToolRegistry.Service - yield* waitForTool(registry, "execute") - const definitions = yield* toolDefinitions(registry) - const execute = definitions.find((tool) => tool.name === "execute") - + const toolSet = yield* waitForCodeModeTool(registry, "demo.search") + const execute = toolSet.definitions.find((tool) => tool.name === "execute") + + expect(toolSet.definitions.map((tool) => tool.name)).toEqual([ + "direct_fail", + "direct_lookup", + "direct_media", + "execute", + ]) + expect(toolSet.codeModeCatalog?.find((tool) => tool.path === "demo.search")?.signature).toContain("ok: boolean") expect(execute?.description).not.toContain("tools.demo.search") }), ) @@ -873,9 +879,9 @@ it.effect("waits for permission before calling an MCP tool", () => const permission = yield* Deferred.make() decision = Deferred.await(permission) const registry = yield* ToolRegistry.Service - yield* waitForTool(registry, "execute") + const toolSet = yield* waitForCodeModeTool(registry, "demo.search") - const fiber = yield* executeTool(registry, { + const fiber = yield* toolSet.execute({ sessionID: SessionV2.ID.make("ses_mcp_permission"), ...toolIdentity, call: { @@ -912,9 +918,9 @@ it.effect("does not call MCP when permission is blocked", () => assertion = yield* Deferred.make() decision = Effect.fail(new PermissionV2.BlockedError({ rules: [], permission: "demo_search", resources: ["*"] })) const registry = yield* ToolRegistry.Service - yield* waitForTool(registry, "execute") + const toolSet = yield* waitForCodeModeTool(registry, "demo.search") - const execution = yield* executeTool(registry, { + const execution = yield* toolSet.execute({ sessionID: SessionV2.ID.make("ses_mcp_blocked"), ...toolIdentity, call: { diff --git a/packages/core/test/session-runner-recorded.test.ts b/packages/core/test/session-runner-recorded.test.ts index fb3619e4c697..70bc15328213 100644 --- a/packages/core/test/session-runner-recorded.test.ts +++ b/packages/core/test/session-runner-recorded.test.ts @@ -186,6 +186,7 @@ describe("SessionRunnerLLM recorded", () => { yield* agents.transform((draft) => draft.update(AgentV2.ID.make("build"), (agent) => { agent.mode = "primary" + agent.permissions.push({ action: "execute", resource: "*", effect: "deny" }) }), ) const pluginHost = host({ diff --git a/packages/core/test/session-runner-tool-registry.test.ts b/packages/core/test/session-runner-tool-registry.test.ts index b46f7192457d..b924f15fb7bd 100644 --- a/packages/core/test/session-runner-tool-registry.test.ts +++ b/packages/core/test/session-runner-tool-registry.test.ts @@ -88,7 +88,7 @@ describe("ToolRegistry", () => { expect(error).toBeInstanceOf(Tool.RegistrationError) expect(error.message).toBe('Invalid tool namespace: "slack..admin"') - expect((yield* service.snapshot()).definitions).toEqual([]) + expect((yield* service.snapshot()).definitions.map((tool) => tool.name)).toEqual(["execute"]) }), ) @@ -102,7 +102,7 @@ describe("ToolRegistry", () => { .register({ "echo.tool": make(), echo_tool: make() }, { codemode: false }) .pipe(Effect.flip) expect(collision.message).toBe("Duplicate normalized tool name: echo_tool") - expect((yield* service.snapshot()).definitions).toEqual([]) + expect((yield* service.snapshot()).definitions.map((tool) => tool.name)).toEqual(["execute"]) }), ) @@ -117,7 +117,7 @@ describe("ToolRegistry", () => { .pipe(Effect.flip) expect(error).toBeInstanceOf(Tool.RegistrationError) - expect((yield* service.snapshot()).definitions).toEqual([]) + expect((yield* service.snapshot()).definitions.map((tool) => tool.name)).toEqual(["execute"]) }), ) @@ -148,6 +148,20 @@ describe("ToolRegistry", () => { }), ) + it.effect("keeps execute available without Code Mode tools unless explicitly denied", () => + Effect.gen(function* () { + const service = yield* ToolRegistry.Service + + const available = yield* service.snapshot() + expect(available.definitions.map((tool) => tool.name)).toEqual(["execute"]) + expect(available.codeModeCatalog).toEqual([]) + + const denied = yield* service.snapshot([{ action: "execute", resource: "*", effect: "deny" }]) + expect(denied.definitions).toEqual([]) + expect(denied.codeModeCatalog).toBeUndefined() + }), + ) + it.effect("filters disabled tools with edit aliases and ordered wildcard precedence", () => Effect.gen(function* () { const service = yield* ToolRegistry.Service @@ -156,7 +170,12 @@ describe("ToolRegistry", () => { const names = (permissions: PermissionV2.Ruleset) => toolDefinitions(service, permissions).pipe(Effect.map((definitions) => definitions.map((tool) => tool.name))) - expect(yield* names([{ action: "question", resource: "*", effect: "deny" }])).toEqual(["bash", "edit", "write"]) + expect(yield* names([{ action: "question", resource: "*", effect: "deny" }])).toEqual([ + "bash", + "edit", + "write", + "execute", + ]) expect( yield* names([ { action: "*", resource: "*", effect: "deny" }, @@ -169,7 +188,11 @@ describe("ToolRegistry", () => { { action: "*", resource: "*", effect: "deny" }, ]), ).toEqual([]) - expect(yield* names([{ action: "edit", resource: "*", effect: "deny" }])).toEqual(["bash", "question"]) + expect(yield* names([{ action: "edit", resource: "*", effect: "deny" }])).toEqual([ + "bash", + "question", + "execute", + ]) }), ) @@ -182,7 +205,7 @@ describe("ToolRegistry", () => { expect( (yield* toolDefinitions(service, [{ action: "edit", resource: "*", effect: "deny" }])).map((tool) => tool.name), - ).toEqual(["first"]) + ).toEqual(["first", "execute"]) }), ) @@ -191,9 +214,9 @@ describe("ToolRegistry", () => { const service = yield* ToolRegistry.Service const scope = yield* Scope.make() yield* service.register({ echo: make() }, { codemode: false }).pipe(Scope.provide(scope)) - expect((yield* toolDefinitions(service)).map((tool) => tool.name)).toEqual(["echo"]) + expect((yield* toolDefinitions(service)).map((tool) => tool.name)).toEqual(["echo", "execute"]) yield* Scope.close(scope, Exit.void) - expect(yield* toolDefinitions(service)).toEqual([]) + expect((yield* toolDefinitions(service)).map((tool) => tool.name)).toEqual(["execute"]) }), ) @@ -213,9 +236,9 @@ describe("ToolRegistry", () => { yield* Deferred.await(registered) yield* Fiber.interrupt(fiber) - expect((yield* toolDefinitions(service)).map((tool) => tool.name)).toEqual(["echo"]) + expect((yield* toolDefinitions(service)).map((tool) => tool.name)).toEqual(["echo", "execute"]) yield* Scope.close(scope, Exit.void) - expect(yield* toolDefinitions(service)).toEqual([]) + expect((yield* toolDefinitions(service)).map((tool) => tool.name)).toEqual(["execute"]) }), ) diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 882308604605..5e07b3551af6 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -880,6 +880,46 @@ describe("SessionRunnerLLM", () => { }), ) + it.effect("advertises execute and durable guidance for an empty Code Mode catalog", () => + Effect.gen(function* () { + const session = yield* setup + const empty = { + catalog: [], + tool: Tool.make({ + description: "Execute Code Mode", + input: Schema.Struct({ code: Schema.String }), + output: Schema.String, + execute: () => Effect.succeed({ output: "unused" }), + }), + } + codeModeMaterializations = [empty, empty, {}] + yield* admit(session, "Continue without Code Mode tools") + response = reply.stop() + + yield* session.resume(sessionID) + yield* admit(session, "Still no Code Mode tools") + yield* session.resume(sessionID) + yield* admit(session, "Code Mode denied") + yield* session.resume(sessionID) + + expect(requests).toHaveLength(3) + expect(requests[0]?.tools.map((tool) => tool.name)).toEqual(["defect", "echo", "storefail", "execute"]) + expect(requests[0]?.system.some((part) => part.text.includes("Do not call `execute`"))).toBe(true) + expect(requests[1]?.tools.map((tool) => tool.name)).toEqual(["defect", "echo", "storefail", "execute"]) + expect(requests[1]?.messages.filter((message) => message.role === "system")).toEqual([]) + expect(requests[2]?.tools.map((tool) => tool.name)).toEqual(["defect", "echo", "storefail"]) + expect( + requests[2]?.messages.some( + (message) => + message.role === "system" && + message.content.some( + (part) => part.type === "text" && part.text.includes("Code Mode tools are no longer available"), + ), + ), + ).toBe(true) + }), + ) + it.effect("applies session context hooks without exposing unavailable tools", () => Effect.gen(function* () { const session = yield* setup diff --git a/packages/core/test/tool-edit.test.ts b/packages/core/test/tool-edit.test.ts index 446e6aecf55d..8eaa9c3fbb84 100644 --- a/packages/core/test/tool-edit.test.ts +++ b/packages/core/test/tool-edit.test.ts @@ -137,10 +137,12 @@ describe("EditTool", () => { Effect.andThen( withTool(tmp.path, (registry) => Effect.gen(function* () { - expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["edit"]) - expect(yield* toolDefinitions(registry, [{ action: "edit", resource: "*", effect: "deny" }])).toEqual( - [], - ) + expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["edit", "execute"]) + expect( + (yield* toolDefinitions(registry, [{ action: "edit", resource: "*", effect: "deny" }])).map( + (tool) => tool.name, + ), + ).toEqual(["execute"]) const settled = yield* executeTool( registry, call({ path: "hello.txt", oldString: "before", newString: "after" }), diff --git a/packages/core/test/tool-patch.test.ts b/packages/core/test/tool-patch.test.ts index 64616e7d2ab8..e573edb19b46 100644 --- a/packages/core/test/tool-patch.test.ts +++ b/packages/core/test/tool-patch.test.ts @@ -181,7 +181,7 @@ describe("PatchTool", () => { Effect.andThen( withTool(tmp.path, (registry) => Effect.gen(function* () { - expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["patch"]) + expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["patch", "execute"]) const settled = yield* executeTool( registry, call( diff --git a/packages/core/test/tool-question.test.ts b/packages/core/test/tool-question.test.ts index 9a72bca1b5e4..f63e7f6d2d72 100644 --- a/packages/core/test/tool-question.test.ts +++ b/packages/core/test/tool-question.test.ts @@ -97,7 +97,11 @@ describe("QuestionTool", () => { deny = true const registry = yield* ToolRegistry.Service - expect(yield* toolDefinitions(registry, [{ action: "question", resource: "*", effect: "deny" }])).toEqual([]) + expect( + (yield* toolDefinitions(registry, [{ action: "question", resource: "*", effect: "deny" }])).map( + (tool) => tool.name, + ), + ).toEqual(["execute"]) expect( yield* executeTool(registry, { sessionID, @@ -142,7 +146,7 @@ describe("QuestionTool", () => { }, ] - expect((yield* toolDefinitions(registry)).map((definition) => definition.name)).toEqual(["question"]) + expect((yield* toolDefinitions(registry)).map((definition) => definition.name)).toEqual(["question", "execute"]) expect( yield* executeTool(registry, { sessionID, diff --git a/packages/core/test/tool-read.test.ts b/packages/core/test/tool-read.test.ts index 6b3d084a10a2..a176b3b0c87c 100644 --- a/packages/core/test/tool-read.test.ts +++ b/packages/core/test/tool-read.test.ts @@ -197,8 +197,12 @@ describe("ReadTool", () => { Effect.gen(function* () { const registry = yield* ToolRegistry.Service - expect(yield* toolDefinitions(registry)).toMatchObject([{ name: "read" }]) - expect(yield* toolDefinitions(registry, [{ action: "read", resource: "*", effect: "deny" }])).toEqual([]) + expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["read", "execute"]) + expect( + (yield* toolDefinitions(registry, [{ action: "read", resource: "*", effect: "deny" }])).map( + (tool) => tool.name, + ), + ).toEqual(["execute"]) const execution = yield* executeTool(registry, { sessionID, ...toolIdentity, diff --git a/packages/core/test/tool-webfetch.test.ts b/packages/core/test/tool-webfetch.test.ts index feedafc1a2da..cd46a83c5f13 100644 --- a/packages/core/test/tool-webfetch.test.ts +++ b/packages/core/test/tool-webfetch.test.ts @@ -92,7 +92,7 @@ describe("WebFetchTool registration", () => { const registry = yield* ToolRegistry.Service const url = "http://example.com/public" - expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["webfetch"]) + expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["webfetch", "execute"]) expect(yield* executeTool(registry, call({ url, format: "text", timeout: 4 }))).toEqual({ status: "completed", output: { url, contentType: "text/plain", format: "text", output: "hello" }, diff --git a/packages/core/test/tool-websearch.test.ts b/packages/core/test/tool-websearch.test.ts index 7a8c2bb09c12..254dc4221d1b 100644 --- a/packages/core/test/tool-websearch.test.ts +++ b/packages/core/test/tool-websearch.test.ts @@ -154,7 +154,7 @@ describe("WebSearchTool registration", () => { config = { provider: "exa", enableExa: false, enableParallel: false } const registry = yield* ToolRegistry.Service - expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["websearch"]) + expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["websearch", "execute"]) expect( yield* executeTool(registry, { sessionID, diff --git a/packages/core/test/tool-write.test.ts b/packages/core/test/tool-write.test.ts index 3cd3dcb4b617..f786fc934800 100644 --- a/packages/core/test/tool-write.test.ts +++ b/packages/core/test/tool-write.test.ts @@ -118,7 +118,7 @@ describe("WriteTool", () => { reset() return withTool(tmp.path, (registry) => Effect.gen(function* () { - expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["write"]) + expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["write", "execute"]) const settled = yield* executeTool(registry, call({ path: "src/new.txt", content: "created" })) expect(settled).toEqual({ status: "completed", From 02c66c5fc16dbf8c74c2f2367c48c738cc698b59 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 25 Jul 2026 14:08:00 -0400 Subject: [PATCH 109/150] docs(core): fix OpenCode skill links --- packages/core/src/plugin/skill/opencode.md | 32 +++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/core/src/plugin/skill/opencode.md b/packages/core/src/plugin/skill/opencode.md index 1d41476ca045..669f162d7baa 100644 --- a/packages/core/src/plugin/skill/opencode.md +++ b/packages/core/src/plugin/skill/opencode.md @@ -4,7 +4,7 @@ Use this guide as the starting point for work involving OpenCode itself. It covers the core concepts needed to configure and customize OpenCode, extend it with plugins, and build integrations with the OpenCode SDK, clients, and API. -Full documentation is available at . This overview is +Full documentation is available at . This overview is only an index of core concepts. Before answering a question about a topic below, fetch the URL named in that section and use the full page as the source of truth. Follow links from that page when the question needs more detail. Fetch @@ -16,7 +16,7 @@ documentation page. Always answer for OpenCode V2 unless the user explicitly asks about V1, legacy OpenCode, or migrating from V1. -Use only documentation as the source of truth for V2. +Use only documentation as the source of truth for V2. Do not use , which documents V1, and do not use general web search to resolve a V2 documentation question when the V2 docs or their `llms.txt` index cover it. The schema served from @@ -29,7 +29,7 @@ V1 documentation and syntax may be consulted only when the user explicitly asks about V1 or when needed as migration input. Outputs and recommendations must still use V2 unless the user specifically requests a V1 result. -## [Configuration](https://v2.opencode.ai/config) +## [Configuration](https://v2.opencode.ai/docs/config) OpenCode configuration uses JSON or JSONC. Include the published schema so the user's editor can validate fields and provide autocomplete: @@ -60,14 +60,14 @@ linked topic guide as the source of truth, and preserve unrelated settings when editing an existing file. Keep the published `$schema` URL in configuration examples, but do not fetch it to determine the V2 configuration shape. -See the [full configuration guide](https://v2.opencode.ai/config) for +See the [full configuration guide](https://v2.opencode.ai/docs/config) for every field, examples, config locations, and links to dedicated feature guides. -## [V1 to V2 migration](https://v2.opencode.ai/migrate-v1) +## [V1 to V2 migration](https://v2.opencode.ai/docs/migrate-v1) For any request to migrate OpenCode configuration, agents, commands, skills, plugins, integrations, or other behavior from V1 to V2, read the full -[migration guide](https://v2.opencode.ai/migrate-v1) before acting. In +[migration guide](https://v2.opencode.ai/docs/migrate-v1) before acting. In the repository, its source is `packages/docs/migrate-v1.mdx`. V1 config files and `.opencode/` definitions are intended to remain compatible. @@ -76,18 +76,18 @@ V2 config uses more ergonomic shapes, but conversion is optional. When the user requests conversion, inspect the complete configuration, preserve behavior and unrelated settings, and apply only the relevant migrations from the guide. For plugin migrations, fetch and follow both the migration guide and the full -[plugins guide](https://v2.opencode.ai/build/plugins). If non-API V1 +[plugins guide](https://v2.opencode.ai/docs/build/plugins). If non-API V1 functionality fails in V2, use the `report` skill to file it as a compatibility bug. -## [Plugins](https://v2.opencode.ai/build/plugins) +## [Plugins](https://v2.opencode.ai/docs/build/plugins) For questions about creating, configuring, loading, publishing, or migrating -plugins, fetch the full [plugins guide](https://v2.opencode.ai/build/plugins) +plugins, fetch the full [plugins guide](https://v2.opencode.ai/docs/build/plugins) before answering. This includes questions about the Effect plugin API, hooks, transforms, tools, plugin context capabilities, and package entrypoints. -## [Service](https://v2.opencode.ai/troubleshooting#check-the-background-service) +## [Service](https://v2.opencode.ai/docs/troubleshooting#check-the-background-service) OpenCode uses a client-server architecture. Interfaces such as the TUI connect to a background OpenCode service, which owns sessions, configuration, plugins, @@ -106,7 +106,7 @@ Check its status after restarting: opencode2 service status ``` -## [API](https://v2.opencode.ai/api) +## [API](https://v2.opencode.ai/docs/api) OpenCode exposes an HTTP API from its server. The API is described by an OpenAPI document available from the running server at `/openapi.json`. @@ -135,15 +135,15 @@ connected to an explicit server instead of its managed background service, use the same configured server and authentication context rather than constructing an unauthenticated request separately. -See the [full API reference](https://v2.opencode.ai/api) for available +See the [full API reference](https://v2.opencode.ai/docs/api) for available endpoints, parameters, request bodies, and response schemas. The raw [OpenAPI specification](https://v2.opencode.ai/openapi.json) is also available for code generation and other tooling. -## [Client](https://v2.opencode.ai/build/client) +## [Client](https://v2.opencode.ai/docs/build/client) For questions about connecting an application to OpenCode over the network, -fetch the full [client guide](https://v2.opencode.ai/build/client) before +fetch the full [client guide](https://v2.opencode.ai/docs/build/client) before answering. `@opencode-ai/client` is the generated TypeScript client for the OpenCode HTTP @@ -154,7 +154,7 @@ exposes typed Effects, Streams, and decoded OpenCode schema values. Its `Service` API can discover, start, stop, and authenticate with the local background service from a Node application. -## [Troubleshooting](https://v2.opencode.ai/troubleshooting) +## [Troubleshooting](https://v2.opencode.ai/docs/troubleshooting) OpenCode runs a client and a background server. Start by determining whether a problem belongs to the client, the shared server, or one project. @@ -174,6 +174,6 @@ problem belongs to the client, the shared server, or one project. - Redact API keys, authorization headers, prompts, file contents, and other sensitive data before sharing diagnostics. -See the [full troubleshooting guide](https://v2.opencode.ai/troubleshooting) +See the [full troubleshooting guide](https://v2.opencode.ai/docs/troubleshooting) for service lifecycle commands, API inspection, log locations, explicit server connections, issue-reporting details, and local development paths. From cce8bb0e1c8c15b4bff9ed786ec840c4d204b587 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:20:16 -0500 Subject: [PATCH 110/150] fix(core): clarify empty Code Mode guidance (#38883) --- packages/core/src/codemode/instructions.ts | 2 +- packages/core/test/codemode/catalog.test.ts | 2 +- packages/core/test/codemode/instructions.test.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/codemode/instructions.ts b/packages/core/src/codemode/instructions.ts index 2561725fe85a..7e5d135edb1c 100644 --- a/packages/core/src/codemode/instructions.ts +++ b/packages/core/src/codemode/instructions.ts @@ -18,7 +18,7 @@ Use \`search\` to discover exact paths and signatures for additional tools: export function render(catalog: CodeModeCatalog.Summary) { if (catalog.total === 0) - return "No Code Mode tools are currently available. Do not call `execute` until a later system update announces available tools." + return "No Code Mode tools are currently available. Later Code Mode catalog updates may add or remove tools. Do not call `execute` unless there is at least one available Code Mode tool." const tools = catalog.namespaces.flatMap((namespace) => { const count = namespace.count === 1 ? "1 tool" : `${namespace.count} tools` diff --git a/packages/core/test/codemode/catalog.test.ts b/packages/core/test/codemode/catalog.test.ts index 486ebb36c1b0..e12df20085fe 100644 --- a/packages/core/test/codemode/catalog.test.ts +++ b/packages/core/test/codemode/catalog.test.ts @@ -114,7 +114,7 @@ describe("CodeModeInstructions.render", () => { test("renders only the no-tools notice for an empty catalog", () => { expect(render([])).toBe( - "No Code Mode tools are currently available. Do not call `execute` until a later system update announces available tools.", + "No Code Mode tools are currently available. Later Code Mode catalog updates may add or remove tools. Do not call `execute` unless there is at least one available Code Mode tool.", ) }) }) diff --git a/packages/core/test/codemode/instructions.test.ts b/packages/core/test/codemode/instructions.test.ts index 8f812241188e..44f46a9837af 100644 --- a/packages/core/test/codemode/instructions.test.ts +++ b/packages/core/test/codemode/instructions.test.ts @@ -25,7 +25,7 @@ describe("CodeModeInstructions", () => { Effect.gen(function* () { const initialized = yield* readInitial(CodeModeInstructions.make([])) expect(initialized.text).toBe( - "No Code Mode tools are currently available. Do not call `execute` until a later system update announces available tools.", + "No Code Mode tools are currently available. Later Code Mode catalog updates may add or remove tools. Do not call `execute` unless there is at least one available Code Mode tool.", ) const added = yield* readUpdate(CodeModeInstructions.make([echo]), initialized) @@ -35,7 +35,7 @@ describe("CodeModeInstructions", () => { expect(yield* readUpdate(CodeModeInstructions.make([]), { values: added.values })).toMatchObject({ text: "The Code Mode tool catalog has changed. This catalog supersedes the previous Code Mode tool catalog.\n\n" + - "No Code Mode tools are currently available. Do not call `execute` until a later system update announces available tools.", + "No Code Mode tools are currently available. Later Code Mode catalog updates may add or remove tools. Do not call `execute` unless there is at least one available Code Mode tool.", }) }), ) From c5bf4edb105d62e1cf24eda2d01d8732c6b03980 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 25 Jul 2026 14:06:00 -0500 Subject: [PATCH 111/150] fix(ai): preserve response message phases (#38777) --- packages/ai/src/protocols/open-responses.ts | 164 +++++++++++++++--- packages/ai/src/protocols/openai-responses.ts | 21 ++- .../openai-compatible-responses.test.ts | 27 ++- .../ai/test/provider/openai-responses.test.ts | 115 ++++++++++++ .../client/src/promise/generated/types.ts | 46 +++-- packages/core/src/session/message-updater.ts | 5 +- .../src/session/runner/publish-llm-event.ts | 9 +- .../core/src/session/runner/to-llm-message.ts | 9 +- .../core/test/session-runner-message.test.ts | 31 ++++ packages/core/test/session-runner.test.ts | 41 +++++ packages/schema/src/session-event.ts | 1 + packages/schema/src/session-message.ts | 1 + 12 files changed, 416 insertions(+), 54 deletions(-) diff --git a/packages/ai/src/protocols/open-responses.ts b/packages/ai/src/protocols/open-responses.ts index 249f6a93b43d..3372145087c0 100644 --- a/packages/ai/src/protocols/open-responses.ts +++ b/packages/ai/src/protocols/open-responses.ts @@ -55,6 +55,9 @@ const OpenResponsesOutputText = Schema.Struct({ text: Schema.String, }) +export const MessagePhase = Schema.Literals(["commentary", "final_answer"]) +type MessagePhase = Schema.Schema.Type + const OpenResponsesReasoningSummaryText = Schema.Struct({ type: Schema.tag("summary_text"), text: Schema.String, @@ -86,10 +89,14 @@ const OpenResponsesFunctionCallOutput = Schema.Union([ Schema.Array(OpenResponsesFunctionCallOutputContent), ]) -const OpenResponsesInputItem = Schema.Union([ +export const InputItem = Schema.Union([ Schema.Struct({ role: Schema.tag("system"), content: Schema.String }), Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenResponsesInputContent) }), - Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenResponsesOutputText) }), + Schema.Struct({ + role: Schema.tag("assistant"), + content: Schema.Array(OpenResponsesOutputText), + phase: Schema.optionalKey(MessagePhase), + }), OpenResponsesReasoningItem, OpenResponsesItemReference, Schema.Struct({ @@ -104,7 +111,14 @@ const OpenResponsesInputItem = Schema.Union([ output: OpenResponsesFunctionCallOutput, }), ]) -type OpenResponsesInputItem = Schema.Schema.Type +type OpenResponsesInputItem = Schema.Schema.Type +type LoweredInputItem = + | OpenResponsesInputItem + | { + readonly role: "assistant" + readonly content: ReadonlyArray<{ readonly type: "output_text"; readonly text: string }> + readonly phase?: MessagePhase | null + } // Mutable counterpart of the schema reasoning item so `lowerMessages` can fold // multiple streamed summary parts into the same item before flushing. @@ -135,7 +149,7 @@ export const ToolChoice = Schema.Union([ // transports in sync without a destructure-and-strip dance. export const coreFields = { model: Schema.String, - input: Schema.Array(OpenResponsesInputItem), + input: Schema.Array(InputItem), instructions: Schema.optional(Schema.String), tools: optionalArray(Tool), tool_choice: Schema.optional(ToolChoice), @@ -206,6 +220,7 @@ export const Event = Schema.StructWithRest( Schema.Struct({ type: Schema.String, delta: Schema.optional(Schema.String), + text: Schema.optional(Schema.String), item_id: Schema.optional(Schema.String), summary_index: Schema.optional(Schema.Number), item: Schema.optional(StreamItem), @@ -238,6 +253,7 @@ export interface Extension { readonly media: ProviderShared.ValidatedMedia readonly request: LLMRequest }) => MediaInput | undefined + readonly messagePhase?: (value: unknown) => MessagePhase | null | undefined } const BASE: Extension = { id: ADAPTER, name: NAME } @@ -249,6 +265,9 @@ export interface ParserState { readonly tools: ToolStream.State readonly hasFunctionCall: boolean readonly lifecycle: Lifecycle.State + readonly messageItems: ReadonlySet + readonly messagePhase: (value: unknown) => MessagePhase | null | undefined + readonly messagePhases: Readonly> readonly reasoningItems: Readonly> readonly store: boolean | undefined } @@ -378,9 +397,9 @@ const lowerToolResultOutput = Effect.fn("OpenResponses.lowerToolResultOutput")(f }) const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (request: LLMRequest, extension: Extension) { - const system: OpenResponsesInputItem[] = + const system: LoweredInputItem[] = request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }] - const input: OpenResponsesInputItem[] = [...system] + const input: LoweredInputItem[] = [...system] const store = OpenResponsesOptions.resolve(request).store const providerMetadataKey = request.model.route.providerMetadataKey ?? "openresponses" @@ -412,7 +431,27 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques const hostedToolReferences = new Set() const flushText = () => { if (content.length === 0) return - input.push({ role: "assistant", content: content.map((part) => ({ type: "output_text", text: part.text })) }) + const groups = content.reduce>( + (groups, part) => { + const metadata = part.providerMetadata?.[providerMetadataKey] + const phase = + ProviderShared.isRecord(metadata) + ? messagePhase(metadata.phase, extension) + : undefined + const group = groups.at(-1) + if (group && group.phase === phase) group.parts.push(part) + else groups.push({ phase, parts: [part] }) + return groups + }, + [], + ) + input.push( + ...groups.map((group) => ({ + role: "assistant" as const, + content: group.parts.map((part) => ({ type: "output_text" as const, text: part.text })), + ...(group.phase === undefined ? {} : { phase: group.phase }), + })), + ) content.splice(0, content.length) } for (const part of message.content) { @@ -513,9 +552,9 @@ const lowerOptions = (request: LLMRequest) => { } } -export const fromRequest = Effect.fn("OpenResponses.fromRequest")(function* ( +export const fromRequestWithExtension = Effect.fn("OpenResponses.fromRequestWithExtension")(function* ( request: LLMRequest, - extension: Extension = BASE, + extension: Extension, ) { const generation = request.generation const toolSchemaCompatibility = request.model.compatibility?.toolSchema @@ -541,6 +580,12 @@ export const fromRequest = Effect.fn("OpenResponses.fromRequest")(function* ( } }) +const decodeBody = ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenResponsesBody)) + +export const fromRequest = Effect.fn("OpenResponses.fromRequest")(function* (request: LLMRequest) { + return yield* decodeBody(yield* fromRequestWithExtension(request, BASE)) +}) + // ============================================================================= // Stream Parsing // ============================================================================= @@ -595,24 +640,30 @@ const NO_EVENTS: StepResult["1"] = [] const TERMINAL_TYPES = new Set(["response.completed", "response.incomplete", "response.failed"]) export const terminal = (event: Event) => TERMINAL_TYPES.has(event.type) -const onOutputTextDelta = (state: ParserState, event: Event): StepResult => { +const onOutputTextDelta = (state: ParserState, event: Event, id: string): StepResult => { if (!event.delta) return [state, NO_EVENTS] const events: LLMEvent[] = [] + const phase = state.messagePhases[id] + const metadata = phase === undefined ? undefined : providerMetadata(state, { phase }) + const lifecycle = Lifecycle.textStart(state.lifecycle, events, id, metadata) return [ - { ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, event.item_id ?? "text-0", event.delta) }, + { ...state, lifecycle: Lifecycle.textDelta(lifecycle, events, id, event.delta) }, events, ] } -const onOutputTextDone = (state: ParserState, event: Event): StepResult => { +const onOutputTextDone = (state: ParserState, event: Event, id: string): StepResult => { + if (state.messageItems.has(id)) { + if (state.lifecycle.text.has(id) || event.text === undefined) return [state, NO_EVENTS] + return onOutputTextDelta(state, { ...event, delta: event.text }, id) + } const events: LLMEvent[] = [] - return [{ ...state, lifecycle: Lifecycle.textEnd(state.lifecycle, events, event.item_id ?? "text-0") }, events] + return [{ ...state, lifecycle: Lifecycle.textEnd(state.lifecycle, events, id) }, events] } -export const onReasoningDelta = (state: ParserState, event: Event): StepResult => { +export const onReasoningDelta = (state: ParserState, event: Event, itemID: string): StepResult => { if (!event.delta) return [state, NO_EVENTS] const events: LLMEvent[] = [] - const itemID = event.item_id ?? "reasoning-0" const id = event.summary_index !== undefined || state.reasoningItems[itemID] ? `${itemID}:${event.summary_index ?? 0}` : itemID return [ @@ -643,6 +694,18 @@ const reasoningMetadata = (state: ParserState, item: StreamItem & { id: string } // best-effort, not guaranteed. const onOutputItemAdded = (state: ParserState, event: Event): StepResult => { const item = event.item + if (item?.type === "message" && item.id) + return [ + { + ...state, + messageItems: new Set([...state.messageItems, item.id]), + messagePhases: (() => { + const phase = state.messagePhase(item.phase) + return phase === undefined ? state.messagePhases : { ...state.messagePhases, [item.id]: phase } + })(), + }, + NO_EVENTS, + ] if (item && isReasoningItem(item)) { const events: LLMEvent[] = [] return [ @@ -799,7 +862,28 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* ( const item = event.item if (!item) return [state, NO_EVENTS] satisfies StepResult - if (item.type === "message" && item.id) return onOutputTextDone(state, { ...event, item_id: item.id }) + if (item.type === "message" && item.id) { + const itemPhase = state.messagePhase(item.phase) + const phase = itemPhase === undefined ? state.messagePhases[item.id] : itemPhase + const events: LLMEvent[] = [] + const messageItems = new Set(state.messageItems) + messageItems.delete(item.id) + const { [item.id]: _phase, ...messagePhases } = state.messagePhases + return [ + { + ...state, + lifecycle: Lifecycle.textEnd( + state.lifecycle, + events, + item.id, + phase === undefined ? undefined : providerMetadata(state, { phase }), + ), + messageItems, + messagePhases, + }, + events, + ] satisfies StepResult + } if (item.type === "function_call") { if (!item.id || !item.call_id || !item.name) return [state, NO_EVENTS] satisfies StepResult @@ -899,19 +983,41 @@ const providerError = (state: ParserState, event: Event, fallback: string) => { } export const step = (state: ParserState, event: Event) => { - if (event.type === "response.output_text.delta") return Effect.succeed(onOutputTextDelta(state, event)) - if (event.type === "response.output_text.done") return Effect.succeed(onOutputTextDone(state, event)) - if (event.type === "response.reasoning.delta" || event.type === "response.reasoning_summary_text.delta") - return Effect.succeed(onReasoningDelta(state, event)) - if (event.type === "response.reasoning.done" || event.type === "response.reasoning_summary_text.done") + if (event.type === "response.output_text.delta" || event.type === "response.output_text.done") { + if (!event.item_id) return ProviderShared.eventError(state.id, `${event.type} is missing item_id`) + return Effect.succeed( + event.type === "response.output_text.delta" + ? onOutputTextDelta(state, event, event.item_id) + : onOutputTextDone(state, event, event.item_id), + ) + } + if (event.type === "response.reasoning.delta" || event.type === "response.reasoning_summary_text.delta") { + if (!event.item_id) return ProviderShared.eventError(state.id, `${event.type} is missing item_id`) + return Effect.succeed(onReasoningDelta(state, event, event.item_id)) + } + if (event.type === "response.reasoning.done" || event.type === "response.reasoning_summary_text.done") { + if (!event.item_id) return ProviderShared.eventError(state.id, `${event.type} is missing item_id`) return Effect.succeed(onReasoningDone(state, event)) + } if (event.type === "response.reasoning_summary_part.added") - return Effect.succeed(onReasoningSummaryPartAdded(state, event)) + return event.item_id + ? Effect.succeed(onReasoningSummaryPartAdded(state, event)) + : ProviderShared.eventError(state.id, `${event.type} is missing item_id`) if (event.type === "response.reasoning_summary_part.done") - return Effect.succeed(onReasoningSummaryPartDone(state, event)) - if (event.type === "response.output_item.added") return Effect.succeed(onOutputItemAdded(state, event)) + return event.item_id + ? Effect.succeed(onReasoningSummaryPartDone(state, event)) + : ProviderShared.eventError(state.id, `${event.type} is missing item_id`) + if (event.type === "response.output_item.added") { + if (event.item?.type === "message" && !event.item.id) + return ProviderShared.eventError(state.id, `${event.type} message is missing id`) + return Effect.succeed(onOutputItemAdded(state, event)) + } if (event.type === "response.function_call_arguments.delta") return onFunctionCallArgumentsDelta(state, event) - if (event.type === "response.output_item.done") return onOutputItemDone(state, event) + if (event.type === "response.output_item.done") { + if (event.item?.type === "message" && !event.item.id) + return ProviderShared.eventError(state.id, `${event.type} message is missing id`) + return onOutputItemDone(state, event) + } if (event.type === "response.completed" || event.type === "response.incomplete") return Effect.succeed(onResponseFinish(state, event)) if (event.type === "response.failed") return providerError(state, event, `${state.name} response failed`) @@ -933,10 +1039,18 @@ export const initial = (request: LLMRequest, extension: Extension = BASE): Parse hasFunctionCall: false, tools: ToolStream.empty(), lifecycle: Lifecycle.initial(), + messageItems: new Set(), + messagePhase: (value) => messagePhase(value, extension), + messagePhases: {}, reasoningItems: {}, store: OpenResponsesOptions.resolve(request).store, }) +const messagePhase = (value: unknown, extension: Extension): MessagePhase | null | undefined => { + if (value === "commentary" || value === "final_answer") return value + return extension.messagePhase?.(value) +} + export const protocol = Protocol.make({ id: ADAPTER, body: { diff --git a/packages/ai/src/protocols/openai-responses.ts b/packages/ai/src/protocols/openai-responses.ts index f8655c67294a..43237e89f60a 100644 --- a/packages/ai/src/protocols/openai-responses.ts +++ b/packages/ai/src/protocols/openai-responses.ts @@ -35,8 +35,18 @@ const OpenAIResponsesToolChoice = Schema.Union([ Schema.Struct({ type: Schema.tag("image_generation") }), ]) +const OpenAIResponsesInputItem = Schema.Union([ + Schema.Struct({ + role: Schema.tag("assistant"), + content: Schema.Array(Schema.Struct({ type: Schema.tag("output_text"), text: Schema.String })), + phase: Schema.optionalKey(Schema.NullOr(OpenResponses.MessagePhase)), + }), + OpenResponses.InputItem, +]) + const OpenAIResponsesCoreFields = { ...OpenResponses.coreFields, + input: Schema.Array(OpenAIResponsesInputItem), tools: optionalArray(OpenAIResponsesTools), tool_choice: Schema.optional(OpenAIResponsesToolChoice), } @@ -60,6 +70,7 @@ const encodeWebSocketMessage = Schema.encodeSync(Schema.fromJsonString(OpenAIRes const extension = { id: ADAPTER, name: NAME, + messagePhase: (value: unknown) => (value === null ? null : undefined), lowerMedia: ({ part, media, request }) => { if (request.model.provider !== "xai" || media.mime !== "application/pdf") return undefined return { @@ -102,7 +113,7 @@ const lowerToolChoice = (toolChoice: NonNullable, tool }) const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request: LLMRequest) { - const body = yield* OpenResponses.fromRequest( + const body = yield* OpenResponses.fromRequestWithExtension( LLMRequest.update(request, { tools: [], toolChoice: undefined }), extension, ) @@ -208,9 +219,13 @@ const onHostedToolDone = Effect.fn("OpenAIResponses.onHostedToolDone")(function* const step = (state: OpenResponses.ParserState, event: OpenResponses.Event) => { if (event.type === "response.reasoning_text.delta" || event.type === "response.reasoning_summary.delta") - return Effect.succeed(OpenResponses.onReasoningDelta(state, event)) + return event.item_id + ? Effect.succeed(OpenResponses.onReasoningDelta(state, event, event.item_id)) + : ProviderShared.eventError(ADAPTER, `${event.type} is missing item_id`) if (event.type === "response.reasoning_text.done" || event.type === "response.reasoning_summary.done") - return Effect.succeed(OpenResponses.onReasoningDone(state, event)) + return event.item_id + ? Effect.succeed(OpenResponses.onReasoningDone(state, event)) + : ProviderShared.eventError(ADAPTER, `${event.type} is missing item_id`) if (event.type === "response.output_item.done" && event.item && isHostedToolItem(event.item)) return onHostedToolDone(state, event.item) return OpenResponses.step(state, event) diff --git a/packages/ai/test/provider/openai-compatible-responses.test.ts b/packages/ai/test/provider/openai-compatible-responses.test.ts index 39819821097f..f6591f50d2ce 100644 --- a/packages/ai/test/provider/openai-compatible-responses.test.ts +++ b/packages/ai/test/provider/openai-compatible-responses.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" -import { LLM, LLMEvent } from "../../src" +import { LLM, LLMEvent, Message } from "../../src" import { configure } from "../../src/providers/openai-compatible-responses" import { OpenAI } from "../../src/providers" import { OpenResponses } from "../../src/protocols/open-responses" @@ -70,6 +70,31 @@ describe("Open Responses-compatible route", () => { }), ) + it.effect("omits OpenAI-only nullable phases from the Open Responses baseline", () => + Effect.gen(function* () { + const model = configure({ + apiKey: "test-key", + baseURL: "https://responses.example.test/v1", + }).model("example-model") + const prepared = yield* LLMClient.prepare( + LLM.request({ + model, + messages: [ + Message.assistant({ + type: "text", + text: "Unclassified.", + providerMetadata: { openresponses: { phase: null } }, + }), + ], + }), + ) + + expect(prepared.body).toMatchObject({ + input: [{ role: "assistant", content: [{ type: "output_text", text: "Unclassified." }] }], + }) + }), + ) + it.effect("reads standard options from the Open Responses namespace", () => Effect.gen(function* () { const model = configure({ diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index 43d19ed42ec2..a3d75a7eebf0 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -882,6 +882,121 @@ describe("OpenAI Responses route", () => { }), ) + it.effect("preserves and replays assistant message phases", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { + type: "response.output_item.added", + item: { type: "message", id: "msg_commentary" }, + }, + { type: "response.output_text.delta", item_id: "msg_commentary", delta: "Checking." }, + { type: "response.output_text.done", item_id: "msg_commentary" }, + { + type: "response.output_item.done", + item: { type: "message", id: "msg_commentary", phase: "commentary" }, + }, + { + type: "response.output_item.added", + item: { type: "message", id: "msg_final", phase: "final_answer" }, + }, + { type: "response.output_text.done", item_id: "msg_final", text: "Finished." }, + { + type: "response.output_item.done", + item: { type: "message", id: "msg_final", phase: "final_answer" }, + }, + { type: "response.output_item.added", item: { type: "message", id: "msg_null", phase: null } }, + { type: "response.output_text.delta", item_id: "msg_null", delta: "Unclassified." }, + { type: "response.output_item.done", item: { type: "message", id: "msg_null", phase: null } }, + { type: "response.completed", response: { id: "resp_1" } }, + ), + ), + ), + ) + + expect(response.message.content).toEqual([ + { + type: "text", + text: "Checking.", + providerMetadata: { openai: { phase: "commentary" } }, + }, + { + type: "text", + text: "Finished.", + providerMetadata: { openai: { phase: "final_answer" } }, + }, + { + type: "text", + text: "Unclassified.", + providerMetadata: { openai: { phase: null } }, + }, + ]) + + const prepared = yield* LLMClient.prepare( + LLM.request({ model, messages: [response.message] }), + ) + expect(prepared.body.input).toEqual([ + { + role: "assistant", + content: [{ type: "output_text", text: "Checking." }], + phase: "commentary", + }, + { + role: "assistant", + content: [{ type: "output_text", text: "Finished." }], + phase: "final_answer", + }, + { + role: "assistant", + content: [{ type: "output_text", text: "Unclassified." }], + phase: null, + }, + ]) + }), + ) + + it.effect("rejects output text events without the spec-required item id", () => + Effect.gen(function* () { + const error = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { type: "response.output_text.delta", delta: "orphaned" }, + { type: "response.completed", response: { id: "resp_1" } }, + ), + ), + ), + Effect.flip, + ) + + expect(error.reason._tag).toBe("InvalidProviderOutput") + expect(error.message).toContain("response.output_text.delta is missing item_id") + }), + ) + + it.effect("rejects reasoning events without the spec-required item id", () => + Effect.gen(function* () { + const events = [ + { type: "response.reasoning_summary_part.added", summary_index: 0 }, + { type: "response.reasoning_summary_part.done", summary_index: 0 }, + { type: "response.reasoning_text.done" }, + ] + + for (const event of events) { + const error = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse(sseEvents(event, { type: "response.completed", response: { id: "resp_1" } })), + ), + Effect.flip, + ) + expect(error.reason._tag).toBe("InvalidProviderOutput") + expect(error.message).toContain(`${event.type} is missing item_id`) + } + }), + ) + it.effect("maps incomplete response reasons", () => Effect.gen(function* () { const generate = (incompleteDetails: object) => diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index a1b2367c8b4e..ef614aa1bbde 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -98,8 +98,6 @@ export type SessionMessageShell = { output?: { output: string; cursor: number; size: number; truncated: boolean } } -export type SessionMessageAssistantText = { type: "text"; text: string } - export type SessionMessageProviderState = { [x: string]: JsonValue } export type SessionMessageToolStateStreaming = { status: "streaming"; input: string } @@ -157,8 +155,6 @@ export type ShellInfo = { time: { started: number; completed?: number } } -export type SessionMessageProviderState3 = { [x: string]: any } - export type SessionMessageProviderState4 = { [x: string]: any } export type SessionMessageProviderState5 = { [x: string]: any } @@ -167,6 +163,10 @@ export type SessionMessageProviderState6 = { [x: string]: any } export type SessionMessageProviderState7 = { [x: string]: any } +export type SessionMessageProviderState8 = { [x: string]: any } + +export type SessionMessageProviderState9 = { [x: string]: any } + export type EventLogSynced = { type: "log.synced"; aggregateID: string; seq?: number } export type ModelReasoningField = "reasoning" | "reasoning_content" | "reasoning_text" | (string & {}) @@ -733,16 +733,6 @@ export type SessionTextStarted = { data: { sessionID: string; assistantMessageID: string; ordinal: number } } -export type SessionTextEnded = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.text.ended" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; assistantMessageID: string; ordinal: number; text: string } -} - export type SessionToolInputStarted = { id: string created: number @@ -1249,6 +1239,8 @@ export type SessionPendingSynthetic = { delivery: "steer" | "queue" } +export type SessionMessageAssistantText = { type: "text"; text: string; state?: SessionMessageProviderState } + export type SessionMessageAssistantReasoning = { type: "reasoning" text: string @@ -1359,6 +1351,22 @@ export type ShellCreated = { data: { info: ShellInfo } } +export type SessionTextEnded = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.text.ended" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { + sessionID: string + assistantMessageID: string + ordinal: number + text: string + state?: SessionMessageProviderState4 + } +} + export type SessionReasoningStarted = { id: string created: number @@ -1366,7 +1374,7 @@ export type SessionReasoningStarted = { type: "session.reasoning.started" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef - data: { sessionID: string; assistantMessageID: string; ordinal: number; state?: SessionMessageProviderState3 } + data: { sessionID: string; assistantMessageID: string; ordinal: number; state?: SessionMessageProviderState5 } } export type SessionReasoningEnded = { @@ -1381,7 +1389,7 @@ export type SessionReasoningEnded = { assistantMessageID: string ordinal: number text: string - state?: SessionMessageProviderState4 + state?: SessionMessageProviderState6 } } @@ -1398,7 +1406,7 @@ export type SessionToolCalled = { callID: string input: { [x: string]: any } executed: boolean - state?: SessionMessageProviderState5 + state?: SessionMessageProviderState7 } } @@ -1853,7 +1861,7 @@ export type SessionToolSuccess = { content: [LLMToolContent, ...Array] metadata?: { [x: string]: JsonValue } executed: boolean - resultState?: SessionMessageProviderState6 + resultState?: SessionMessageProviderState8 } } @@ -1872,7 +1880,7 @@ export type SessionToolFailed = { content?: [LLMToolContent, ...Array] metadata?: { [x: string]: JsonValue } executed: boolean - resultState?: SessionMessageProviderState7 + resultState?: SessionMessageProviderState9 } } diff --git a/packages/core/src/session/message-updater.ts b/packages/core/src/session/message-updater.ts index 3a256e043a88..6d9c4723ff23 100644 --- a/packages/core/src/session/message-updater.ts +++ b/packages/core/src/session/message-updater.ts @@ -319,7 +319,10 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { "session.text.ended": (event) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { const match = latestText(draft) - if (match) match.text = event.data.text + if (match) { + match.text = event.data.text + match.state = castDraft(event.data.state) + } }) }, "session.tool.input.started": (event) => { diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts index fcec89352cfa..440f6779b865 100644 --- a/packages/core/src/session/runner/publish-llm-event.ts +++ b/packages/core/src/session/runner/publish-llm-event.ts @@ -149,13 +149,14 @@ export const createLLMEventPublisher = (events: Pick + (_textID, value, ordinal, state) => Effect.gen(function* () { yield* events.publish(SessionEvent.Text.Ended, { sessionID: input.sessionID, assistantMessageID: yield* currentAssistantMessageID(), ordinal, text: value, + state, }) }), true, @@ -326,7 +327,7 @@ export const createLLMEventPublisher = (events: Pick { - if (item.type === "text") return [{ type: "text", text: item.text }] + if (item.type === "text") + return [ + { + type: "text", + text: item.text, + providerMetadata: sameProvider ? providerMetadata(providerMetadataKey, item.state) : undefined, + }, + ] if (item.type === "reasoning") return reuseProviderMetadata ? [ diff --git a/packages/core/test/session-runner-message.test.ts b/packages/core/test/session-runner-message.test.ts index 3cebdcb985d1..de33bcd69dc4 100644 --- a/packages/core/test/session-runner-message.test.ts +++ b/packages/core/test/session-runner-message.test.ts @@ -767,4 +767,35 @@ Recent work }, ]) }) + + test("preserves assistant text provider state across same-provider model changes and failures", () => { + const messages = toLLMMessages( + [ + SessionMessage.Assistant.make({ + id: id("assistant-phase"), + type: "assistant", + agent: build, + model: { id: ModelV2.ID.make("old"), providerID: ProviderV2.ID.make("provider") }, + content: [ + SessionMessage.AssistantText.make({ + type: "text", + text: "Checking.", + state: { phase: "commentary" }, + }), + ], + error: { type: "provider.unknown", message: "Interrupted after commentary" }, + time: { created, completed: created }, + }), + ], + ModelV2.Ref.make({ id: ModelV2.ID.make("new"), providerID: ProviderV2.ID.make("provider") }), + ) + + expect(messages[0]?.content).toEqual([ + { + type: "text", + text: "Checking.", + providerMetadata: { provider: { phase: "commentary" } }, + }, + ]) + }) }) diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 5e07b3551af6..1f96f41cbc75 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -2672,6 +2672,47 @@ describe("SessionRunnerLLM", () => { }), ) + it.effect("restores durable text provider metadata in the next request", () => + Effect.gen(function* () { + const session = yield* setup + yield* admit(session, "Check first") + + response = [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.textStart({ id: "commentary", providerMetadata: { openai: { phase: "commentary" } } }), + LLMEvent.textDelta({ id: "commentary", text: "Checking." }), + LLMEvent.textEnd({ + id: "commentary", + providerMetadata: { openai: { phase: "commentary" }, anthropic: { ignored: true } }, + }), + LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } }), + LLMEvent.finish({ reason: { normalized: "stop" } }), + ] + yield* session.resume(sessionID) + yield* replaySessionProjection(sessionID) + + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Check first" }, + { + type: "assistant", + content: [{ type: "text", text: "Checking.", state: { phase: "commentary" } }], + }, + ]) + + yield* admit(session, "Continue") + response = [] + yield* session.resume(sessionID) + + expect(requests[1]?.messages[1]?.content).toEqual([ + { + type: "text", + text: "Checking.", + providerMetadata: { openai: { phase: "commentary" } }, + }, + ]) + }), + ) + it.effect("replays durable provider-executed tool results inline in the next request", () => Effect.gen(function* () { const session = yield* setup diff --git a/packages/schema/src/session-event.ts b/packages/schema/src/session-event.ts index faa4853adedc..3a31643bbbf7 100644 --- a/packages/schema/src/session-event.ts +++ b/packages/schema/src/session-event.ts @@ -314,6 +314,7 @@ export namespace Text { assistantMessageID: SessionMessage.ID, ordinal: NonNegativeInt, text: Schema.String, + state: SessionMessage.ProviderState.pipe(optional), }, }) export type Ended = typeof Ended.Type diff --git a/packages/schema/src/session-message.ts b/packages/schema/src/session-message.ts index c30ffc6aa465..4e0858e40554 100644 --- a/packages/schema/src/session-message.ts +++ b/packages/schema/src/session-message.ts @@ -155,6 +155,7 @@ export interface AssistantText extends Schema.Schema.Type export const AssistantText = Schema.Struct({ type: Schema.tag("text"), text: Schema.String, + state: ProviderState.pipe(optional), }).annotate({ identifier: "Session.Message.Assistant.Text" }) export interface AssistantReasoning extends Schema.Schema.Type {} From 1e35d33ecba56d218620d887518ad1d0a0d1a6f1 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 25 Jul 2026 14:17:49 -0500 Subject: [PATCH 112/150] fix(codemode): search nested namespaces (#38887) --- packages/codemode/src/tool-runtime.ts | 8 +++++--- packages/codemode/test/tool-paths.test.ts | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index f1e189f7335f..5200c13b26ad 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -342,7 +342,6 @@ export type DiscoveryPlan = { export type SearchEntry = { readonly description: ToolDescription - readonly namespace: string readonly searchText: string } @@ -373,7 +372,11 @@ const makeSearchTool = (searchIndex: ReadonlyArray): Tool => ({ const scoped = request.namespace === undefined ? searchIndex - : searchIndex.filter((entry) => entry.namespace === request.namespace) + : searchIndex.filter( + (entry) => + entry.description.path === request.namespace || + entry.description.path.startsWith(`${request.namespace}.`), + ) const trimmed = query.trim() const pathQuery = trimmed.startsWith("tools.") ? trimmed.slice("tools.".length) : trimmed const exact = @@ -428,7 +431,6 @@ export const searchSignature = (() => { const toSearchEntry = (path: string, tool: Tool, description: ToolDescription): SearchEntry => ({ description, - namespace: path.split(".", 1)[0]!, searchText: [ path, tool.description, diff --git a/packages/codemode/test/tool-paths.test.ts b/packages/codemode/test/tool-paths.test.ts index 3cc92d6dfcdb..b90b7194442b 100644 --- a/packages/codemode/test/tool-paths.test.ts +++ b/packages/codemode/test/tool-paths.test.ts @@ -54,6 +54,27 @@ describe("dotted tool names", () => { expect(flat.catalog()[0]?.path).toBe("issues.list") expect(await value(flat, `return await tools.issues.list({})`)).toBe("flat") }) + + test("search scopes to a nested namespace subtree", async () => { + const nested = CodeMode.make({ + tools: { + slack: { + admin: echo("Admin", "admin"), + "admin.invite": echo("Invite", "invite"), + "admin.users.list": echo("List users", "users"), + "administrator.list": echo("List administrators", "administrators"), + read: echo("Read Slack", "read"), + }, + }, + }) + + const result = await value(nested, `return search({ query: "", namespace: "slack.admin" })`) + expect((result as { items: Array<{ path: string }> }).items.map((item) => item.path)).toEqual([ + "tools.slack.admin", + "tools.slack.admin.invite", + "tools.slack.admin.users.list", + ]) + }) }) describe("callable namespaces", () => { From f753103e82abc1bc8b740c9b3878dacac6a2de1d Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 25 Jul 2026 14:43:18 -0500 Subject: [PATCH 113/150] fix(core): reject file glob roots (#38890) --- packages/core/src/tool/glob.ts | 6 +++++- packages/core/test/tool-search.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/core/src/tool/glob.ts b/packages/core/src/tool/glob.ts index edcf98d7f1db..5b269d8dccde 100644 --- a/packages/core/src/tool/glob.ts +++ b/packages/core/src/tool/glob.ts @@ -83,13 +83,17 @@ export const Plugin = { agent: context.agent, source, }) - yield* fs + const info = yield* fs .stat(target.canonical) .pipe( Effect.catchReason("PlatformError", "NotFound", () => Effect.fail(new ToolFailure({ message: `Search path does not exist: ${input.path ?? "."}` })), ), ) + if (info.type !== "Directory") + return yield* Effect.fail( + new ToolFailure({ message: `Search path is not a directory: ${input.path ?? "."}` }), + ) const root = path.resolve(location.directory, input.path ?? ".") const limit = input.limit ?? FileSystem.DEFAULT_SEARCH_LIMIT const entries = yield* ripgrep diff --git a/packages/core/test/tool-search.test.ts b/packages/core/test/tool-search.test.ts index 022599e88412..b9da2007639d 100644 --- a/packages/core/test/tool-search.test.ts +++ b/packages/core/test/tool-search.test.ts @@ -143,6 +143,29 @@ describe("search tools", () => { ) } + it.live("reports a file used as the glob search path", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => + Effect.promise(() => fs.writeFile(path.join(tmp.path, "file.txt"), "content\n")).pipe( + Effect.andThen( + withTools(tmp.path, (registry) => + executeTool(registry, call("glob", { path: "file.txt", pattern: "*" })), + ), + ), + Effect.tap((result) => + Effect.sync(() => { + expect(result).toEqual({ + status: "error", + error: { type: "tool.execution", message: "Search path is not a directory: file.txt" }, + }) + }), + ), + ), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + it.live("requires external_directory approval for an explicit external glob path", () => Effect.acquireUseRelease( Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), From 9eea5bc925e5f9e8867d76fa9f31359351468db0 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 25 Jul 2026 16:52:36 -0500 Subject: [PATCH 114/150] fix(core): tweak glob tool description/parameters (#38899) --- packages/core/src/tool/glob.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/core/src/tool/glob.ts b/packages/core/src/tool/glob.ts index 5b269d8dccde..39929ad44bf5 100644 --- a/packages/core/src/tool/glob.ts +++ b/packages/core/src/tool/glob.ts @@ -18,10 +18,10 @@ export const name = "glob" export const Input = Schema.Struct({ pattern: FileSystem.GlobInput.fields.pattern.annotate({ description: "Glob pattern to match files against" }), path: RelativePath.pipe(Schema.optional).annotate({ - description: "Relative directory to search. Defaults to the active Location.", + description: "Directory to search. Defaults to the current working directory.", }), limit: FileSystem.GlobInput.fields.limit.annotate({ - description: `Maximum results to return (default: ${FileSystem.DEFAULT_SEARCH_LIMIT})`, + description: `Maximum number of matching files to return (default: ${FileSystem.DEFAULT_SEARCH_LIMIT})`, }), }) @@ -55,13 +55,14 @@ export const Plugin = { name, Tool.make({ description: - "Find files by glob pattern within the active Location. Returns concise relative file resources. Use a relative path to narrow the search and limit to bound the result count.", + 'Search file paths using a glob pattern (examples: "**/*.ts", "src/**/*.tsx").', input: Input, output: Output, execute: (input, context) => Effect.gen(function* () { + const searchPath = input.path === "undefined" || input.path === "null" ? undefined : input.path const source = { type: "tool" as const, messageID: context.messageID, callID: context.callID } - const target = yield* mutation.resolve({ path: input.path ?? ".", kind: "directory" }) + const target = yield* mutation.resolve({ path: searchPath ?? ".", kind: "directory" }) const external = target.externalDirectory if (external) yield* permission.assert({ @@ -75,8 +76,8 @@ export const Plugin = { resources: [input.pattern], save: ["*"], metadata: { - root: input.path ?? ".", - path: input.path, + root: searchPath ?? ".", + path: searchPath, limit: input.limit, }, sessionID: context.sessionID, @@ -87,14 +88,14 @@ export const Plugin = { .stat(target.canonical) .pipe( Effect.catchReason("PlatformError", "NotFound", () => - Effect.fail(new ToolFailure({ message: `Search path does not exist: ${input.path ?? "."}` })), + Effect.fail(new ToolFailure({ message: `Search path does not exist: ${searchPath ?? "."}` })), ), ) if (info.type !== "Directory") return yield* Effect.fail( - new ToolFailure({ message: `Search path is not a directory: ${input.path ?? "."}` }), + new ToolFailure({ message: `Search path is not a directory: ${searchPath ?? "."}` }), ) - const root = path.resolve(location.directory, input.path ?? ".") + const root = path.resolve(location.directory, searchPath ?? ".") const limit = input.limit ?? FileSystem.DEFAULT_SEARCH_LIMIT const entries = yield* ripgrep .glob({ From 7d8f1bdab3b80e35358718924ad381d9674c523e Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:09:20 -0500 Subject: [PATCH 115/150] tweak(core): simplify skill tool description (#38900) --- packages/core/src/tool/skill.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/core/src/tool/skill.ts b/packages/core/src/tool/skill.ts index c39748920400..48bdb0b17f7a 100644 --- a/packages/core/src/tool/skill.ts +++ b/packages/core/src/tool/skill.ts @@ -22,9 +22,7 @@ export const Output = Schema.Struct({ output: Schema.String, }) export const description = [ - "Load a specialized skill when the task at hand matches one of the available skills in the instructions.", - "", - "Use this tool to inject the skill's instructions and resources into the current conversation. The output may contain detailed workflow guidance as well as references to scripts, files, etc. in the same directory as the skill.", + "Load a specialized skill's instructions and resources into the current conversation when the task at hand matches its description.", "", "The skill ID must match one of the available skills in the instructions.", ].join("\n") From 203a0613b81920796b81d3c02bc737957e2da9f5 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 25 Jul 2026 21:14:47 -0400 Subject: [PATCH 116/150] feat(www): migrate docs to Blume --- .github/workflows/deploy-www.yml | 38 + .github/workflows/test.yml | 2 +- bun.lock | 3132 +++++++++-------- packages/core/src/plugin/skill/opencode.md | 2 +- packages/docs/AGENTS.md | 22 - packages/docs/README.md | 33 - packages/docs/api/index.mdx | 6 - packages/docs/assets/favicon.svg | 5 - packages/docs/assets/logo-dark.svg | 10 - packages/docs/assets/logo-light.svg | 10 - packages/docs/build/client.mdx | 183 - packages/docs/build/index.mdx | 24 - packages/docs/build/plugins.mdx | 446 --- packages/docs/build/sdk.mdx | 84 - packages/docs/docs.json | 78 - packages/docs/lsp.mdx | 105 - packages/docs/package.json | 17 - packages/docs/permissions.mdx | 209 -- packages/docs/references.mdx | 177 - packages/www/.gitignore | 10 +- packages/www/AGENTS.md | 22 + packages/www/README.md | 11 +- packages/www/blume.config.ts | 43 + packages/www/components.ts | 8 + .../content/docs/(Configure)}/agents.mdx | 8 +- .../content/docs/(Configure)}/attachments.mdx | 8 +- .../content/docs/(Configure)}/commands.mdx | 4 +- .../content/docs/(Configure)}/compaction.mdx | 0 .../content/docs/(Configure)}/formatters.mdx | 4 +- .../docs/(Configure)}/instructions.mdx | 8 +- .../docs/{(docs) => (Configure)}/lsp.mdx | 4 +- .../content/docs/(Configure)}/mcp-servers.mdx | 0 packages/www/content/docs/(Configure)/meta.ts | 24 + .../content/docs/(Configure)}/models.mdx | 0 .../{(docs) => (Configure)}/permissions.mdx | 8 +- .../content/docs/(Configure)}/providers.mdx | 2 +- .../{(docs) => (Configure)}/references.mdx | 8 +- .../content/docs/(Configure)}/sharing.mdx | 4 +- .../content/docs/(Configure)}/skills.mdx | 0 .../content/docs/(Configure)}/snapshots.mdx | 8 +- .../content/docs/(Configure)}/themes.mdx | 10 +- .../content/docs/(Configure)}/warming.mdx | 0 packages/www/content/docs/(docs)/agents.mdx | 283 -- .../www/content/docs/(docs)/attachments.mdx | 168 - packages/www/content/docs/(docs)/commands.mdx | 163 - .../www/content/docs/(docs)/compaction.mdx | 153 - packages/www/content/docs/(docs)/config.mdx | 450 --- .../www/content/docs/(docs)/formatters.mdx | 93 - packages/www/content/docs/(docs)/index.mdx | 160 - .../www/content/docs/(docs)/instructions.mdx | 128 - .../www/content/docs/(docs)/mcp-servers.mdx | 262 -- packages/www/content/docs/(docs)/meta.json | 28 - .../www/content/docs/(docs)/migrate-v1.mdx | 576 --- packages/www/content/docs/(docs)/models.mdx | 213 -- .../www/content/docs/(docs)/providers.mdx | 204 -- packages/www/content/docs/(docs)/sharing.mdx | 39 - packages/www/content/docs/(docs)/skills.mdx | 227 -- .../www/content/docs/(docs)/snapshots.mdx | 108 - .../content/docs/(docs)/troubleshooting.mdx | 177 - packages/www/content/docs/api/index.mdx | 6 - packages/www/content/docs/api/meta.json | 6 - packages/www/content/docs/build/client.mdx | 61 +- packages/www/content/docs/build/index.mdx | 11 +- packages/www/content/docs/build/meta.json | 7 - packages/www/content/docs/build/meta.ts | 6 + packages/www/content/docs/build/plugins.mdx | 23 +- packages/www/content/docs/build/sdk.mdx | 25 +- packages/{ => www/content}/docs/config.mdx | 4 +- packages/{ => www/content}/docs/index.mdx | 18 +- packages/www/content/docs/meta.json | 4 - packages/www/content/docs/meta.ts | 5 + .../{ => www/content}/docs/migrate-v1.mdx | 10 +- .../content}/docs/troubleshooting.mdx | 16 +- packages/{docs => www}/openapi.json | 0 packages/www/package.json | 41 +- packages/www/pages/index.astro | 5 + .../script/generate-theme-tokens.ts | 4 +- packages/www/script/prepare-cloudflare.ts | 8 + .../snippets/generated/theme-tokens.mdx | 2 +- packages/www/source.config.ts | 5 - packages/www/src/components/mdx.tsx | 55 - packages/www/src/lib/layout.tsx | 17 - packages/www/src/lib/source.ts | 7 - packages/www/src/routeTree.gen.ts | 104 - packages/www/src/router.tsx | 16 - packages/www/src/routes/__root.tsx | 34 - packages/www/src/routes/api/search.ts | 15 - packages/www/src/routes/docs/$.tsx | 62 - packages/www/src/routes/index.tsx | 18 - packages/www/src/styles.css | 85 - packages/www/tsconfig.json | 23 +- packages/www/vite.config.ts | 22 - packages/www/wrangler.jsonc | 40 +- script/generate.ts | 2 +- 94 files changed, 1953 insertions(+), 7023 deletions(-) create mode 100644 .github/workflows/deploy-www.yml delete mode 100644 packages/docs/AGENTS.md delete mode 100644 packages/docs/README.md delete mode 100644 packages/docs/api/index.mdx delete mode 100644 packages/docs/assets/favicon.svg delete mode 100644 packages/docs/assets/logo-dark.svg delete mode 100644 packages/docs/assets/logo-light.svg delete mode 100644 packages/docs/build/client.mdx delete mode 100644 packages/docs/build/index.mdx delete mode 100644 packages/docs/build/plugins.mdx delete mode 100644 packages/docs/build/sdk.mdx delete mode 100644 packages/docs/docs.json delete mode 100644 packages/docs/lsp.mdx delete mode 100644 packages/docs/package.json delete mode 100644 packages/docs/permissions.mdx delete mode 100644 packages/docs/references.mdx create mode 100644 packages/www/AGENTS.md create mode 100644 packages/www/blume.config.ts create mode 100644 packages/www/components.ts rename packages/{docs => www/content/docs/(Configure)}/agents.mdx (99%) rename packages/{docs => www/content/docs/(Configure)}/attachments.mdx (98%) rename packages/{docs => www/content/docs/(Configure)}/commands.mdx (99%) rename packages/{docs => www/content/docs/(Configure)}/compaction.mdx (100%) rename packages/{docs => www/content/docs/(Configure)}/formatters.mdx (98%) rename packages/{docs => www/content/docs/(Configure)}/instructions.mdx (98%) rename packages/www/content/docs/{(docs) => (Configure)}/lsp.mdx (98%) rename packages/{docs => www/content/docs/(Configure)}/mcp-servers.mdx (100%) create mode 100644 packages/www/content/docs/(Configure)/meta.ts rename packages/{docs => www/content/docs/(Configure)}/models.mdx (100%) rename packages/www/content/docs/{(docs) => (Configure)}/permissions.mdx (99%) rename packages/{docs => www/content/docs/(Configure)}/providers.mdx (98%) rename packages/www/content/docs/{(docs) => (Configure)}/references.mdx (98%) rename packages/{docs => www/content/docs/(Configure)}/sharing.mdx (97%) rename packages/{docs => www/content/docs/(Configure)}/skills.mdx (100%) rename packages/{docs => www/content/docs/(Configure)}/snapshots.mdx (98%) rename packages/{docs => www/content/docs/(Configure)}/themes.mdx (97%) rename packages/{docs => www/content/docs/(Configure)}/warming.mdx (100%) delete mode 100644 packages/www/content/docs/(docs)/agents.mdx delete mode 100644 packages/www/content/docs/(docs)/attachments.mdx delete mode 100644 packages/www/content/docs/(docs)/commands.mdx delete mode 100644 packages/www/content/docs/(docs)/compaction.mdx delete mode 100644 packages/www/content/docs/(docs)/config.mdx delete mode 100644 packages/www/content/docs/(docs)/formatters.mdx delete mode 100644 packages/www/content/docs/(docs)/index.mdx delete mode 100644 packages/www/content/docs/(docs)/instructions.mdx delete mode 100644 packages/www/content/docs/(docs)/mcp-servers.mdx delete mode 100644 packages/www/content/docs/(docs)/meta.json delete mode 100644 packages/www/content/docs/(docs)/migrate-v1.mdx delete mode 100644 packages/www/content/docs/(docs)/models.mdx delete mode 100644 packages/www/content/docs/(docs)/providers.mdx delete mode 100644 packages/www/content/docs/(docs)/sharing.mdx delete mode 100644 packages/www/content/docs/(docs)/skills.mdx delete mode 100644 packages/www/content/docs/(docs)/snapshots.mdx delete mode 100644 packages/www/content/docs/(docs)/troubleshooting.mdx delete mode 100644 packages/www/content/docs/api/index.mdx delete mode 100644 packages/www/content/docs/api/meta.json delete mode 100644 packages/www/content/docs/build/meta.json create mode 100644 packages/www/content/docs/build/meta.ts rename packages/{ => www/content}/docs/config.mdx (99%) rename packages/{ => www/content}/docs/index.mdx (90%) delete mode 100644 packages/www/content/docs/meta.json create mode 100644 packages/www/content/docs/meta.ts rename packages/{ => www/content}/docs/migrate-v1.mdx (99%) rename packages/{ => www/content}/docs/troubleshooting.mdx (97%) rename packages/{docs => www}/openapi.json (100%) create mode 100644 packages/www/pages/index.astro rename packages/{docs => www}/script/generate-theme-tokens.ts (97%) create mode 100644 packages/www/script/prepare-cloudflare.ts rename packages/{docs => www}/snippets/generated/theme-tokens.mdx (99%) delete mode 100644 packages/www/source.config.ts delete mode 100644 packages/www/src/components/mdx.tsx delete mode 100644 packages/www/src/lib/layout.tsx delete mode 100644 packages/www/src/lib/source.ts delete mode 100644 packages/www/src/routeTree.gen.ts delete mode 100644 packages/www/src/router.tsx delete mode 100644 packages/www/src/routes/__root.tsx delete mode 100644 packages/www/src/routes/api/search.ts delete mode 100644 packages/www/src/routes/docs/$.tsx delete mode 100644 packages/www/src/routes/index.tsx delete mode 100644 packages/www/src/styles.css delete mode 100644 packages/www/vite.config.ts diff --git a/.github/workflows/deploy-www.yml b/.github/workflows/deploy-www.yml new file mode 100644 index 000000000000..e636cbf970b0 --- /dev/null +++ b/.github/workflows/deploy-www.yml @@ -0,0 +1,38 @@ +name: deploy-www + +on: + push: + branches: + - dev + - v2 + workflow_dispatch: + +concurrency: + group: deploy-www-${{ github.ref_name }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + deploy: + if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'v2') + runs-on: ubuntu-latest + environment: ${{ github.ref_name == 'v2' && 'production' || 'dev' }} + steps: + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + + - uses: ./.github/actions/setup-bun + + - name: Build + working-directory: packages/www + run: bun run build + env: + BLUME_ENV: ${{ github.ref_name == 'v2' && 'production' || 'dev' }} + CLOUDFLARE_ENV: ${{ github.ref_name == 'v2' && 'production' || 'dev' }} + + - name: Deploy + working-directory: packages/www + run: bun run deploy + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1ae28ea87449..dcae87cda700 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -99,7 +99,7 @@ jobs: - name: Check generated documentation if: runner.os == 'Linux' - working-directory: packages/docs + working-directory: packages/www run: bun run check:generated e2e: diff --git a/bun.lock b/bun.lock index fb36652e940b..56620c0a20fe 100644 --- a/bun.lock +++ b/bun.lock @@ -468,14 +468,6 @@ "@parcel/watcher-win32-x64": "2.5.1", }, }, - "packages/docs": { - "name": "@opencode-ai/docs", - "devDependencies": { - "effect": "catalog:", - "mint": "4.2.666", - "prettier": "3.6.2", - }, - }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", "version": "1.18.4", @@ -1044,29 +1036,15 @@ }, "packages/www": { "name": "@opencode-ai/www", - "version": "1.17.18", "dependencies": { - "@cloudflare/vite-plugin": "1.44.0", - "@tailwindcss/vite": "4.3.2", - "@tanstack/react-router": "1.170.17", - "@tanstack/react-start": "1.168.27", - "@tanstack/router-plugin": "1.168.19", - "fumadocs-core": "16.11.1", - "fumadocs-mdx": "15.1.0", - "fumadocs-ui": "16.11.1", - "react": "19.2.7", - "react-dom": "19.2.7", - "tailwindcss": "4.3.2", - "vite": "8.1.4", + "blume": "1.1.4", }, "devDependencies": { - "@types/mdx": "2.0.14", - "@types/node": "catalog:", - "@types/react": "19.2.17", - "@types/react-dom": "19.2.3", - "@typescript/native-preview": "catalog:", - "@vitejs/plugin-react": "6.0.3", - "typescript": "catalog:", + "@astrojs/cloudflare": "14.1.4", + "@types/bun": "catalog:", + "astro": "7.1.3", + "effect": "catalog:", + "prettier": "3.6.2", "wrangler": "4.110.0", }, }, @@ -1228,12 +1206,12 @@ "@ai-sdk/xai": ["@ai-sdk/xai@3.0.102", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.56", "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.35" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-NeQyOR7OCqDMgaLS4uNX/ep/HrwUzzFYLzXQSRoqLy2jsnqxAJhsgltRwAwf+ADjyPBIAKEOestWnIQA+LrLrQ=="], - "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.5", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw=="], - "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.71.2", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ=="], "@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="], @@ -1264,16 +1242,44 @@ "@astrojs/compiler": ["@astrojs/compiler@2.13.1", "", {}, "sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg=="], + "@astrojs/compiler-binding": ["@astrojs/compiler-binding@0.3.1", "", { "optionalDependencies": { "@astrojs/compiler-binding-darwin-arm64": "0.3.1", "@astrojs/compiler-binding-darwin-x64": "0.3.1", "@astrojs/compiler-binding-linux-arm64-gnu": "0.3.1", "@astrojs/compiler-binding-linux-arm64-musl": "0.3.1", "@astrojs/compiler-binding-linux-x64-gnu": "0.3.1", "@astrojs/compiler-binding-linux-x64-musl": "0.3.1", "@astrojs/compiler-binding-wasm32-wasi": "0.3.1", "@astrojs/compiler-binding-win32-arm64-msvc": "0.3.1", "@astrojs/compiler-binding-win32-x64-msvc": "0.3.1" } }, "sha512-DaAUj29AIBU2XdJ8uwcab8lW5O2pk9pY8AXkcMw0sw77nVa3oeTYRcO+Dvbbpoexf6ThMc0FMWYCQ/wN1/T7oQ=="], + + "@astrojs/compiler-binding-darwin-arm64": ["@astrojs/compiler-binding-darwin-arm64@0.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IEmEF2fUIlTHtpeE/isyEGVOB14cEyh/LZOFYt6wn3jNyVpdC8aR5OZ+RzFUR/f+8ZDM1LaMwZKvoA7eMyJeFw=="], + + "@astrojs/compiler-binding-darwin-x64": ["@astrojs/compiler-binding-darwin-x64@0.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-GF2kIxjpPDLsn94zbZNMsxEmkU828QqnmM7kiQJnaooS3jmI+I7kk6+oI6EpwOsK3femCMdcm+wmOsEqtGrmjQ=="], + + "@astrojs/compiler-binding-linux-arm64-gnu": ["@astrojs/compiler-binding-linux-arm64-gnu@0.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-XJL3SDmOtVrqFhCirNcHwE91+IesJqlgNo23I4qW9QUYfwzm/TBZuH61fgqsb1ttgR1mMYz6ooPWs0JDhwMqpQ=="], + + "@astrojs/compiler-binding-linux-arm64-musl": ["@astrojs/compiler-binding-linux-arm64-musl@0.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-xqE8BVbDoBueK/B47w30PtkVofUWJKGkwoMVE+EOMLf11rnoANxIAdA9FPqY+rng4oNI5ndHGsri1yPj2k8vZQ=="], + + "@astrojs/compiler-binding-linux-x64-gnu": ["@astrojs/compiler-binding-linux-x64-gnu@0.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-1y0StU1qiCuDFH3rmbRJXcxdfHxFPrES1Rd+RLffosvUR7I2cH5SF5SFnBN9vXpzpkmyElZm3Yr47iJBPN7vVA=="], + + "@astrojs/compiler-binding-linux-x64-musl": ["@astrojs/compiler-binding-linux-x64-musl@0.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-16q0fYf7kpbmdObZEeZJEup8hQv/whgNwVjrSvT8umrKwLDSnNIWiQpm09lQQu6bweZB0XyIvHwlPitvJhC+hg=="], + + "@astrojs/compiler-binding-wasm32-wasi": ["@astrojs/compiler-binding-wasm32-wasi@0.3.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-cB456shIwDv/PrVT+2QG7LFndpHkVge5HjqADKZgGaAc9JHVktCtjSrcdkRQ+3tbkPazNKaTLRjXLIiz2NIx9g=="], + + "@astrojs/compiler-binding-win32-arm64-msvc": ["@astrojs/compiler-binding-win32-arm64-msvc@0.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-ur/9+If/yTE69mmeX5MqSZndL0HOyx67GeNZUy3N7wVdWpLz9UTJXwyWS4UR2PUQHitghjsM5xoX0Ge56WRVQQ=="], + + "@astrojs/compiler-binding-win32-x64-msvc": ["@astrojs/compiler-binding-win32-x64-msvc@0.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-k0W+kDBzDkNZOqu4kElDvCOIbKw5Ut9S1WZ1Krj3KTgNuBERNKXsMMsRLLcbgfdMdbe7bTekQLshZrrvmYpmwA=="], + + "@astrojs/compiler-rs": ["@astrojs/compiler-rs@0.3.1", "", { "dependencies": { "@astrojs/compiler-binding": "0.3.1" } }, "sha512-aT7xkgsbNoS6nriY5qKpbihK43slFHO41iqgHCTdOvn1ifaQxLCc5yXy+6GzAtiafoaC1zA7OwVXCXMsvUZOkg=="], + "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.1", "", {}, "sha512-7dwEVigz9vUWDw3nRwLQ/yH/xYovlUA0ZD86xoeKEBmkz9O6iELG1yri67PgAPW6VLL/xInA4t7H0CK6VmtkKQ=="], "@astrojs/language-server": ["@astrojs/language-server@2.16.12", "", { "dependencies": { "@astrojs/compiler": "^2.13.1", "@astrojs/yaml2ts": "^0.2.4", "@jridgewell/sourcemap-codec": "^1.5.5", "@volar/kit": "~2.4.28", "@volar/language-core": "~2.4.28", "@volar/language-server": "~2.4.28", "@volar/language-service": "~2.4.28", "muggle-string": "^0.4.1", "tinyglobby": "^0.2.16", "volar-service-css": "0.0.71", "volar-service-emmet": "0.0.71", "volar-service-html": "0.0.71", "volar-service-prettier": "0.0.71", "volar-service-typescript": "0.0.71", "volar-service-typescript-twoslash-queries": "0.0.71", "volar-service-yaml": "0.0.71", "vscode-html-languageservice": "^5.6.2", "vscode-uri": "^3.1.0" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "./bin/nodeServer.js" } }, "sha512-3LpFphBCzveUgm5ZVDINB/v3YA4TgPa1EMOEFn3Zt/Ww6jojR25iN+kmzeUz7v/b9xkmq+hMACTX4hizN3VCEQ=="], "@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.1", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.2.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "remark-smartypants": "^3.0.2", "shiki": "^3.0.0", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-c5F5gGrkczUaTVgmMW9g1YMJGzOtRvjjhw6IfGuxarM6ct09MpwysP10US729dy07gg8y+ofVifezvP3BNsWZg=="], + "@astrojs/markdown-satteri": ["@astrojs/markdown-satteri@0.3.4", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.1", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "satteri": "^0.9.1" } }, "sha512-6Lvt/bQZEBW+zzdhPblvfZEy5PGEYJaUsUqaCgwHeRPxZJL1gc9I+DRLKWJjjYTWDzVUTzXlMq4WwSK+X34CVw=="], + "@astrojs/mdx": ["@astrojs/mdx@4.3.14", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.11", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.15.0", "es-module-lexer": "^1.7.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-FBrqJQORVm+rkRa2TS5CjU9PBA6hkhrwLVBSS9A77gN2+iehvjq1w6yya/d0YKC7osiVorKkr3Qd9wNbl0ZkGA=="], + "@astrojs/node": ["@astrojs/node@11.0.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.1", "send": "^1.2.1", "server-destroy": "^1.0.1" }, "peerDependencies": { "astro": "^7.0.0" } }, "sha512-/ijULxT+A5Cm8wSwWZ2vgqfim1b05D6B8n/a9l6MMA4FCotIH73g7fL7y76XojKXpTe75FVvQH92OxsMqea9kQ=="], + "@astrojs/prism": ["@astrojs/prism@3.2.0", "", { "dependencies": { "prismjs": "^1.29.0" } }, "sha512-GilTHKGCW6HMq7y3BUv9Ac7GMe/MO9gi9GW62GzKtth0SwukCu/qp2wLiGpEujhY+VVhaG9v7kv/5vFzvf4NYw=="], + "@astrojs/react": ["@astrojs/react@6.0.1", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.1", "@vitejs/plugin-react": "^5.2.0", "devalue": "^5.8.1", "ultrahtml": "^1.6.0", "vite": "^8.0.13" }, "peerDependencies": { "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0", "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" } }, "sha512-Afs1sEm72P2plDnrOGxmIteJ7bjx/VqxlcaQLNip5eHJ5tIvKUORQetC9UKcvgwKnj51t60HWl5mOANkOsWs4w=="], + "@astrojs/sitemap": ["@astrojs/sitemap@3.7.3", "", { "dependencies": { "sitemap": "^9.0.0", "stream-replace-string": "^2.0.0", "zod": "^4.3.6" } }, "sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA=="], "@astrojs/solid-js": ["@astrojs/solid-js@5.1.0", "", { "dependencies": { "vite": "^6.3.5", "vite-plugin-solid": "^2.11.6" }, "peerDependencies": { "solid-devtools": "^0.30.1", "solid-js": "^1.8.5" }, "optionalPeers": ["solid-devtools"] }, "sha512-VmPHOU9k7m6HHCT2Y1mNzifilUnttlowBM36frGcfj5wERJE9Ci0QtWJbzdf6AlcoIirb7xVw+ByupU011Di9w=="], @@ -1284,11 +1290,9 @@ "@astrojs/underscore-redirects": ["@astrojs/underscore-redirects@1.0.0", "", {}, "sha512-qZxHwVnmb5FXuvRsaIGaqWgnftjCuMY+GSbaVZdBmE4j8AfgPqKPxYp8SUERyJcjpKCEmO4wD6ybuGH8A2kVRQ=="], - "@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.4", "", { "dependencies": { "yaml": "^2.8.3" } }, "sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A=="], - - "@asyncapi/parser": ["@asyncapi/parser@3.4.0", "", { "dependencies": { "@asyncapi/specs": "^6.8.0", "@openapi-contrib/openapi-schema-to-json-schema": "~3.2.0", "@stoplight/json": "3.21.0", "@stoplight/json-ref-readers": "^1.2.2", "@stoplight/json-ref-resolver": "^3.1.5", "@stoplight/spectral-core": "^1.18.3", "@stoplight/spectral-functions": "^1.7.2", "@stoplight/spectral-parsers": "^1.0.2", "@stoplight/spectral-ref-resolver": "^1.0.3", "@stoplight/types": "^13.12.0", "@types/json-schema": "^7.0.11", "@types/urijs": "^1.19.19", "ajv": "^8.17.1", "ajv-errors": "^3.0.0", "ajv-formats": "^2.1.1", "avsc": "^5.7.5", "js-yaml": "^4.1.0", "jsonpath-plus": "^10.0.0", "node-fetch": "2.6.7" } }, "sha512-Sxn74oHiZSU6+cVeZy62iPZMFMvKp4jupMFHelSICCMw1qELmUHPvuZSr+ZHDmNGgHcEpzJM5HN02kR7T4g+PQ=="], + "@astrojs/vercel": ["@astrojs/vercel@11.0.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.1", "@vercel/analytics": "^1.6.1", "@vercel/functions": "^3.4.3", "@vercel/nft": "^1.3.2", "@vercel/routing-utils": "^5.3.3", "esbuild": "^0.28.0", "tinyglobby": "^0.2.15" }, "peerDependencies": { "astro": "^7.0.0" } }, "sha512-UMhcZ/lWB0/B2l1L8BJh6QxysjZt8SLS9e40xRfBdVhfi3Y6SIDw+99rY/+OGW/yem/S7sBRkvqvto8neevO2A=="], - "@asyncapi/specs": ["@asyncapi/specs@6.8.1", "", { "dependencies": { "@types/json-schema": "^7.0.11" } }, "sha512-czHoAk3PeXTLR+X8IUaD+IpT+g+zUvkcgMDJVothBsan+oHN3jfcFcFUNdOPAAFoUCQN1hXF1dWuphWy05THlA=="], + "@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.4", "", { "dependencies": { "yaml": "^2.8.3" } }, "sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A=="], "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], @@ -1478,14 +1482,34 @@ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], + + "@bruits/satteri-darwin-arm64": ["@bruits/satteri-darwin-arm64@0.9.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iw4nZgx9v30lWo/MTngQqi1pI78KI0DnkSm+lVJGYdmPLgAyDNJigVhpG42/Iq55A6c1Ll8q66ljyyRiQUxwow=="], + + "@bruits/satteri-darwin-x64": ["@bruits/satteri-darwin-x64@0.9.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-6T26Z5Kf3cFW2PSlk9p7zT7yVxvuBSiJvYyz9u8KjYwMTqZyIDOj2wDyNpxKV4+6yUVG7rddq2QwvG/8LJA2+Q=="], + + "@bruits/satteri-linux-arm64-gnu": ["@bruits/satteri-linux-arm64-gnu@0.9.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-u51id17uJwNEMK9nBlICsq6U31c+XVqQueVBkwRIzZG+gMpS8TOJctt5h5Wz33Z8xnMdTd+adtACVz0yHgGuOA=="], + + "@bruits/satteri-linux-arm64-musl": ["@bruits/satteri-linux-arm64-musl@0.9.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-v39HxiwGC5Rqm01HksP6+5Y+xKLPlsuVFgIgpEAo+SiQ22c+mJVhS3u7Z6ePAKdhL5NJoK1xq70kLz3L13AhpQ=="], + + "@bruits/satteri-linux-x64-gnu": ["@bruits/satteri-linux-x64-gnu@0.9.5", "", { "os": "linux", "cpu": "x64" }, "sha512-F3uO8uFp3pAP5ZGXttwvh57GS7s0lL953tnNdyI2gRyP4kOOkp6pyGojNJzCjkDvWI2Cvb9iNrKok3aqQPauAw=="], + + "@bruits/satteri-linux-x64-musl": ["@bruits/satteri-linux-x64-musl@0.9.5", "", { "os": "linux", "cpu": "x64" }, "sha512-bicEqglLlz++mWyADaZoP0JY20s4vDfLjaPYgQqC+NI4zZLTOOg1T4GB8aqtc822Pqji8SQBmSrTb7CrP8i08Q=="], + + "@bruits/satteri-wasm32-wasi": ["@bruits/satteri-wasm32-wasi@0.9.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-zauAuMwfPnKPUkd4AFixRFpXdgKwP2mKgxrIIo2gJzW0/ZneF9dbHnLkojSpaBnCCp7VUL1hIi5WWZvB1CqmAQ=="], + + "@bruits/satteri-win32-arm64-msvc": ["@bruits/satteri-win32-arm64-msvc@0.9.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-SrfE7NEsgZjBvU3c+RR6oQRu0ToXY5uVJEbieXEF0YTctIV2zAVlbaMjWLts074QCgh3a+XHWkR/lWh2VH2LUg=="], + + "@bruits/satteri-win32-x64-msvc": ["@bruits/satteri-win32-x64-msvc@0.9.5", "", { "os": "win32", "cpu": "x64" }, "sha512-5Kw9ZAtTGS8WHizyn+CJhjjfIQrw+7jcZodpmpXJjefnO15M8UexIi6JR2E5thyvsmHyhL6ZDDMUNR4bKJPd4g=="], + "@bufbuild/protobuf": ["@bufbuild/protobuf@2.12.1", "", {}, "sha512-BvAMfS6LrgZiryOAZ4pBYucu4wG/Ei/9o9DZ9akbREnMLbPJiom2i8b9C8IsKErQoiKqVhrerzt3kOT/RrzLHg=="], "@bufbuild/protoplugin": ["@bufbuild/protoplugin@2.12.1", "", { "dependencies": { "@bufbuild/protobuf": "2.12.1", "@typescript/vfs": "^1.6.2", "typescript": "5.4.5" } }, "sha512-PY58KxQVAD1BnnKtStOctsMoegEVGfBnY5AOqVQOIu711nA13oYtTqJM8df5lUQg2J1DR3XxUXptE+fWX5oLdA=="], - "@canvas/image-data": ["@canvas/image-data@1.1.0", "", {}, "sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA=="], - "@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="], + "@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="], + "@clack/core": ["@clack/core@1.0.0-alpha.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rFbCU83JnN7l3W1nfgCqqme4ZZvTTgsiKQ6FM0l+r0P+o2eJpExcocBUWUIwnDzL76Aca9VhUdWmB2MbUv+Qyg=="], "@clack/prompts": ["@clack/prompts@1.0.0-alpha.1", "", { "dependencies": { "@clack/core": "1.0.0-alpha.1", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-07MNT0OsxjKOcyVfX8KhXBhJiyUbDP1vuIAcHc+nx5v93MJO23pX3X/k3bWz6T3rpM9dgWPq90i4Jq7gZAyMbw=="], @@ -1686,10 +1710,6 @@ "@fontsource/noto-sans-symbols-2": ["@fontsource/noto-sans-symbols-2@5.2.5", "", {}, "sha512-F4O9WLifwoZS1quNzY1ebjMNo2cQPe/UP68Dmud0ONi2lOxaR6xp6fFPO2gG17MI7DwAnfMyQFl64A2tAd28hg=="], - "@fuma-translate/react": ["@fuma-translate/react@1.0.2", "", { "peerDependencies": { "@types/react": "*", "react": "^19.2.0", "react-dom": "^19.2.0" }, "optionalPeers": ["@types/react"] }, "sha512-uOiOtBx3nRXR8Nu1GzBf1tApgF1FErDBTHxRIAQeyQdyOoZbrNRN6H4kDCWObY4qyGeGbHydG0DHzgeUgFDMIw=="], - - "@fumadocs/tailwind": ["@fumadocs/tailwind@0.1.0", "", { "peerDependencies": { "tailwindcss": "^4.0.0" }, "optionalPeers": ["tailwindcss"] }, "sha512-nF/DCAwOR21HZ4AkjIOv3Iqwyqywzb6pdyeMcoa+aZzirXj5ntvNZbe3jJ0v3ehhtrRfYYeXBezvjn8ZmV+fuQ=="], - "@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="], "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], @@ -1712,6 +1732,12 @@ "@ibm/telemetry-js": ["@ibm/telemetry-js@1.11.0", "", { "bin": { "ibmtelemetry": "dist/collect.js" } }, "sha512-RO/9j+URJnSfseWg9ZkEX9p+a3Ousd33DBU7rOafoZB08RqdzxFVYJ2/iM50dkBuD0o7WX7GYt1sLbNgCoE+pA=="], + "@iconify-json/lucide": ["@iconify-json/lucide@1.2.118", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-JBnK4YOq6K/lA0JP//27QxFxJ4120TjvfXAzGZZIGjCcXcRRRFxl1rcV7+IWdcVCe90KXdqVaAwLaLf6G3HELw=="], + + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + + "@iconify/utils": ["@iconify/utils@3.1.4", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw=="], + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], @@ -1762,38 +1788,6 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], - "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="], - - "@inquirer/checkbox": ["@inquirer/checkbox@4.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA=="], - - "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="], - - "@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="], - - "@inquirer/editor": ["@inquirer/editor@4.2.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/external-editor": "^1.0.3", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ=="], - - "@inquirer/expand": ["@inquirer/expand@4.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew=="], - - "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], - - "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], - - "@inquirer/input": ["@inquirer/input@4.3.1", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g=="], - - "@inquirer/number": ["@inquirer/number@3.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg=="], - - "@inquirer/password": ["@inquirer/password@4.0.23", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA=="], - - "@inquirer/prompts": ["@inquirer/prompts@7.9.0", "", { "dependencies": { "@inquirer/checkbox": "^4.3.0", "@inquirer/confirm": "^5.1.19", "@inquirer/editor": "^4.2.21", "@inquirer/expand": "^4.0.21", "@inquirer/input": "^4.2.5", "@inquirer/number": "^3.0.21", "@inquirer/password": "^4.0.21", "@inquirer/rawlist": "^4.1.9", "@inquirer/search": "^3.2.0", "@inquirer/select": "^4.4.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A=="], - - "@inquirer/rawlist": ["@inquirer/rawlist@4.1.11", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw=="], - - "@inquirer/search": ["@inquirer/search@3.2.2", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA=="], - - "@inquirer/select": ["@inquirer/select@4.4.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w=="], - - "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="], - "@internationalized/date": ["@internationalized/date@3.12.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw=="], "@internationalized/number": ["@internationalized/number@3.6.7", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg=="], @@ -1822,12 +1816,6 @@ "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], - "@jsep-plugin/assignment": ["@jsep-plugin/assignment@1.3.0", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ=="], - - "@jsep-plugin/regex": ["@jsep-plugin/regex@1.0.4", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg=="], - - "@jsep-plugin/ternary": ["@jsep-plugin/ternary@1.1.4", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg=="], - "@jsx-email/all": ["@jsx-email/all@2.2.3", "", { "dependencies": { "@jsx-email/body": "1.0.2", "@jsx-email/button": "1.0.4", "@jsx-email/column": "1.0.3", "@jsx-email/container": "1.0.2", "@jsx-email/font": "1.0.3", "@jsx-email/head": "1.0.2", "@jsx-email/heading": "1.0.2", "@jsx-email/hr": "1.0.2", "@jsx-email/html": "1.0.2", "@jsx-email/img": "1.0.2", "@jsx-email/link": "1.0.2", "@jsx-email/markdown": "2.0.4", "@jsx-email/preview": "1.0.2", "@jsx-email/render": "1.1.1", "@jsx-email/row": "1.0.2", "@jsx-email/section": "1.0.2", "@jsx-email/tailwind": "2.4.4", "@jsx-email/text": "1.0.2" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-OBvLe/hVSQc0LlMSTJnkjFoqs3bmxcC4zpy/5pT5agPCSKMvAKQjzmsc2xJ2wO73jSpRV1K/g38GmvdCfrhSoQ=="], "@jsx-email/body": ["@jsx-email/body@1.0.2", "", { "peerDependencies": { "react": "^18.2.0" } }, "sha512-NjR2tgLH4XGfGkm+O8kcVwi9MBqZsXZCLlmk3HlMux3/n/+a5zB+yhJqXWZBJl2i+6cSF+E2O6hK11ekyK9WWQ=="], @@ -1876,8 +1864,6 @@ "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], - "@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="], - "@lydell/node-pty": ["@lydell/node-pty@1.2.0-beta.12", "", { "optionalDependencies": { "@lydell/node-pty-darwin-arm64": "1.2.0-beta.12", "@lydell/node-pty-darwin-x64": "1.2.0-beta.12", "@lydell/node-pty-linux-arm64": "1.2.0-beta.12", "@lydell/node-pty-linux-x64": "1.2.0-beta.12", "@lydell/node-pty-win32-arm64": "1.2.0-beta.12", "@lydell/node-pty-win32-x64": "1.2.0-beta.12" } }, "sha512-qIK890UwPupoj07osVvgOIa++1mxeHbcGry4PKRHhNVNs81V2SCG34eJr46GybiOmBtc8Sj5PB1/GGM5PL549g=="], "@lydell/node-pty-darwin-arm64": ["@lydell/node-pty-darwin-arm64@1.2.0-beta.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tqaifcY9Cr41SblO1+FLzh8oxxtkNhuW9Dhl22lKme9BreYvKvxEZcdPIXTuqkJc5tagOEC4QHShKmJjLyLXLQ=="], @@ -1896,29 +1882,13 @@ "@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="], + "@mapbox/node-pre-gyp": ["@mapbox/node-pre-gyp@2.0.3", "", { "dependencies": { "consola": "^3.2.3", "detect-libc": "^2.0.0", "https-proxy-agent": "^7.0.5", "node-fetch": "^2.6.7", "nopt": "^8.0.0", "semver": "^7.5.3", "tar": "^7.4.0" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg=="], + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], - "@mintlify/cli": ["@mintlify/cli@4.0.1269", "", { "dependencies": { "@inquirer/prompts": "7.9.0", "@mintlify/common": "1.0.985", "@mintlify/link-rot": "3.0.1172", "@mintlify/models": "0.0.333", "@mintlify/prebuild": "1.0.1131", "@mintlify/previewing": "4.0.1197", "@mintlify/validation": "0.1.769", "adm-zip": "0.5.16", "chalk": "5.2.0", "color": "4.2.3", "detect-port": "1.5.1", "front-matter": "4.0.2", "fs-extra": "11.2.0", "ink": "6.3.0", "inquirer": "12.3.0", "js-yaml": "4.1.1", "mdast-util-mdx-jsx": "3.2.0", "open": "8.4.2", "openid-client": "6.8.2", "posthog-node": "5.17.2", "react": "19.2.3", "semver": "7.7.2", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "4.3.6" }, "optionalDependencies": { "keytar": "7.9.0" }, "bin": { "mint": "bin/index.js", "mintlify": "bin/index.js" } }, "sha512-l9b7InT55JWXV7TU7Jr4Wrijv4/gMFHLQyWJ7fcjqpSxAetR+xNyeEARRlcf7OicGTjZuvmRGGfj75kp3O4p7A=="], - - "@mintlify/common": ["@mintlify/common@1.0.985", "", { "dependencies": { "@asyncapi/parser": "3.4.0", "@asyncapi/specs": "6.8.1", "@mintlify/mdx": "3.0.4", "@mintlify/models": "0.0.333", "@mintlify/openapi-parser": "0.0.8", "@mintlify/validation": "0.1.769", "@sindresorhus/slugify": "2.2.0", "@types/mdast": "4.0.4", "acorn": "8.11.2", "acorn-jsx": "5.3.2", "color-blend": "4.0.0", "estree-util-to-js": "2.0.0", "estree-walker": "3.0.3", "front-matter": "4.0.2", "hast-util-from-html": "2.0.3", "hast-util-to-html": "9.0.4", "hast-util-to-text": "4.0.2", "hex-rgb": "5.0.0", "ignore": "7.0.5", "js-yaml": "4.1.1", "lodash": "4.18.1", "mdast-util-from-markdown": "2.0.2", "mdast-util-gfm": "3.0.0", "mdast-util-mdx": "3.0.0", "mdast-util-mdx-jsx": "3.1.3", "micromark-extension-gfm": "3.0.0", "micromark-extension-mdx-jsx": "3.0.1", "micromark-extension-mdxjs": "3.0.0", "openapi-types": "12.1.3", "postcss": "8.5.14", "rehype-stringify": "10.0.1", "remark": "15.0.1", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.0", "remark-math": "6.0.0", "remark-mdx": "3.1.0", "remark-parse": "11.0.0", "remark-rehype": "11.1.1", "remark-stringify": "11.0.0", "sucrase": "3.34.0", "tailwindcss": "3.4.17", "unified": "11.0.5", "unist-builder": "4.0.0", "unist-util-map": "4.0.0", "unist-util-remove": "4.0.0", "unist-util-remove-position": "5.0.0", "unist-util-visit": "5.0.0", "unist-util-visit-parents": "6.0.1", "vfile": "6.0.3", "xss": "1.0.15" } }, "sha512-eJPeR99AKgVifXLdiA2hhfNy2+CmZ3zqQAscRXmFbJFST8SgsUj6rU3D2fx0XYt38b7T3LqEMD7DH5ipSC+Zfg=="], - - "@mintlify/link-rot": ["@mintlify/link-rot@3.0.1172", "", { "dependencies": { "@mintlify/common": "1.0.985", "@mintlify/models": "0.0.333", "@mintlify/prebuild": "1.0.1131", "@mintlify/previewing": "4.0.1197", "@mintlify/scraping": "4.0.849", "@mintlify/validation": "0.1.769", "fs-extra": "11.1.0", "unist-util-visit": "4.1.2" } }, "sha512-8962sk/WO/0YcSkHTiaVZ2DvrCb8TC4BPgp21a9qW6nsPKKNixMhsC2uUB90D+gOzPTejJl0NNd4gLk4fA3SKw=="], - - "@mintlify/mdx": ["@mintlify/mdx@3.0.4", "", { "dependencies": { "@shikijs/transformers": "^3.11.0", "@shikijs/twoslash": "^3.12.2", "arktype": "^2.1.26", "hast-util-to-string": "^3.0.1", "mdast-util-from-markdown": "^2.0.2", "mdast-util-gfm": "^3.1.0", "mdast-util-mdx-jsx": "^3.2.0", "mdast-util-to-hast": "^13.2.0", "next-mdx-remote-client": "^1.0.3", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "remark-smartypants": "^3.0.2", "shiki": "^3.11.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@radix-ui/react-popover": "^1.1.15", "react": "^18.3.1", "react-dom": "^18.3.1" } }, "sha512-tJhdpnM5ReJLNJ2fuDRIEr0zgVd6id7/oAIfs26V46QlygiLsc8qx4Rz3LWIX51rUXW/cfakjj0EATxIciIw+g=="], - - "@mintlify/models": ["@mintlify/models@0.0.333", "", { "dependencies": { "axios": "1.16.1", "openapi-types": "12.1.3" } }, "sha512-0uAsuTsV8gYCDpv4aA0MWilQu+a/mrK+G6q5FVcgunbEZ3meeCXfrQFTviaEw7A+0cR/7Pc2KLA66cPDm+3Qdg=="], - - "@mintlify/openapi-parser": ["@mintlify/openapi-parser@0.0.8", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-9MBRq9lS4l4HITYCrqCL7T61MOb20q9IdU7HWhqYMNMM1jGO1nHjXasFy61yZ8V6gMZyyKQARGVoZ0ZrYN48Og=="], - - "@mintlify/prebuild": ["@mintlify/prebuild@1.0.1131", "", { "dependencies": { "@mintlify/common": "1.0.985", "@mintlify/openapi-parser": "0.0.8", "@mintlify/scraping": "4.0.849", "@mintlify/validation": "0.1.769", "chalk": "5.3.0", "favicons": "7.2.0", "front-matter": "4.0.2", "fs-extra": "11.1.0", "js-yaml": "4.1.1", "openapi-types": "12.1.3", "sharp": "0.33.5", "sharp-ico": "0.1.5", "unist-util-visit": "4.1.2", "uuid": "11.1.1" } }, "sha512-EbPf1/z1m8K/Jl4qXggiMQwfdqXLF25sj+d5SHaGl6T0auTOYMayN578Rb/qM4fjhhlBQwub919Zsq9syYeYUA=="], - - "@mintlify/previewing": ["@mintlify/previewing@4.0.1197", "", { "dependencies": { "@mintlify/common": "1.0.985", "@mintlify/prebuild": "1.0.1131", "@mintlify/validation": "0.1.769", "adm-zip": "0.5.16", "better-opn": "3.0.2", "chalk": "5.2.0", "chokidar": "3.5.3", "express": "4.22.0", "front-matter": "4.0.2", "fs-extra": "11.1.0", "got": "13.0.0", "ink": "6.3.0", "ink-spinner": "5.0.0", "is-online": "10.0.0", "js-yaml": "4.1.1", "openapi-types": "12.1.3", "react": "19.2.3", "socket.io": "4.8.0", "tar": "7.5.15", "unist-util-visit": "4.1.2", "yargs": "17.7.1" } }, "sha512-q4TunK8KjE1k9ve5nOAQTvBpsE7bX+RJN/ozx7RdIyQSCSexpkywmSM6nE3ECJyjUWFy8pg6yrYYbUQLHN/oww=="], - - "@mintlify/scraping": ["@mintlify/scraping@4.0.849", "", { "dependencies": { "@mintlify/common": "1.0.985", "@mintlify/openapi-parser": "0.0.8", "fs-extra": "11.1.1", "hast-util-to-mdast": "10.1.0", "js-yaml": "4.1.1", "mdast-util-mdx-jsx": "3.1.3", "neotraverse": "0.6.18", "puppeteer": "22.14.0", "rehype-parse": "9.0.1", "remark-gfm": "4.0.0", "remark-mdx": "3.0.1", "remark-parse": "11.0.0", "remark-stringify": "11.0.0", "unified": "11.0.5", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "3.24.0" }, "bin": { "mintlify-scrape": "bin/cli.js" } }, "sha512-4aMltLtfSU5rkUJt2SCVaYIbuggiEnx7lMWKM8NW93SaZUeoL0iKNbo0K24SmOqS7kYATRRH8zI7gefPX2ZLDw=="], - - "@mintlify/validation": ["@mintlify/validation@0.1.769", "", { "dependencies": { "@mintlify/mdx": "3.0.4", "@mintlify/models": "0.0.333", "arktype": "2.1.27", "fractional-indexing": "3.2.0", "js-yaml": "4.1.1", "lcm": "0.0.3", "lodash": "4.18.1", "neotraverse": "0.6.18", "object-hash": "3.0.0", "openapi-types": "12.1.3", "uuid": "11.1.1", "zod": "3.24.0", "zod-to-json-schema": "3.20.4" } }, "sha512-8Sg6DCdQ7RSc3NIyaSSWy7G6KaAuw0NfUSBCYLwGhtp6dYPh1jEsPn6YU8hqUMNZn1OQHsA52rA6lZQxjnyLHQ=="], + "@mermaid-js/parser": ["@mermaid-js/parser@1.2.0", "", { "dependencies": { "@chevrotain/types": "~11.1.2" } }, "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA=="], "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], @@ -2056,16 +2026,6 @@ "@one-ini/wasm": ["@one-ini/wasm@0.1.1", "", {}, "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="], - "@oozcitak/dom": ["@oozcitak/dom@2.0.2", "", { "dependencies": { "@oozcitak/infra": "^2.0.2", "@oozcitak/url": "^3.0.0", "@oozcitak/util": "^10.0.0" } }, "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w=="], - - "@oozcitak/infra": ["@oozcitak/infra@2.0.2", "", { "dependencies": { "@oozcitak/util": "^10.0.0" } }, "sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA=="], - - "@oozcitak/url": ["@oozcitak/url@3.0.0", "", { "dependencies": { "@oozcitak/infra": "^2.0.2", "@oozcitak/util": "^10.0.0" } }, "sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ=="], - - "@oozcitak/util": ["@oozcitak/util@10.0.0", "", {}, "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA=="], - - "@openapi-contrib/openapi-schema-to-json-schema": ["@openapi-contrib/openapi-schema-to-json-schema@3.2.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" } }, "sha512-Gj6C0JwCr8arj0sYuslWXUBSP/KnUlEGnPW4qxlXvAl543oaNQgMgIgkQUA6vs5BCCvwTEiL8m/wdWzfl4UvSw=="], - "@openauthjs/openauth": ["@openauthjs/openauth@0.0.0-20250322224806", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-p5IWSRXvABcwocH2dNI0w8c1QJelIOFulwhKk+aLLFfUbs8u1pr7kQbYe8yCSM2+bcLHiwbogpUQc2ovrGwCuw=="], "@opencode-ai/ai": ["@opencode-ai/ai@workspace:packages/ai"], @@ -2094,8 +2054,6 @@ "@opencode-ai/desktop": ["@opencode-ai/desktop@workspace:packages/desktop"], - "@opencode-ai/docs": ["@opencode-ai/docs@workspace:packages/docs"], - "@opencode-ai/effect-drizzle-sqlite": ["@opencode-ai/effect-drizzle-sqlite@workspace:packages/effect-drizzle-sqlite"], "@opencode-ai/effect-sqlite-node": ["@opencode-ai/effect-sqlite-node@workspace:packages/effect-sqlite-node"], @@ -2472,8 +2430,6 @@ "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], - "@posthog/core": ["@posthog/core@1.7.1", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-kjK0eFMIpKo9GXIbts8VtAknsoZ18oZorANdtuTj1CbgS28t4ZVq//HAWhnxEuXRTrtkd+SUJ6Ux3j2Af8NCuA=="], - "@preact/signals-core": ["@preact/signals-core@1.14.4", "", {}, "sha512-HNB6HYeYKhQbJ1aKl+YRjrS4+QWHLKX6qKoUsfS/m0vqzsVaEBiZiaKbG/e+NKk2ch5ALQr/ihWaMHxiCuuWHA=="], "@protobuf-ts/plugin": ["@protobuf-ts/plugin@2.11.1", "", { "dependencies": { "@bufbuild/protobuf": "^2.4.0", "@bufbuild/protoplugin": "^2.4.0", "@protobuf-ts/protoc": "^2.11.1", "@protobuf-ts/runtime": "^2.11.1", "@protobuf-ts/runtime-rpc": "^2.11.1", "typescript": "^3.9" }, "bin": { "protoc-gen-ts": "bin/protoc-gen-ts", "protoc-gen-dump": "bin/protoc-gen-dump" } }, "sha512-HyuprDcw0bEEJqkOWe1rnXUP0gwYLij8YhPuZyZk6cJbIgc/Q0IFgoHQxOXNIXAcXM4Sbehh6kjVnCzasElw1A=="], @@ -2502,29 +2458,21 @@ "@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="], - "@puppeteer/browsers": ["@puppeteer/browsers@2.3.0", "", { "dependencies": { "debug": "^4.3.5", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.4.0", "semver": "^7.6.3", "tar-fs": "^3.0.6", "unbzip2-stream": "^1.4.3", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA=="], - "@radix-ui/colors": ["@radix-ui/colors@1.0.1", "", {}, "sha512-xySw8f0ZVsAEP+e7iLl3EvcBXX7gsIlC1Zso/sPBW9gIWerBTgz6axrjU+MZ39wD+WFi5h5zdWpsg3+hwt2Qsg=="], - "@radix-ui/number": ["@radix-ui/number@1.1.2", "", {}, "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig=="], - "@radix-ui/primitive": ["@radix-ui/primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw=="], - "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-collapsible": "1.1.16", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-BpZJNmetujnGgUI6OX0jEhEmlA46WPqgub8Rv09Kyquwd0cc1ndMKpiPYCjmBU6KSSRPAMtgLpEoZSG/tdNIWQ=="], - "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA=="], "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-id": "1.0.1", "@radix-ui/react-presence": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-use-controllable-state": "1.0.1", "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg=="], - "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.12", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-nb67INpE0IahJKN7EYPp9m9YGwYeKlnzxT3MwXVkgCskaSJia97kG4T0ywpjNUSSnoJk/uvk12V8vbrEHEj+/Q=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-slot": "1.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw=="], "@radix-ui/react-context": ["@radix-ui/react-context@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg=="], - "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.12", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-portal": "1.1.13", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+HhbN2+YtkRgVirjZ2afMeutQRuGOrdkWR5+EFC58SJojGmtyNQwYzgi6tHBpOxvFHefMtPeHdgtjz0BOGxFQg=="], - - "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA=="], + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA=="], "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.0.4", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-use-callback-ref": "1.0.1", "@radix-ui/react-use-escape-keydown": "1.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg=="], @@ -2534,46 +2482,34 @@ "@radix-ui/react-id": ["@radix-ui/react-id@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ=="], - "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.18", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-visually-hidden": "1.2.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-K9HiuxZ6xCwSaHcIuUpxyhy4w5gpwzWjh9dHTSbMN3Ix4qAyVObS9RlU3zMycb0PO3v9Tpk0BXMwWvXOUbVXew=="], - "@radix-ui/react-popover": ["@radix-ui/react-popover@1.0.6", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-dismissable-layer": "1.0.4", "@radix-ui/react-focus-guards": "1.0.1", "@radix-ui/react-focus-scope": "1.0.3", "@radix-ui/react-id": "1.0.1", "@radix-ui/react-popper": "1.1.2", "@radix-ui/react-portal": "1.0.3", "@radix-ui/react-presence": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-slot": "1.0.2", "@radix-ui/react-use-controllable-state": "1.0.1", "aria-hidden": "^1.1.1", "react-remove-scroll": "2.5.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-cZ4defGpkZ0qTRtlIBzJLSzL6ht7ofhhW4i1+pkemjV1IKXm0wgCRnee154qlV6r9Ttunmh2TNZhMfV2bavUyA=="], "@radix-ui/react-popper": ["@radix-ui/react-popper@1.1.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.0.3", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-use-callback-ref": "1.0.1", "@radix-ui/react-use-layout-effect": "1.0.1", "@radix-ui/react-use-rect": "1.0.1", "@radix-ui/react-use-size": "1.0.1", "@radix-ui/rect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg=="], "@radix-ui/react-portal": ["@radix-ui/react-portal@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA=="], - "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.7", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zBZ4QM5XG3JRanDmqXYf3MD6th4AFXFmgU6KNMFzUaV6F3uw9I5/zjMUvFriSEn5ewo1nxuibvyxJdmLlDcslA=="], + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg=="], "@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g=="], "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.0.4", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-collection": "1.0.3", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-direction": "1.0.1", "@radix-ui/react-id": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-use-callback-ref": "1.0.1", "@radix-ui/react-use-controllable-state": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ=="], - "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.14", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bBODCWZK7JTbQLHs0uIP4f73wIWatakK4OS33UzkR1x897wu0PuO658a3f+6P2GEGyDzGYMuHRatMVoAk9WZTw=="], - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg=="], - "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-roving-focus": "1.1.15", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-nRyXnrAVCwjeXcHbvEbLS6ndbTeKHG1RqCP4A8Gw5L4cemDzPXdD8rAmr6wet0v57R69wGvuIIsFjHSVkZiMzQ=="], - "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-use-controllable-state": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg=="], "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.0.4", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-direction": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-roving-focus": "1.0.4", "@radix-ui/react-toggle": "1.0.3", "@radix-ui/react-use-controllable-state": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A=="], "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.0.6", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-dismissable-layer": "1.0.4", "@radix-ui/react-id": "1.0.1", "@radix-ui/react-popper": "1.1.2", "@radix-ui/react-portal": "1.0.3", "@radix-ui/react-presence": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-slot": "1.0.2", "@radix-ui/react-use-controllable-state": "1.0.1", "@radix-ui/react-visually-hidden": "1.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-DmNFOiwEc2UDigsYj6clJENma58OelxD24O4IODoZ+3sQc3Zb+L8w1EP+y9laTuKCLAysPw4fD6/v0j4KNV8rg=="], - "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="], "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA=="], - "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.3", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA=="], - "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg=="], - "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A=="], - "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ=="], - "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw=="], - "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/rect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw=="], "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g=="], @@ -2670,7 +2606,25 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="], - "@scarf/scarf": ["@scarf/scarf@1.4.0", "", {}, "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="], + "@scalar/astro": ["@scalar/astro@0.4.11", "", { "dependencies": { "@scalar/client-side-rendering": "0.3.4" }, "peerDependencies": { "astro": "^4.0.0 || ^5.0.0" } }, "sha512-iZHZrLb0bx/s9efUkn5JKdZeyawODmfnoJl5hTu1uJxUwjKtAVtx8MFVXo8JkyC3/s8XFXGGs66d+S/Ji8eYJg=="], + + "@scalar/client-side-rendering": ["@scalar/client-side-rendering@0.3.4", "", { "dependencies": { "@scalar/schemas": "0.7.4", "@scalar/types": "0.16.4", "@scalar/validation": "0.6.2" } }, "sha512-kb3B+FGjvAUr2DU0fe9dVKBLwot1TjOO+iHCTmY8r8FUJySmYKGQYCicRzzilOyTjvKspiZBCEr03PWaWW+1gw=="], + + "@scalar/helpers": ["@scalar/helpers@0.9.2", "", {}, "sha512-hjyMpMZjTBZQhyByZmz5oUgRKUQJO5V5AOiJxsVEGbUmgA7sJRQeTrXLB+BEwzaKS5nm2opJeNyMBYLFNK4hiQ=="], + + "@scalar/json-magic": ["@scalar/json-magic@0.12.19", "", { "dependencies": { "@scalar/helpers": "0.9.2", "pathe": "^2.0.3", "yaml": "^2.8.3" } }, "sha512-1T4QoFYZ1nKt25xFeHtghAuZzaLq2X4CpCSLFXG0Fjcz6K2HIZqo+RtywfI0WD8RRRRgS34keo7X4Gv1BQUNoQ=="], + + "@scalar/openapi-parser": ["@scalar/openapi-parser@0.28.10", "", { "dependencies": { "@scalar/helpers": "0.9.2", "@scalar/json-magic": "0.12.19", "@scalar/openapi-types": "0.9.3", "@scalar/openapi-upgrader": "0.2.11", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.8.3" } }, "sha512-jn3ftvtNTcWOgxf7XVn9CvJGMjFS7QU1b6FiGmibzAlR/6pOP3Ei7sBBnfIY4PBwHjHgMAGBoGApfOV8h75gPQ=="], + + "@scalar/openapi-types": ["@scalar/openapi-types@0.9.3", "", {}, "sha512-34qglt5jSo55iZfH9i7EhjQCdE0Po2xZeh8wytQKolSnXrxsYMSyFDJEBxz1Gaew4on9N3XIWsd0QpwKVA5CSA=="], + + "@scalar/openapi-upgrader": ["@scalar/openapi-upgrader@0.2.11", "", { "dependencies": { "@scalar/openapi-types": "0.9.3" } }, "sha512-eYEFBO8mZfgXEO/hv8rdL5OA4oOB8orFT5kXNK4I/x9xca2D7A4BteuFXqRaw9lE1CIjtG+StlAUYVz5omTXew=="], + + "@scalar/schemas": ["@scalar/schemas@0.7.4", "", { "dependencies": { "@scalar/helpers": "0.9.2", "@scalar/validation": "0.6.2" } }, "sha512-Or31zxR+ceGGhkVU5XBO2Zv7oyGDtjsJOjM2Rr0WRZ1/tRsQc2/FsVv+9kPmCA7sqFL8BKWlNfTXcPITI7WRHw=="], + + "@scalar/types": ["@scalar/types@0.16.4", "", { "dependencies": { "@scalar/helpers": "0.9.2", "nanoid": "^5.1.6", "type-fest": "^5.3.1", "zod": "^4.3.5" } }, "sha512-fLf0ANAC3iQq0sIVdmH6aGM+pFSLyD8GfGBxSWcLpI0jE0iFAzX2A3p0qVZm7vqBT4TQnn0fSwg5U+h+FZ52BA=="], + + "@scalar/validation": ["@scalar/validation@0.6.2", "", {}, "sha512-Sc1TkcwGV6aVCO51AyKeaGiP8gpwAHxEtO5d3tZzPV+KsnlC/YokQxFxwBrbIXw73k9hmcExnJyGu3k5i6n6VA=="], "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], @@ -2728,7 +2682,7 @@ "@shikijs/transformers": ["@shikijs/transformers@3.9.2", "", { "dependencies": { "@shikijs/core": "3.9.2", "@shikijs/types": "3.9.2" } }, "sha512-MW5hT4TyUp6bNAgTExRYLk1NNasVQMTCw1kgbxHcEC0O5cbepPWaB+1k+JzW9r3SP2/R8kiens8/3E6hGKfgsA=="], - "@shikijs/twoslash": ["@shikijs/twoslash@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0", "twoslash": "^0.3.6" }, "peerDependencies": { "typescript": ">=5.5.0" } }, "sha512-pNaLJWMA3LU7PhT8tm9OQBZ1epy0jmdgeJzntBtr1EVXLbHxGzTj3mnf9vOdcl84l96qnlJXkJ/NGXZYBpXl5g=="], + "@shikijs/twoslash": ["@shikijs/twoslash@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/types": "4.3.1", "twoslash": "^0.3.9" }, "peerDependencies": { "typescript": ">=5.5.0" } }, "sha512-xK8inH/gK++1V4rTxrwCwjvaNwkkJ7oDjOIpdqONVxIpAFnVC3gzqjH5KiXGTelUcxpUJ3PtOKWct1YQ0kAloA=="], "@shikijs/types": ["@shikijs/types@3.9.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw=="], @@ -2748,11 +2702,7 @@ "@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="], - "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], - - "@sindresorhus/slugify": ["@sindresorhus/slugify@2.2.0", "", { "dependencies": { "@sindresorhus/transliterate": "^1.0.0", "escape-string-regexp": "^5.0.0" } }, "sha512-9Vybc/qX8Kj6pxJaapjkFbiUJPk7MAkCh/GFCxIBnnsuYCFPIXKvnLidG8xlepht3i24L5XemUmGtrJ3UWrl6w=="], - - "@sindresorhus/transliterate": ["@sindresorhus/transliterate@1.6.0", "", { "dependencies": { "escape-string-regexp": "^5.0.0" } }, "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ=="], + "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], "@slack/bolt": ["@slack/bolt@3.22.0", "", { "dependencies": { "@slack/logger": "^4.0.0", "@slack/oauth": "^2.6.3", "@slack/socket-mode": "^1.3.6", "@slack/types": "^2.13.0", "@slack/web-api": "^6.13.0", "@types/express": "^4.16.1", "@types/promise.allsettled": "^1.0.3", "@types/tsscmp": "^1.0.0", "axios": "^1.7.4", "express": "^4.21.0", "path-to-regexp": "^8.1.0", "promise.allsettled": "^1.0.2", "raw-body": "^2.3.3", "tsscmp": "^1.0.6" } }, "sha512-iKDqGPEJDnrVwxSVlFW6OKTkijd7s4qLBeSufoBsTM0reTyfdp/5izIQVkxNfzjHi3o6qjdYbRXkYad5HBsBog=="], @@ -2910,36 +2860,6 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@stoplight/better-ajv-errors": ["@stoplight/better-ajv-errors@1.0.3", "", { "dependencies": { "jsonpointer": "^5.0.0", "leven": "^3.1.0" }, "peerDependencies": { "ajv": ">=8" } }, "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA=="], - - "@stoplight/json": ["@stoplight/json@3.21.0", "", { "dependencies": { "@stoplight/ordered-object-literal": "^1.0.3", "@stoplight/path": "^1.3.2", "@stoplight/types": "^13.6.0", "jsonc-parser": "~2.2.1", "lodash": "^4.17.21", "safe-stable-stringify": "^1.1" } }, "sha512-5O0apqJ/t4sIevXCO3SBN9AHCEKKR/Zb4gaj7wYe5863jme9g02Q0n/GhM7ZCALkL+vGPTe4ZzTETP8TFtsw3g=="], - - "@stoplight/json-ref-readers": ["@stoplight/json-ref-readers@1.2.2", "", { "dependencies": { "node-fetch": "^2.6.0", "tslib": "^1.14.1" } }, "sha512-nty0tHUq2f1IKuFYsLM4CXLZGHdMn+X/IwEUIpeSOXt0QjMUbL0Em57iJUDzz+2MkWG83smIigNZ3fauGjqgdQ=="], - - "@stoplight/json-ref-resolver": ["@stoplight/json-ref-resolver@3.1.6", "", { "dependencies": { "@stoplight/json": "^3.21.0", "@stoplight/path": "^1.3.2", "@stoplight/types": "^12.3.0 || ^13.0.0", "@types/urijs": "^1.19.19", "dependency-graph": "~0.11.0", "fast-memoize": "^2.5.2", "immer": "^9.0.6", "lodash": "^4.17.21", "tslib": "^2.6.0", "urijs": "^1.19.11" } }, "sha512-YNcWv3R3n3U6iQYBsFOiWSuRGE5su1tJSiX6pAPRVk7dP0L7lqCteXGzuVRQ0gMZqUl8v1P0+fAKxF6PLo9B5A=="], - - "@stoplight/ordered-object-literal": ["@stoplight/ordered-object-literal@1.0.5", "", {}, "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg=="], - - "@stoplight/path": ["@stoplight/path@1.3.2", "", {}, "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ=="], - - "@stoplight/spectral-core": ["@stoplight/spectral-core@1.23.1", "", { "dependencies": { "@scarf/scarf": "^1.4.0", "@stoplight/better-ajv-errors": "1.0.3", "@stoplight/json": "~3.21.0", "@stoplight/path": "1.3.2", "@stoplight/spectral-parsers": "^1.0.0", "@stoplight/spectral-ref-resolver": "^1.0.4", "@stoplight/spectral-runtime": "^1.1.2", "@stoplight/types": "~13.6.0", "@types/es-aggregate-error": "^1.0.2", "@types/json-schema": "^7.0.11", "ajv": "^8.18.0", "ajv-errors": "~3.0.0", "ajv-formats": "~2.1.1", "es-aggregate-error": "^1.0.7", "expr-eval-fork": "^3.0.1", "jsonpath-plus": "^10.3.0", "lodash": "^4.18.1", "lodash.topath": "^4.5.2", "minimatch": "^3.1.4", "nimma": "0.2.3", "pony-cause": "^1.1.1", "tslib": "^2.8.1" } }, "sha512-VLC8OhpO/pMJKb6IHhurxJjXO1qB56Ng1unIb8b+hNxdw0+SEcASvmR+RpjfHYX/jv/DfSaA1x8QhFBJBmqBOQ=="], - - "@stoplight/spectral-formats": ["@stoplight/spectral-formats@1.8.5", "", { "dependencies": { "@scarf/scarf": "^1.4.0", "@stoplight/json": "^3.17.0", "@stoplight/spectral-core": "^1.23.0", "@types/json-schema": "^7.0.7", "tslib": "^2.8.1" } }, "sha512-xaC0rCH0p7/bzNJsz+JgLSj+Cp6uwYGWpePQxdLkF2G6a8Zyp3OyS7umkGYNiimEwKrOjvCNNTFJpeuiENZSBA=="], - - "@stoplight/spectral-functions": ["@stoplight/spectral-functions@1.10.5", "", { "dependencies": { "@scarf/scarf": "^1.4.0", "@stoplight/better-ajv-errors": "1.0.3", "@stoplight/json": "^3.17.1", "@stoplight/spectral-core": "^1.23.0", "@stoplight/spectral-formats": "^1.8.1", "@stoplight/spectral-runtime": "^1.1.2", "ajv": "^8.18.0", "ajv-draft-04": "~1.0.0", "ajv-errors": "~3.0.0", "ajv-formats": "~2.1.1", "lodash": "^4.18.1", "tslib": "^2.8.1" } }, "sha512-vDCd0NJ93715bcUpZZ5vNHiyxd4cgHF6tuXsDiXOXKAByg+I1fR5/dMijEo6Ce1Lz95a+RZ22JKYhF1YuzVvuA=="], - - "@stoplight/spectral-parsers": ["@stoplight/spectral-parsers@1.0.5", "", { "dependencies": { "@stoplight/json": "~3.21.0", "@stoplight/types": "^14.1.1", "@stoplight/yaml": "~4.3.0", "tslib": "^2.8.1" } }, "sha512-ANDTp2IHWGvsQDAY85/jQi9ZrF4mRrA5bciNHX+PUxPr4DwS6iv4h+FVWJMVwcEYdpyoIdyL+SRmHdJfQEPmwQ=="], - - "@stoplight/spectral-ref-resolver": ["@stoplight/spectral-ref-resolver@1.0.5", "", { "dependencies": { "@stoplight/json-ref-readers": "1.2.2", "@stoplight/json-ref-resolver": "~3.1.6", "@stoplight/spectral-runtime": "^1.1.2", "dependency-graph": "0.11.0", "tslib": "^2.8.1" } }, "sha512-gj3TieX5a9zMW29z3mBlAtDOCgN3GEc1VgZnCVlr5irmR4Qi5LuECuFItAq4pTn5Zu+sW5bqutsCH7D4PkpyAA=="], - - "@stoplight/spectral-runtime": ["@stoplight/spectral-runtime@1.1.6", "", { "dependencies": { "@stoplight/json": "^3.20.1", "@stoplight/path": "^1.3.2", "@stoplight/types": "^13.6.0", "lodash": "^4.18.1", "node-fetch": "^2.7.0", "tslib": "^2.8.1" } }, "sha512-Y8rEDyMN4bSMJCrDs2shdcVHYyCnH3FvXRP4dBhha4Z8iJv+JPp7KqOV/hwVB/hWFC209upiwj2oDmLfR0qCDg=="], - - "@stoplight/types": ["@stoplight/types@13.20.0", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-2FNTv05If7ib79VPDA/r9eUet76jewXFH2y2K5vuge6SXbRHtWBhcaRmu+6QpF4/WRNoJj5XYRSwLGXDxysBGA=="], - - "@stoplight/yaml": ["@stoplight/yaml@4.3.0", "", { "dependencies": { "@stoplight/ordered-object-literal": "^1.0.5", "@stoplight/types": "^14.1.1", "@stoplight/yaml-ast-parser": "0.0.50", "tslib": "^2.2.0" } }, "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w=="], - - "@stoplight/yaml-ast-parser": ["@stoplight/yaml-ast-parser@0.0.50", "", {}, "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ=="], - "@storybook/addon-a11y": ["@storybook/addon-a11y@10.5.2", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.5.2" } }, "sha512-BMze+oj1Jz3QcGl0E6mjuVA32zPD/cTu3Ev4O4qNbKbqLXO4wA1G6aEd8+CGcQtTGEyMCsr0BH6I/WnXcwA1bQ=="], "@storybook/addon-docs": ["@storybook/addon-docs@10.5.2", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "10.5.2", "@storybook/icons": "^2.0.2", "@storybook/react-dom-shim": "10.5.2", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.5.2" }, "optionalPeers": ["@types/react"] }, "sha512-MoBANDsh5qEA14U+JaBoQcYsKbayJDDMopigFN0NdVAsZTdBfVIsL7cnjTFBL6ubB3ifb5M0tCXbScpml1KqiQ=="], @@ -2994,33 +2914,37 @@ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.11", "", { "os": "win32", "cpu": "x64" }, "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg=="], + "@tailwindcss/typography": ["@tailwindcss/typography@0.5.20", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || >=4.0.0 || insiders" } }, "sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="], - "@tanstack/directive-functions-plugin": ["@tanstack/directive-functions-plugin@1.134.5", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.27.7", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@tanstack/router-utils": "1.133.19", "babel-dead-code-elimination": "^1.0.10", "pathe": "^2.0.3", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "vite": ">=6.0.0 || >=7.0.0" } }, "sha512-J3oawV8uBRBbPoLgMdyHt+LxzTNuWRKNJJuCLWsm/yq6v0IQSvIVCgfD2+liIiSnDPxGZ8ExduPXy8IzS70eXw=="], + "@takumi-rs/core": ["@takumi-rs/core@2.4.1", "", { "dependencies": { "@takumi-rs/helpers": "2.4.1" }, "optionalDependencies": { "@takumi-rs/core-darwin-arm64": "2.4.1", "@takumi-rs/core-darwin-x64": "2.4.1", "@takumi-rs/core-linux-arm64-gnu": "2.4.1", "@takumi-rs/core-linux-arm64-musl": "2.4.1", "@takumi-rs/core-linux-x64-gnu": "2.4.1", "@takumi-rs/core-linux-x64-musl": "2.4.1", "@takumi-rs/core-win32-arm64-msvc": "2.4.1", "@takumi-rs/core-win32-x64-msvc": "2.4.1" }, "peerDependencies": { "csstype": "*" }, "optionalPeers": ["csstype"] }, "sha512-eAFDIg9HRLU1ciCd2HPfZ3MPREF3MAkTHMzZ3570fcYI9Y/qLdPE77oo6CgBPxisp7pccxC1ub3/MxvpL6r6Pg=="], - "@tanstack/history": ["@tanstack/history@1.162.0", "", {}, "sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA=="], + "@takumi-rs/core-darwin-arm64": ["@takumi-rs/core-darwin-arm64@2.4.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8Caa7tIU6OY0uww6hh8LFJfAy6zsf2+bDNUcqkkWAAEkxPSLgUNdRxRZPTVNAqpYXE70LxO+D7xhmbOvx3jw4g=="], - "@tanstack/query-core": ["@tanstack/query-core@5.91.2", "", {}, "sha512-Uz2pTgPC1mhqrrSGg18RKCWT/pkduAYtxbcyIyKBhw7dTWjXZIzqmpzO2lBkyWr4hlImQgpu1m1pei3UnkFRWw=="], + "@takumi-rs/core-darwin-x64": ["@takumi-rs/core-darwin-x64@2.4.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-24rySftJ5Jldn2BhtGr4Z15flQfD+y4qPEh5OkuQ65Wput4n7AwXbYMjx57Su1jn2kSgePjLVOO+DaRs3MPz9A=="], + + "@takumi-rs/core-linux-arm64-gnu": ["@takumi-rs/core-linux-arm64-gnu@2.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-YuzyFk4NsNlrYORBSevLwX7LDsmnmQRIYFyAtfpcVv+YZw4vkEyvekU04nlh8dd7hjJaOY25KY68l+y5FAQkqA=="], - "@tanstack/react-router": ["@tanstack/react-router@1.170.17", "", { "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.171.14", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-ppLkjCfSMaeug9rmFRYzOd4TIqWV+yTE7tzIny7alJsSnM7w4lzEZm6eqCehG0SPetpZ0R3K+UnanSmBgOAVcQ=="], + "@takumi-rs/core-linux-arm64-musl": ["@takumi-rs/core-linux-arm64-musl@2.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-w+dor/U/zQffPXhoOrgViUy0pjHZUjkjyXojdGHe9z2i2/NMsAaONdUNASE/ZjWaehrDs2bWPdJ+cgxYFwbKGg=="], - "@tanstack/react-start": ["@tanstack/react-start@1.168.27", "", { "dependencies": { "@tanstack/react-router": "1.170.17", "@tanstack/react-start-client": "1.168.15", "@tanstack/react-start-rsc": "0.1.26", "@tanstack/react-start-server": "1.167.21", "@tanstack/router-utils": "1.162.2", "@tanstack/start-client-core": "1.170.13", "@tanstack/start-plugin-core": "1.171.19", "@tanstack/start-server-core": "1.169.16", "pathe": "^2.0.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "@vitejs/plugin-rsc": "*", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "@vitejs/plugin-rsc", "vite"] }, "sha512-rdGFDqfCW71gyofyAxaYxhelNKmeVjpmbpm0uFYbNHORCa///4aBxi7B7ecShibKv9O4GfJ66MPX5F0ozbm+ig=="], + "@takumi-rs/core-linux-x64-gnu": ["@takumi-rs/core-linux-x64-gnu@2.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-6zFsEkSMOhXJxkktT0v/OnE/CPDAI+DxE3QHsj5L4VJa9yYA4MrxB+yiop2aXS63ZbnRdS5GlqUft/tkxZWs9w=="], - "@tanstack/react-start-client": ["@tanstack/react-start-client@1.168.15", "", { "dependencies": { "@tanstack/react-router": "1.170.17", "@tanstack/router-core": "1.171.14", "@tanstack/start-client-core": "1.170.13" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-pW50PHvadgi50iNCw6deUOvqc9rzs30SstyFZY2tcS9z1XlqTlELSvGowjxdu2m0ymtqm1emj1jau1iP7+3+PQ=="], + "@takumi-rs/core-linux-x64-musl": ["@takumi-rs/core-linux-x64-musl@2.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-95wuhp/gM1NwJ47CGEW89Np9/Qt1gMkpmyYACjtpX9jMS9iTA5uAeU1Ur+ouGEa+SRp6wf5v5HEBmVMO9TfCBQ=="], - "@tanstack/react-start-rsc": ["@tanstack/react-start-rsc@0.1.26", "", { "dependencies": { "@tanstack/react-router": "1.170.17", "@tanstack/router-core": "1.171.14", "@tanstack/router-utils": "1.162.2", "@tanstack/start-client-core": "1.170.13", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-plugin-core": "1.171.19", "@tanstack/start-server-core": "1.169.16", "@tanstack/start-storage-context": "1.167.16", "pathe": "^2.0.3" }, "peerDependencies": { "@rspack/core": ">=2.0.0-0", "@vitejs/plugin-rsc": ">=0.5.20", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "react-server-dom-rspack": ">=0.0.2" }, "optionalPeers": ["@rspack/core", "@vitejs/plugin-rsc", "react-server-dom-rspack"] }, "sha512-+FMm3qtT1gWsl0i5sG/Q70mh1k7tzZUsPBaqbg4v34zVJZ+XGn1mJb34x2w7z1M0/e7co6GkZfV8BRpczrs8UA=="], + "@takumi-rs/core-win32-arm64-msvc": ["@takumi-rs/core-win32-arm64-msvc@2.4.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-I8k6g4BDY48Q4C6L6CQL9M7lfpijtegKk1AtD0i41SaRpWLkGcEmxjH8iYym3X3LYEL/GX6J9iQZaoG9J8saZg=="], - "@tanstack/react-start-server": ["@tanstack/react-start-server@1.167.21", "", { "dependencies": { "@tanstack/react-router": "1.170.17", "@tanstack/router-core": "1.171.14", "@tanstack/start-server-core": "1.169.16" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-puJ7eFxaLuDzeM/tiLDJaXCA4uK+PZnEOoIC73zipsFqx865MGzrRS/GSZxeVxjavC5iHU+ZwC+rgI0qYSol1A=="], + "@takumi-rs/core-win32-x64-msvc": ["@takumi-rs/core-win32-x64-msvc@2.4.1", "", { "os": "win32", "cpu": "x64" }, "sha512-ugGBq7pbGr0Tqm+egrgDcGglsJXsiFdE3w33iI4+ImXcuoaV+19oSPZZqP5Vr44/+lp3rcEJ29vlufUmljBB3g=="], - "@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="], + "@takumi-rs/helpers": ["@takumi-rs/helpers@2.4.1", "", { "peerDependencies": { "preact": "^10.0.0", "react": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["preact", "react"] }, "sha512-gpm1exkspnPNcPp9lnII204hTC7i0bu0ksyqC01aMF4eSw6u1OxnlKEDVUTGm1dFBFecfHaPZBbi+O8SAypXpw=="], - "@tanstack/router-core": ["@tanstack/router-core@1.171.14", "", { "dependencies": { "@tanstack/history": "1.162.0", "cookie-es": "^3.0.0", "seroval": "^1.5.4", "seroval-plugins": "^1.5.4" } }, "sha512-Mo3hwx0qB0cJsVYGDjG0+Ouf7VV74h/vsoDMGztdlyzDanp4gBA2s7IVvm6hFrmQM6GpD9F0Z7SqD7OldfLE7g=="], + "@takumi-rs/wasm": ["@takumi-rs/wasm@2.4.1", "", { "dependencies": { "@takumi-rs/helpers": "2.4.1" }, "peerDependencies": { "csstype": "*" }, "optionalPeers": ["csstype"] }, "sha512-VhJtfCCRs+vxGCAFEkeu3/UI0VpToX6ys7KfeNBUTtVb9SGtkxBFxXixwmorALW0kzUuRkLHUl2HpylvlKk7Xg=="], - "@tanstack/router-generator": ["@tanstack/router-generator@1.167.18", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.14", "@tanstack/router-utils": "1.162.2", "@tanstack/virtual-file-routes": "1.162.0", "jiti": "^2.7.0", "magic-string": "^0.30.21", "prettier": "^3.5.0", "zod": "^4.4.3" } }, "sha512-kFvM4caRds9Q3EXg64bZubJ6rbDxyV0YDSBSGvOGzmKspQPdz5Xrh0uj5T1Ov8avUUg+c761u04VQAaEzSBXRw=="], + "@tanstack/directive-functions-plugin": ["@tanstack/directive-functions-plugin@1.134.5", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.27.7", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@tanstack/router-utils": "1.133.19", "babel-dead-code-elimination": "^1.0.10", "pathe": "^2.0.3", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "vite": ">=6.0.0 || >=7.0.0" } }, "sha512-J3oawV8uBRBbPoLgMdyHt+LxzTNuWRKNJJuCLWsm/yq6v0IQSvIVCgfD2+liIiSnDPxGZ8ExduPXy8IzS70eXw=="], - "@tanstack/router-plugin": ["@tanstack/router-plugin@1.168.19", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.14", "@tanstack/router-generator": "1.167.18", "@tanstack/router-utils": "1.162.2", "chokidar": "^5.0.0", "unplugin": "^3.0.0", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2 || ^2.0.0", "@tanstack/react-router": "^1.170.17", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-aFglwLc+bbPTgZlkXn3PvOwpjJAfgUyPGSuql4MP3XrqTTh6WkBiy2RYb6oaG5h0s7EKwivEuq85K3Y4V0Mt1g=="], + "@tanstack/query-core": ["@tanstack/query-core@5.91.2", "", {}, "sha512-Uz2pTgPC1mhqrrSGg18RKCWT/pkduAYtxbcyIyKBhw7dTWjXZIzqmpzO2lBkyWr4hlImQgpu1m1pei3UnkFRWw=="], - "@tanstack/router-utils": ["@tanstack/router-utils@1.162.2", "", { "dependencies": { "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-hTWqJtqIFFdvuCl8WXNyrodp2L9zo2G37xKRrcVmVRWpAB2h+U1LuRAfS4tsFTiWOIoE/B+WDVFB8JpoEdw6jQ=="], + "@tanstack/router-utils": ["@tanstack/router-utils@1.133.19", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.5", "@babel/preset-typescript": "^7.27.1", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-WEp5D2gPxvlLDRXwD/fV7RXjYtqaqJNXKB/L6OyZEbT+9BG/Ib2d7oG9GSUZNNMGPGYAlhBUOi3xutySsk6rxA=="], "@tanstack/server-functions-plugin": ["@tanstack/server-functions-plugin@1.134.5", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@tanstack/directive-functions-plugin": "1.134.5", "babel-dead-code-elimination": "^1.0.9", "tiny-invariant": "^1.3.3" } }, "sha512-2sWxq70T+dOEUlE3sHlXjEPhaFZfdPYlWTSkHchWXrFGw2YOAa+hzD6L9wHMjGDQezYd03ue8tQlHG+9Jzbzgw=="], @@ -3028,22 +2952,8 @@ "@tanstack/solid-virtual": ["@tanstack/solid-virtual@3.13.32", "", { "dependencies": { "@tanstack/virtual-core": "3.17.3" }, "peerDependencies": { "solid-js": "^1.3.0" } }, "sha512-yhX4A4Kgn+wyTg6Mmu8+zwoMTwjz4K1ucvLfRJ8f0rPGDDAIqSaf0v6oU0yT9+SvrjmUaZQ0VX7g4byexbhNng=="], - "@tanstack/start-client-core": ["@tanstack/start-client-core@1.170.13", "", { "dependencies": { "@tanstack/router-core": "1.171.14", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-storage-context": "1.167.16", "seroval": "^1.5.4" } }, "sha512-o37M3msIK5ec87kPrIYJWXb1XPnjIe5/jrkGLXiXpFuVL99z7mhoBCzftKtVPtzqI8EElnRE/VGFYT9BHNnWcw=="], - - "@tanstack/start-fn-stubs": ["@tanstack/start-fn-stubs@1.162.0", "", {}, "sha512-QWfUZ3Yo923tdQn38LyKMU8rcTw69zc+T4dAvgTWV4O56SqFRsGfS0lSWIMhJRwXIx/bvdi7nTUBDdZtTHtpTQ=="], - - "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.171.19", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.14", "@tanstack/router-generator": "1.167.18", "@tanstack/router-plugin": "1.168.19", "@tanstack/router-utils": "1.162.2", "@tanstack/start-server-core": "1.169.16", "exsolve": "^1.0.7", "lightningcss": "^1.32.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "seroval": "^1.5.4", "source-map": "^0.7.6", "srvx": "^0.11.9", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "vite"] }, "sha512-+fpW3Z/2vPT8HDV1c5p2WC6/g2k/AV/ujdJVDcn/VFd+gXRtzSX1D/LfozlaDbhDoEsqOnAk/mGwjg60JkUA2Q=="], - - "@tanstack/start-server-core": ["@tanstack/start-server-core@1.169.16", "", { "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/router-core": "1.171.14", "@tanstack/start-client-core": "1.170.13", "@tanstack/start-storage-context": "1.167.16", "fetchdts": "^0.1.6", "h3-v2": "npm:h3@2.0.1-rc.20", "seroval": "^1.5.4" } }, "sha512-lvAjQpH3nHJtd4xy0iHIaWbsTbyN9EBxuYCxbtXH0EpeBQPg+TCPhu9GQC9WbbA1rE//s82CpE55oYDQMqkU5A=="], - - "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.167.16", "", { "dependencies": { "@tanstack/router-core": "1.171.14" } }, "sha512-zTegxlij4BC1DbCrC6rsVlMOQVMzOuG5IllacZEkrUdhiFwMIMYpk0VWGH+d0ucx5RBkmv8e8GNX3AOVBWclfg=="], - - "@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="], - "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.3", "", {}, "sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw=="], - "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.162.0", "", {}, "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA=="], - "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], @@ -3052,8 +2962,6 @@ "@thisbeyond/solid-dnd": ["@thisbeyond/solid-dnd@0.7.5", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="], - "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], - "@tsconfig/bun": ["@tsconfig/bun@1.0.9", "", {}, "sha512-4M0/Ivfwcpz325z6CwSifOBZYji3DFOEpY6zEUt0+Xi2qRhzwvmqQN9XAHJh3OVvRJuAqVTLU2abdCplvp6mwQ=="], "@tsconfig/node22": ["@tsconfig/node22@22.0.2", "", {}, "sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA=="], @@ -3102,22 +3010,74 @@ "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], - "@types/cross-spawn": ["@types/cross-spawn@6.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA=="], + "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], + + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], + + "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], + + "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], + + "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], + + "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], + + "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], + + "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], + + "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], + + "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], + "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="], + "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], + + "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], + + "@types/d3-random": ["@types/d3-random@3.0.4", "", {}, "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA=="], + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], + + "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + + "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], + + "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], - "@types/es-aggregate-error": ["@types/es-aggregate-error@1.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg=="], - "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], @@ -3224,8 +3184,6 @@ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@types/urijs": ["@types/urijs@1.19.26", "", {}, "sha512-wkXrVzX5yoqLnndOwFsieJA7oKM8cNkOKJtf/3vVGSUFkWDKZvFHpIl9Pvqb/T9UsawBBFMTTD8xu7sK5MWuvg=="], - "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], "@types/which": ["@types/which@3.0.4", "", {}, "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w=="], @@ -3256,13 +3214,27 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.3", "", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="], + "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], + "@upstash/redis": ["@upstash/redis@1.38.0", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-wu+dZBptlLy0+MCUEoHmzrY/TnmgDey3+c7EbIGwrLqAvkP8yi5MWZHYGIFtAygmL4Bkz2TdFu+eU0vFPncIcg=="], "@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="], + "@vercel/analytics": ["@vercel/analytics@2.0.1", "", { "peerDependencies": { "@remix-run/react": "^2", "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "nuxt": ">= 3", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@remix-run/react", "@sveltejs/kit", "next", "nuxt", "react", "svelte", "vue", "vue-router"] }, "sha512-MTQG6V9qQrt1tsDeF+2Uoo5aPjqbVPys1xvnIftXSJYG2SrwXRHnqEvVoYID7BTruDz4lCd2Z7rM1BdkUehk2g=="], + + "@vercel/cli-config": ["@vercel/cli-config@0.2.0", "", { "dependencies": { "xdg-app-paths": "5", "zod": "4.1.11" } }, "sha512-fJRRRB7734BDuXZ89yBEaA2ncYhH7bWX30mk04W80J6VAfQc+4iB8lyzAdaGpFV3/vNlkt9VZt+/uoQoWX6UsQ=="], + + "@vercel/cli-exec": ["@vercel/cli-exec@1.0.0", "", { "dependencies": { "execa": "5.1.1" } }, "sha512-kQF8LGie/Hbdq9/psJxLE7owRTcqMQMhgybU04gCeR7cbQAr5t8OrjefDNColJv1QSSucFt4pLwRiARVmlOnug=="], + + "@vercel/functions": ["@vercel/functions@3.7.5", "", { "dependencies": { "@vercel/oidc": "3.8.0" }, "peerDependencies": { "@aws-sdk/credential-provider-web-identity": "*", "ws": ">=8" }, "optionalPeers": ["@aws-sdk/credential-provider-web-identity", "ws"] }, "sha512-ESf8BbeDebqRUyMi09JwRbQqpLn4g6fjcVVHPsHB56j2dSqRrSHO4h3X4aaxJf6iQQjzhAtDGI2xCWQ27JE8PA=="], + + "@vercel/nft": ["@vercel/nft@1.10.2", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^13.0.0", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-w+WyX5Ulmj4dtTZrxaulqrjaLZHSbnPzx75SJsTNYmotKsqn1JlLnDJa+lz5hn90HJofhl/2MAtw0mCrgM3qYw=="], + "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.3", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg=="], + "@vercel/routing-utils": ["@vercel/routing-utils@5.3.3", "", { "dependencies": { "path-to-regexp": "6.1.0", "path-to-regexp-updated": "npm:path-to-regexp@6.3.0" }, "optionalDependencies": { "ajv": "^6.12.3" } }, "sha512-KYm2sLNUD48gDScv8ob4ejc3Gww2jcJyW80hTdYlenAPz/5BQar1Gyh38xrUuZ532TUwSb5mV1uRbAuiykq0EQ=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], @@ -3310,20 +3282,16 @@ "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], - "address": ["address@1.2.2", "", {}, "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA=="], - - "adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="], - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], - "aggregate-error": ["aggregate-error@4.0.1", "", { "dependencies": { "clean-stack": "^4.0.0", "indent-string": "^5.0.0" } }, "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w=="], - "ai": ["ai@6.0.168", "", { "dependencies": { "@ai-sdk/gateway": "3.0.104", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ=="], "ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="], @@ -3332,18 +3300,16 @@ "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], - "ajv-errors": ["ajv-errors@3.0.0", "", { "peerDependencies": { "ajv": "^8.0.1" } }, "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ=="], - "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "ajv-i18n": ["ajv-i18n@4.2.0", "", { "peerDependencies": { "ajv": "^8.0.0-beta.0" } }, "sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg=="], + "am-i-vibing": ["am-i-vibing@0.4.0", "", { "dependencies": { "process-ancestry": "^0.1.0" }, "bin": { "am-i-vibing": "dist/cli.mjs" } }, "sha512-MxT4XZL7pzLHpuvhDKdMaQHMGGkJDLluKBLsbstn+8wv9sWcFT6h+0ve9qkml95amVTZtZV83gQe2hY+ojgHLg=="], + "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], - "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], - "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -3406,20 +3372,18 @@ "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + "async-sema": ["async-sema@3.1.1", "", {}, "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], "atomically": ["atomically@2.1.1", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ=="], - "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], - "autoprefixer": ["autoprefixer@10.5.4", "", { "dependencies": { "browserslist": "^4.28.6", "caniuse-lite": "^1.0.30001806", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA=="], "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "avsc": ["avsc@5.7.9", "", {}, "sha512-yOA4wFeI7ET3v32Di/sUybQ+ttP20JHSW3mxLuNGeO0uD6PPcvLrIQXSvy/rhJOWU5JrYh7U4OHplWMmtAtjMg=="], - "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], "aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="], @@ -3440,6 +3404,8 @@ "babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="], + "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], + "babel-preset-solid": ["babel-preset-solid@1.9.12", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="], "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], @@ -3460,20 +3426,14 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.43", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ=="], - "basic-ftp": ["basic-ftp@5.3.1", "", {}, "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw=="], - "bcp-47": ["bcp-47@2.1.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-KLw+H/gd2p4zly1X7Yh/qziuyae5/w/QFnvTng9eZL5fvszL7Whl3MBoWF8yxL7ksUjBfOD+OxkytiqbBpG+Fw=="], "bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="], "before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="], - "better-opn": ["better-opn@3.0.2", "", { "dependencies": { "open": "^8.0.4" } }, "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ=="], - "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], "bin-links": ["bin-links@6.0.2", "", { "dependencies": { "cmd-shim": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "proc-log": "^6.0.0", "read-cmd-shim": "^6.0.0", "write-file-atomic": "^7.0.0" } }, "sha512-frE1t78WOwJ45PKV2cF2tNPjTcs9L1J9s6VkrV59wanRP4GlaomuxYPVma7BwthMg8WnfSory4w5PTE6FZZ81w=="], @@ -3482,7 +3442,7 @@ "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], - "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], @@ -3490,6 +3450,8 @@ "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="], + "blume": ["blume@1.1.4", "", { "dependencies": { "@astrojs/check": "^0.9.0", "@astrojs/markdown-satteri": "^0.3.2", "@astrojs/mdx": "^7.0.0", "@astrojs/node": "^11.0.0", "@astrojs/react": "^6.0.0", "@astrojs/vercel": "^11.0.0", "@clack/prompts": "^1.7.0", "@iconify-json/lucide": "^1.2.115", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.3", "@modelcontextprotocol/sdk": "^1.29.0", "@orama/orama": "^3.1.18", "@pierre/diffs": "^1.2.11", "@scalar/astro": "^0.4.5", "@scalar/openapi-parser": "^0.28.8", "@scalar/openapi-types": "^0.9.1", "@shikijs/transformers": "^4.2.0", "@shikijs/twoslash": "^4.2.0", "@tailwindcss/typography": "^0.5.20", "@tailwindcss/vite": "^4", "@vercel/analytics": "^2.0.1", "ai": "^5.0.0", "astro": "^7.0.2", "babel-plugin-react-compiler": "^1.0.0", "citty": "^0.1.6", "consola": "^3.4.0", "dompurify": "^3.4.11", "epub-gen-memory": "^1.1.2", "github-slugger": "^2.0.0", "gray-matter": "^4.0.3", "jiti": "^2.4.0", "js-yaml": "^4.1.0", "katex": "^0.17.0", "marked": "^18.0.5", "mermaid": "^11.15.0", "node-html-parser": "^9.0.0", "pagefind": "^1.3.0", "pathe": "^2.0.0", "react": "^19.0.0", "react-dom": "^19.0.0", "satteri": "^0.9.5", "shiki": "^4.2.0", "simple-icons": "^13.0.0", "tailwindcss": "^4", "takumi-js": "^2.2.1", "tinyglobby": "^0.2.10", "twoslash": "^0.3.9", "typescript": "^6.0.3", "undici": "^8.6.0", "zod": "^3.24.0" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^1.0.41", "@astrojs/cloudflare": "^14.0.0", "@astrojs/netlify": "^8.0.0", "@astrojs/svelte": "^9.0.0", "@astrojs/vue": "^7.0.0", "@mixedbread/sdk": "^0.76.0", "@notionhq/client": "^2.2.15", "@openrouter/ai-sdk-provider": "^1.5.4", "@oramacloud/client": "^2.1.0", "@sanity/client": "^6.21.0", "algoliasearch": "^5.55.0", "flexsearch": "^0.8.0", "typesense": "^3.0.0" }, "optionalPeers": ["@ai-sdk/openai-compatible", "@astrojs/cloudflare", "@astrojs/netlify", "@astrojs/svelte", "@astrojs/vue", "@mixedbread/sdk", "@notionhq/client", "@openrouter/ai-sdk-provider", "@oramacloud/client", "@sanity/client", "algoliasearch", "flexsearch", "typesense"], "bin": { "blume": "bin/blume.mjs" } }, "sha512-boCWAMfuyc2788hjGJPrmxu4mNUojG+XkkPypjq+z8KErq1yV4XrHgET8bzVJtmOdpWVGLIjfzmbLniTePBU8Q=="], + "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], @@ -3580,8 +3542,6 @@ "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], - "chardet": ["chardet@2.2.0", "", {}, "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA=="], - "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], @@ -3594,32 +3554,22 @@ "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], - "chromium-bidi": ["chromium-bidi@0.6.2", "", { "dependencies": { "mitt": "3.0.1", "urlpattern-polyfill": "10.0.0", "zod": "3.23.8" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-4WVBa6ijmUTVr9cZD4eicQD8Mdy/HCX3bzEIYYpmk0glqYLoWH+LqQEvV9RpDRzoQSbY1KJHloYXbDMXMbDPhg=="], - "chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="], "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], - "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], - "classnames": ["classnames@2.3.2", "", {}, "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="], "clean-css": ["clean-css@5.3.3", "", { "dependencies": { "source-map": "~0.6.0" } }, "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg=="], - "clean-stack": ["clean-stack@4.2.0", "", { "dependencies": { "escape-string-regexp": "5.0.0" } }, "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg=="], - "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], - "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], - "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], - "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], - "clipboardy": ["clipboardy@4.0.0", "", { "dependencies": { "execa": "^8.0.1", "is-wsl": "^3.1.0", "is64bit": "^2.0.0" } }, "sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w=="], "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], @@ -3636,16 +3586,10 @@ "cmd-shim": ["cmd-shim@8.0.0", "", {}, "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA=="], - "cnfast": ["cnfast@0.0.8", "", { "bin": { "cnfast": "bin/cli.js" } }, "sha512-EjXKMfGfdwtV4AcNSQ6AwQaVzpC1B7IxeiwA3FlhTXz+YFlMKVi4c1JX9tgD2QOlahQXjB8KUXrBaYG+3v871Q=="], - - "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], - "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], - "color-blend": ["color-blend@4.0.0", "", {}, "sha512-fYODTHhI/NG+B5GnzvuL3kiFrK/UnkUezWFTgEPBTY5V+kpyfAn95Vn9sJeeCX6omrCOdxnqCL3CvH+6sXtIbw=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], @@ -3666,8 +3610,6 @@ "compress-commons": ["compress-commons@6.0.2", "", { "dependencies": { "crc-32": "^1.2.0", "crc32-stream": "^6.0.0", "is-stream": "^2.0.1", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg=="], - "compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="], - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "condense-newlines": ["condense-newlines@0.2.1", "", { "dependencies": { "extend-shallow": "^2.0.1", "is-whitespace": "^0.3.0", "kind-of": "^3.0.2" } }, "sha512-P7X+QL9Hb9B/c8HI5BFFKmjgBu2XpQuF98WZ9XkO+dBGgk5XgwiQz7o1SmpglNWId3581UcS0SFAWfoIhMHPfg=="], @@ -3686,8 +3628,6 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], - "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "cookie-es": ["cookie-es@2.0.1", "", {}, "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA=="], @@ -3698,7 +3638,7 @@ "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], - "cosmiconfig": ["cosmiconfig@9.0.2", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg=="], + "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], @@ -3724,26 +3664,82 @@ "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], - "cssfilter": ["cssfilter@0.0.10", "", {}, "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw=="], + "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "cytoscape": ["cytoscape@3.34.0", "", {}, "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg=="], + + "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="], + + "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="], + + "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], + + "d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="], + + "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="], + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="], + + "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], + + "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], + + "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], + + "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="], + + "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], + "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="], + + "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], + + "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], + + "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], + + "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], + + "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], + + "dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], @@ -3752,24 +3748,20 @@ "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + "dayjs": ["dayjs@1.11.21", "", {}, "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="], + "db0": ["db0@0.3.4", "", { "peerDependencies": { "@electric-sql/pglite": "*", "@libsql/client": "*", "better-sqlite3": "*", "drizzle-orm": "*", "mysql2": "*", "sqlite3": "*" }, "optionalPeers": ["@electric-sql/pglite", "@libsql/client", "better-sqlite3", "drizzle-orm", "mysql2", "sqlite3"] }, "sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw=="], "debounce-fn": ["debounce-fn@6.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "decode-bmp": ["decode-bmp@0.2.1", "", { "dependencies": { "@canvas/image-data": "^1.0.0", "to-data-view": "^1.1.0" } }, "sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA=="], - - "decode-ico": ["decode-ico@0.4.1", "", { "dependencies": { "@canvas/image-data": "^1.0.0", "decode-bmp": "^0.2.0", "to-data-view": "^1.1.0" } }, "sha512-69NZfbKIzux1vBOd31al3XnMnH+2mqDhEgLdpygErm4d60N+UwA5Sq5WFjmEDQzumgB9fElojGwWG0vybVfFmA=="], - "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], - "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], - "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], @@ -3786,7 +3778,7 @@ "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], - "degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="], + "delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="], "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], @@ -3794,8 +3786,6 @@ "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - "dependency-graph": ["dependency-graph@0.11.0", "", {}, "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg=="], - "deprecation": ["deprecation@2.3.1", "", {}, "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -3810,16 +3800,12 @@ "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], - "detect-port": ["detect-port@1.5.1", "", { "dependencies": { "address": "^1.0.1", "debug": "4" }, "bin": { "detect": "bin/detect-port.js", "detect-port": "bin/detect-port.js" } }, "sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ=="], - "deterministic-object-hash": ["deterministic-object-hash@2.0.2", "", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="], "devalue": ["devalue@5.8.1", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - "devtools-protocol": ["devtools-protocol@0.0.1312386", "", {}, "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA=="], - "dfa": ["dfa@1.2.0", "", {}, "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="], "diacritics": ["diacritics@1.3.0", "", {}, "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA=="], @@ -3838,10 +3824,6 @@ "dmg-builder": ["dmg-builder@26.15.2", "", { "dependencies": { "app-builder-lib": "26.15.2", "builder-util": "26.15.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0" } }, "sha512-fMkjRqKyPtsz4Kzu/qGP0BGjqzMCIgp+/7kw/u6YH6lvn/8hvL3c0TXhoFayBoYdpPCnEinnCHztd4bW7/jetA=="], - "dns-packet": ["dns-packet@5.6.1", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="], - - "dns-socket": ["dns-socket@4.2.2", "", { "dependencies": { "dns-packet": "^5.2.4" } }, "sha512-BDeBd8najI4/lS00HSKpdFia+OvUMytaVjfzR9n5Lq8MlZRSvtbI+uLtx1+XmQFls5wFU9dssccTmQQ6nfpjdg=="], - "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], @@ -3922,8 +3904,6 @@ "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - "engine.io": ["engine.io@6.6.9", "", { "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "@types/ws": "^8.5.12", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.21.0" } }, "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg=="], - "engine.io-client": ["engine.io-client@6.6.6", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.21.0", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q=="], "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], @@ -3934,12 +3914,10 @@ "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], - "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "epub-gen-memory": ["epub-gen-memory@1.1.2", "", { "dependencies": { "abort-controller": "^3.0.0", "css-select": "^4.1.3", "diacritics": "^1.3.0", "dom-serializer": "^1.3.2", "domhandler": "^4.2.2", "domutils": "^2.8.0", "ejs": "^3.1.6", "htmlparser2": "^7.1.2", "jszip": "^3.7.1", "mime": "^2.5.2", "node-fetch": "^2.0.0", "ow": "^0.28.1", "slugify": "^1.6.5" } }, "sha512-vwGM6MVNqKIskFzPZqhi4ZOs0ZTUXco9oDuHFX1vB2Il9pTAkaHWFBFgHrrl832dYmBPb/raGVUZXFvZYueRyw=="], "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], - "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], - "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], @@ -3948,8 +3926,6 @@ "es-abstract-get": ["es-abstract-get@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "es-object-atoms": "^1.1.2", "is-callable": "^1.2.7", "object-inspect": "^1.13.4" } }, "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg=="], - "es-aggregate-error": ["es-aggregate-error@1.0.14", "", { "dependencies": { "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "globalthis": "^1.0.4", "has-property-descriptors": "^1.0.2", "set-function-name": "^2.0.2" } }, "sha512-3YxX6rVb07B5TV11AV5wsL7nQCHXNwoHPsQC8S4AmBiqYhyNCJ5BRKXkXyDJvs8QzXN20NgRtxe3dEEQD9NLHA=="], - "es-array-method-boxes-properly": ["es-array-method-boxes-properly@1.0.0", "", {}, "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], @@ -3986,12 +3962,8 @@ "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], - "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], @@ -4002,14 +3974,10 @@ "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], - "estree-util-value-to-estree": ["estree-util-value-to-estree@3.5.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ=="], - "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], @@ -4028,14 +3996,10 @@ "exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="], - "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], - "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], - "expr-eval-fork": ["expr-eval-fork@3.0.3", "", {}, "sha512-BhC+hbc5lIVjygr840n5DEkW3MQq7H9o+mc1/N7Z5uIiCFVyESLL5DIE7LNq4CYUNxy+XjA+3jRrL/h0Kt2xcg=="], - "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "express-rate-limit": ["express-rate-limit@8.6.0", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA=="], @@ -4062,27 +4026,29 @@ "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - "fast-memoize": ["fast-memoize@2.5.2", "", {}, "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw=="], + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], + + "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], "fast-uri": ["fast-uri@3.1.3", "", {}, "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg=="], + "fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="], + "fast-xml-builder": ["fast-xml-builder@1.3.0", "", { "dependencies": { "path-expression-matcher": "^1.6.2", "xml-naming": "^0.3.0" } }, "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ=="], "fast-xml-parser": ["fast-xml-parser@4.4.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="], "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], - "fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="], - - "favicons": ["favicons@7.2.0", "", { "dependencies": { "escape-html": "^1.0.3", "sharp": "^0.33.1", "xml2js": "^0.6.1" } }, "sha512-k/2rVBRIRzOeom3wI9jBPaSEvoTSQEW4iM0EveBmBBKFxO8mSyyRWtDlfC3VnEfu0avmjrMzy8/ZFPSe6F71Hw=="], - "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], - "fetchdts": ["fetchdts@0.1.7", "", {}, "sha512-YoZjBdafyLIop9lSxXVI33oLD5kN31q4Td+CasofLLYeLXRFeOsuOw0Uo+XNRi9PZlbfdlN2GmRtm4tCEQ9/KA=="], + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], "filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="], @@ -4106,6 +4072,8 @@ "fontkit": ["fontkit@2.0.4", "", { "dependencies": { "@swc/helpers": "^0.5.12", "brotli": "^1.3.2", "clone": "^2.1.2", "dfa": "^1.2.0", "fast-deep-equal": "^3.1.3", "restructure": "^3.0.0", "tiny-inflate": "^1.0.3", "unicode-properties": "^1.4.0", "unicode-trie": "^2.0.0" } }, "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g=="], + "fontkitten": ["fontkitten@1.0.3", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw=="], + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], @@ -4114,8 +4082,6 @@ "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], - "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], - "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], @@ -4124,16 +4090,10 @@ "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], - "fractional-indexing": ["fractional-indexing@3.2.0", "", {}, "sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ=="], - "framer-motion": ["framer-motion@8.5.5", "", { "dependencies": { "@motionone/dom": "^10.15.3", "hey-listen": "^1.0.8", "tslib": "^2.4.0" }, "optionalDependencies": { "@emotion/is-prop-valid": "^0.8.2" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-5IDx5bxkjWHWUF3CVJoSyUVOtrbAxtzYBBowRE2uYI/6VYhkEBD+rbTHEGuUmbGHRj6YqqSfoG7Aa1cLyWCrBA=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - "front-matter": ["front-matter@4.0.2", "", { "dependencies": { "js-yaml": "^3.13.1" } }, "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg=="], - - "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], - "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], "fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="], @@ -4142,12 +4102,6 @@ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "fumadocs-core": ["fumadocs-core@16.11.1", "", { "dependencies": { "@orama/orama": "^3.1.18", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "js-yaml": "^5.2.1", "mdast-util-mdx": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", "remark": "^15.0.1", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^4.3.1", "tinyglobby": "^0.2.17", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@mdx-js/mdx": "*", "@mixedbread/sdk": "0.x.x", "@orama/core": "1.x.x", "@oramacloud/client": "2.x.x", "@tanstack/react-router": "1.x.x", "@types/estree-jsx": "*", "@types/hast": "*", "@types/mdast": "*", "@types/react": "*", "algoliasearch": "5.x.x", "flexsearch": "*", "lucide-react": "*", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router": "7.x.x || 8.x.x", "waku": "*", "zod": "4.x.x" }, "optionalPeers": ["@mdx-js/mdx", "@mixedbread/sdk", "@orama/core", "@oramacloud/client", "@tanstack/react-router", "@types/estree-jsx", "@types/hast", "@types/mdast", "@types/react", "algoliasearch", "flexsearch", "lucide-react", "next", "react", "react-dom", "react-router", "waku", "zod"] }, "sha512-tKuh1AKoVTb+f7IoAOM2cfz5djd3YhePeqA95q6mf422gEvDTeJms23OJ+icYRWZ6ryNQ5W/ZsgKEe87M5HVYg=="], - - "fumadocs-mdx": ["fumadocs-mdx@15.1.0", "", { "dependencies": { "@mdx-js/mdx": "^3.1.1", "@standard-schema/spec": "^1.1.0", "chokidar": "^5.0.0", "esbuild": "^0.28.1", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", "js-yaml": "^5.2.1", "mdast-util-mdx": "^3.0.0", "picocolors": "^1.1.1", "picomatch": "^4.0.5", "tinyexec": "^1.2.4", "tinyglobby": "^0.2.17", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3", "zod": "^4.4.3" }, "peerDependencies": { "@fumadocs/satteri": "0.x.x", "@types/mdast": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "^16.7.0", "mdast-util-directive": "*", "next": "^15.3.0 || ^16.0.0", "react": "^19.2.0", "rolldown": "*", "satteri": "^0.9.4", "vite": "7.x.x || 8.x.x" }, "optionalPeers": ["@fumadocs/satteri", "@types/mdast", "@types/mdx", "@types/react", "mdast-util-directive", "next", "react", "rolldown", "satteri", "vite"], "bin": { "fumadocs-mdx": "./bin.js" } }, "sha512-2nDusSlYFuNVcyB51jgY3tA3r01ALTwoURrMDNoc7cbJKZ2sac/PW+CDq6SHTArkgRMmFiKYQGfspJdjgTtPTg=="], - - "fumadocs-ui": ["fumadocs-ui@16.11.1", "", { "dependencies": { "@fuma-translate/react": "^1.0.2", "@fumadocs/tailwind": "0.1.0", "@radix-ui/react-accordion": "^1.2.15", "@radix-ui/react-collapsible": "^1.1.15", "@radix-ui/react-dialog": "^1.1.18", "@radix-ui/react-direction": "^1.1.2", "@radix-ui/react-navigation-menu": "^1.2.17", "@radix-ui/react-popover": "^1.1.18", "@radix-ui/react-presence": "^1.1.6", "@radix-ui/react-scroll-area": "^1.2.13", "@radix-ui/react-slot": "^1.3.0", "@radix-ui/react-tabs": "^1.1.16", "class-variance-authority": "^0.7.1", "cnfast": "^0.0.8", "lucide-react": "^1.23.0", "motion": "^12.42.2", "next-themes": "^0.4.6", "react-remove-scroll": "^2.7.2", "rehype-raw": "^7.0.0", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^4.3.1", "unist-util-visit": "^5.1.0" }, "peerDependencies": { "@takumi-rs/image-response": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "16.11.1", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0" }, "optionalPeers": ["@takumi-rs/image-response", "@types/mdx", "@types/react", "next"] }, "sha512-Dq819PFV4RGhAI9Wd4erSCiRlEDLVOZae+kgE5LeOKFH8mbKX49U8N17ldFOhdkC9EZpxMZdEKul77RDgFHQww=="], - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "function.prototype.name": ["function.prototype.name@1.2.0", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2", "hasown": "^2.0.4", "is-callable": "^1.2.7", "is-document.all": "^1.0.0" } }, "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew=="], @@ -4158,8 +4112,6 @@ "gaxios": ["gaxios@7.2.0", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg=="], - "gcd": ["gcd@0.0.1", "", {}, "sha512-VNx3UEGr+ILJTiMs1+xc5SX1cMgJCrXezKPa003APUWNqQqaF6n25W8VcR7nHN6yRWbvvUTwCpZCFJeWC2kXlw=="], - "gcp-metadata": ["gcp-metadata@8.1.3", "", { "dependencies": { "gaxios": "7.1.3", "google-logging-utils": "1.1.3", "json-bigint": "^1.0.0" } }, "sha512-ziTrzUhhpL9Zk5k0HHzgP/KIpWDJT0VMBC/ynt/QIBvTW+UUcSivQRl6VlwTf/EilDxtSWklHoRsKy1c4k+59w=="], "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], @@ -4186,14 +4138,10 @@ "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], - "get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="], - "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#83c0a07", {}, "anomalyco-ghostty-web-83c0a07", "sha512-Lf2v1agHkVUpMpHBWWuCZrhOEmcwwin5/Hboc9rZwQ7/CKkIh5rU1r1CvfLlhkMoFv+ed8z52RZ8hkzGZZj3MQ=="], "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], - "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], - "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], "gitlab-ai-provider": ["gitlab-ai-provider@6.11.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-SJ6f5qa7P8md6lPrserryER3zerLkrezlnqqYQ2AbvDPpHLbwtbyk0FYJ5kNRcmbI80i/VMcsMBP0YIRdc3ucQ=="], @@ -4230,7 +4178,7 @@ "h3": ["h3@2.0.1-rc.4", "", { "dependencies": { "rou3": "^0.7.8", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-vZq8pEUp6THsXKXrUXX44eOqfChic2wVQ1GlSzQCBr7DeFBkfIZAo2WyNND4GSv54TAa0E4LYIK73WSPdgKUgw=="], - "h3-v2": ["h3@2.0.1-rc.20", "", { "dependencies": { "rou3": "^0.8.1", "srvx": "^0.11.13" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg=="], + "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], "happy-dom": ["happy-dom@20.10.6", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-6QD0ilzDDt93tX44y8tbmZdAcdTRYDhUP+Asgi6pC8Pp5IA3cvaZGyoVN/EGtlq9ziT65iPuBBn3ASLr6hCgVw=="], @@ -4252,12 +4200,8 @@ "hast-util-format": ["hast-util-format@1.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-minify-whitespace": "^1.0.0", "hast-util-phrasing": "^3.0.0", "hast-util-whitespace": "^3.0.0", "html-whitespace-sensitive-tag-names": "^3.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA=="], - "hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="], - "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], - "hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw=="], - "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], "hast-util-has-property": ["hast-util-has-property@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA=="], @@ -4284,8 +4228,6 @@ "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], - "hast-util-to-mdast": ["hast-util-to-mdast@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-phrasing": "^3.0.0", "hast-util-to-html": "^9.0.0", "hast-util-to-text": "^4.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "mdast-util-to-string": "^4.0.0", "rehype-minify-whitespace": "^6.0.0", "trim-trailing-lines": "^2.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-DsL/SvCK9V7+vfc6SLQ+vKIyBDXTk2KLSbfBYkH4zeF/uR1yBajHRhkzuaUSGOB1WJSTieJBdHwxlC+HLKvZZw=="], - "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], "hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="], @@ -4300,8 +4242,6 @@ "heap-snapshot-toolkit": ["heap-snapshot-toolkit@1.1.3", "", {}, "sha512-joThu2rEsDu8/l4arupRDI1qP4CZXNG+J6Wr348vnbLGSiBkwRdqZ6aOHl5BzEiC+Dc8OTbMlmWjD0lbXD5K2Q=="], - "hex-rgb": ["hex-rgb@5.0.0", "", {}, "sha512-NQO+lgVUCtHxZ792FodgW0zflK+ozS9X9dwGp9XvvmPlH7pyxd588cn24TD3rmPm/N0AIRXF10Otah8yKqGw4w=="], - "hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="], "hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], @@ -4346,8 +4286,6 @@ "i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="], - "ico-endec": ["ico-endec@0.1.6", "", {}, "sha512-ZdLU38ZoED3g1j3iEyzcQj+wAkY2xfWNkymszfJPoxucIUhK7NayQ+/C4Kv0nDFMIsbtbEHldv3V8PU494/ueQ=="], - "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -4356,9 +4294,9 @@ "ignore-walk": ["ignore-walk@8.0.0", "", { "dependencies": { "minimatch": "^10.0.3" } }, "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A=="], - "immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], + "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + "immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], "import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="], @@ -4372,14 +4310,8 @@ "ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="], - "ink": ["ink@6.3.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.0", "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.32.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^7.2.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^4.19.1" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-2CbJAa7XeziZYe6pDS5RVLirRY28iSGMQuEV8jRU5NQsONQNfcR/BZHHc9vkMg2lGYTHTM2pskxC1YmY28p6bQ=="], - - "ink-spinner": ["ink-spinner@5.0.0", "", { "dependencies": { "cli-spinners": "^2.7.0" }, "peerDependencies": { "ink": ">=4.0.0", "react": ">=18.0.0" } }, "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA=="], - "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], - "inquirer": ["inquirer@12.3.0", "", { "dependencies": { "@inquirer/core": "^10.1.2", "@inquirer/prompts": "^7.2.1", "@inquirer/type": "^3.0.2", "ansi-escapes": "^4.3.2", "mute-stream": "^2.0.0", "run-async": "^3.0.0", "rxjs": "^7.8.1" }, "peerDependencies": { "@types/node": ">=18" } }, "sha512-3NixUXq+hM8ezj2wc7wC37b32/rHq1MwNZDYdvx+d6jokOD+r+i8Q4Pkylh9tISYP114A128LCX8RKhopC5RfQ=="], - "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], @@ -4388,8 +4320,6 @@ "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], - "ip-regex": ["ip-regex@4.3.0", "", {}, "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q=="], - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], @@ -4444,14 +4374,10 @@ "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], - "is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="], - "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], - "is-ip": ["is-ip@3.1.0", "", { "dependencies": { "ip-regex": "^4.0.0" } }, "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q=="], - "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], @@ -4460,7 +4386,7 @@ "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], - "is-online": ["is-online@10.0.0", "", { "dependencies": { "got": "^12.1.0", "p-any": "^4.0.0", "p-timeout": "^5.1.0", "public-ip": "^5.0.0" } }, "sha512-WCPdKwNDjXJJmUubf2VHLMDBkUZEtuOvpXUfUnUFbEnM6In9ByiScL4f4jKACz/fsb2qDkesFerW3snf/AYz3A=="], + "is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], @@ -4502,8 +4428,6 @@ "isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="], - "isbot": ["isbot@5.2.1", "", {}, "sha512-dJ+LpKyClQZ7NG+j3OensC/mAZkGpukE9YUrgPYvAZj2doVL0edfDgywTUh5CXa0o+nW9a1V9e5+CJTX8+SxRw=="], - "isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], @@ -4532,8 +4456,6 @@ "jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="], - "jsep": ["jsep@1.4.0", "", {}, "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw=="], - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], @@ -4564,12 +4486,12 @@ "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="], - "jsonpath-plus": ["jsonpath-plus@10.4.0", "", { "dependencies": { "@jsep-plugin/assignment": "^1.3.0", "@jsep-plugin/regex": "^1.0.4", "jsep": "^1.4.0" }, "bin": { "jsonpath": "bin/jsonpath-cli.js", "jsonpath-plus": "bin/jsonpath-cli.js" } }, "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA=="], - "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], + "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], + "just-diff": ["just-diff@6.0.2", "", {}, "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA=="], "just-diff-apply": ["just-diff-apply@5.5.0", "", {}, "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw=="], @@ -4582,10 +4504,10 @@ "katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="], - "keytar": ["keytar@7.9.0", "", { "dependencies": { "node-addon-api": "^4.3.0", "prebuild-install": "^7.0.1" } }, "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ=="], - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], + "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], @@ -4598,16 +4520,18 @@ "language-map": ["language-map@1.5.0", "", {}, "sha512-n7gFZpe+DwEAX9cXVTw43i3wiudWDDtSn28RmdnS/HCPr284dQI/SztsamWanRr75oSlKSaGbV2nmWCTzGCoVg=="], + "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], + "lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="], "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="], - "lcm": ["lcm@0.0.3", "", { "dependencies": { "gcd": "^0.0.1" } }, "sha512-TB+ZjoillV6B26Vspf9l2L/vKaRY/4ep3hahcyVkCGFgsTNRUQdc24bQeNFiZeoxH0vr5+7SfNRMQuPHv/1IrQ=="], - "leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="], "leven": ["leven@4.1.0", "", {}, "sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew=="], + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], @@ -4640,6 +4564,8 @@ "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], + "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], @@ -4658,8 +4584,6 @@ "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], - "lodash.topath": ["lodash.topath@4.5.2", "", {}, "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg=="], - "loglevelnext": ["loglevelnext@6.0.0", "", {}, "sha512-FDl1AI2sJGjHHG3XKJd6sG3/6ncgiGCQ0YkW46nxe7SfqQq6hujd9CvFXIXtkGBUN83KPZ2KSOJK8q5P0bSSRQ=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -4680,8 +4604,6 @@ "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], - "lucide-react": ["lucide-react@1.25.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw=="], - "luxon": ["luxon@3.6.1", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], @@ -4716,8 +4638,6 @@ "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], - "mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="], - "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], @@ -4730,8 +4650,6 @@ "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], - "mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="], - "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], @@ -4760,6 +4678,8 @@ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "mermaid": ["mermaid@11.16.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.2.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.20", "dompurify": "^3.3.3", "es-toolkit": "^1.45.1", "katex": "^0.16.45", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA=="], + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], @@ -4768,8 +4688,6 @@ "micromark-extension-directive": ["micromark-extension-directive@3.0.2", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA=="], - "micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="], - "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], @@ -4784,8 +4702,6 @@ "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], - "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="], - "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.1", "", { "dependencies": { "@types/acorn": "^4.0.0", "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-vNuFb9czP8QCtAQcEJn0UJQJZA8Dk6DXKBqx+bg/w0WGuSxDxNr7hErW89tHUY31dUW4NqEOWwmEUNhjTFmHkg=="], @@ -4874,14 +4790,8 @@ "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], - "mint": ["mint@4.2.666", "", { "dependencies": { "@mintlify/cli": "4.0.1269" }, "bin": { "mint": "index.js" } }, "sha512-FsdL35EH++MiVDoKxN8M6/obOsrgx0Ko7P/1Y2lBXh/jEPx1UD1Y8msmAOW6cuwaqGIzAxuC7ySmr7D3G2bmBg=="], - - "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], - "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], - "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], - "morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="], "motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="], @@ -4904,8 +4814,6 @@ "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], - "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], - "mysql2": ["mysql2@3.14.4", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-Cs/jx3WZPNrYHVz+Iunp9ziahaG5uFMvD2R8Zlmc194AqXNxt9HBNu7ZsPYrUtmJsF0egETCWIdMIYAwOGjL1w=="], "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], @@ -4916,22 +4824,12 @@ "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], - "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], - "netmask": ["netmask@2.1.1", "", {}, "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA=="], - - "next-mdx-remote-client": ["next-mdx-remote-client@1.1.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "@types/mdx": "^2.0.13", "remark-mdx-remove-esm": "^1.3.2", "serialize-error": "^13.0.1", "vfile": "^6.0.3", "vfile-matter": "^5.0.1" }, "peerDependencies": { "react": ">= 18.3.0 < 19.0.0", "react-dom": ">= 18.3.0 < 19.0.0" } }, "sha512-IElOrn02JjGQZxx+re7wMx/1AUG+Arte9aDImAtxjAfMw6xuSCaH5mTCunKelkWzFyFdRb565jO8jRICvvh96g=="], - - "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], - "nf3": ["nf3@0.1.12", "", {}, "sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ=="], - "nimma": ["nimma@0.2.3", "", { "dependencies": { "@jsep-plugin/regex": "^1.0.1", "@jsep-plugin/ternary": "^1.0.2", "astring": "^1.8.1", "jsep": "^1.2.0" }, "optionalDependencies": { "jsonpath-plus": "^6.0.1 || ^10.1.0", "lodash.topath": "^4.5.2" } }, "sha512-1ZOI8J+1PKKGceo/5CT5GfQOG6H8I2BencSK06YarZ2wXwH37BSSUWldqJmMJYA5JfqDqffxDXynt6f11AyKcA=="], - "nitro": ["nitro@3.0.1-alpha.1", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.1", "db0": "^0.3.4", "h3": "2.0.1-rc.5", "jiti": "^2.6.1", "nf3": "^0.1.10", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.96.0", "oxc-transform": "^0.96.0", "srvx": "^0.9.5", "undici": "^7.16.0", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.4" }, "peerDependencies": { "rolldown": "*", "rollup": "^4", "vite": "^7", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-U4AxIsXxdkxzkFrK0XAw0e5Qbojk8jQ50MjjRBtBakC4HurTtQoiZvF+lSe382jhuQZCfAyywGWOFa9QzXLFaw=="], "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], @@ -4952,6 +4850,8 @@ "node-gyp": ["node-gyp@12.4.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw=="], + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], "node-html-parser": ["node-html-parser@7.1.0", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ=="], @@ -4962,8 +4862,6 @@ "node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="], - "non-error": ["non-error@0.1.0", "", {}, "sha512-TMB1uHiGsHRGv1uYclfhivcnf0/PdFp2pNqRxXjncaAsjYMoisaQJI+SSZCqRq+VliwRTC8tsMQfmrWjDMhkPQ=="], - "nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -4990,8 +4888,6 @@ "nypm": ["nypm@0.6.8", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.2.4" }, "bin": { "nypm": "./dist/cli.mjs" } }, "sha512-Q9K4Diu6l5u6xJQogeFSs/zKtyMSgFKFtRQV+tHP4kL7KPm2grpBU0dFIwFaXwNxN0MtfKWc43VpCugAa+LPsw=="], - "oauth4webapi": ["oauth4webapi@3.8.6", "", {}, "sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ=="], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], @@ -5030,6 +4926,10 @@ "opentui-spinner": ["opentui-spinner@0.0.7", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.3.4", "@opentui/react": "^0.3.4", "@opentui/solid": "^0.3.4", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-nPzwAvJG+y9rVEwwHLHqbsMzLnIk2zw+F9LqwA7aYJvpM5gsrKC2rrGi36A+tZpA+1RnWxXeWEgVZMchnaH18Q=="], + "os-paths": ["os-paths@4.4.0", "", {}, "sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg=="], + + "ow": ["ow@0.28.2", "", { "dependencies": { "@sindresorhus/is": "^4.2.0", "callsites": "^3.1.0", "dot-prop": "^6.0.1", "lodash.isequal": "^4.5.0", "vali-date": "^1.0.0" } }, "sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q=="], + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], "oxc-minify": ["oxc-minify@0.96.0", "", { "optionalDependencies": { "@oxc-minify/binding-android-arm64": "0.96.0", "@oxc-minify/binding-darwin-arm64": "0.96.0", "@oxc-minify/binding-darwin-x64": "0.96.0", "@oxc-minify/binding-freebsd-x64": "0.96.0", "@oxc-minify/binding-linux-arm-gnueabihf": "0.96.0", "@oxc-minify/binding-linux-arm-musleabihf": "0.96.0", "@oxc-minify/binding-linux-arm64-gnu": "0.96.0", "@oxc-minify/binding-linux-arm64-musl": "0.96.0", "@oxc-minify/binding-linux-riscv64-gnu": "0.96.0", "@oxc-minify/binding-linux-s390x-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-musl": "0.96.0", "@oxc-minify/binding-wasm32-wasi": "0.96.0", "@oxc-minify/binding-win32-arm64-msvc": "0.96.0", "@oxc-minify/binding-win32-x64-msvc": "0.96.0" } }, "sha512-dXeeGrfPJJ4rMdw+NrqiCRtbzVX2ogq//R0Xns08zql2HjV3Zi2SBJ65saqfDaJzd2bcHqvGWH+M44EQCHPAcA=="], @@ -5044,8 +4944,6 @@ "oxlint-tsgolint": ["oxlint-tsgolint@0.21.0", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.21.0", "@oxlint-tsgolint/darwin-x64": "0.21.0", "@oxlint-tsgolint/linux-arm64": "0.21.0", "@oxlint-tsgolint/linux-x64": "0.21.0", "@oxlint-tsgolint/win32-arm64": "0.21.0", "@oxlint-tsgolint/win32-x64": "0.21.0" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-HiWPhANwRnN1pZJQ2SgNB3WRR+1etLJHmRzQ/MJhyINsEIaOUCjxhlXJKbEaVUwdnyXwRWqo/P9Fx21lz0/mSg=="], - "p-any": ["p-any@4.0.0", "", { "dependencies": { "p-cancelable": "^3.0.0", "p-some": "^6.0.0" } }, "sha512-S/B50s+pAVe0wmEZHmBs/9yJXeZ5KhHzOsgKzt0hRdgkoR3DxW9ts46fcsWi/r3VnzsnkKS7q4uimze+zjdryw=="], - "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], "p-defer": ["p-defer@3.0.0", "", {}, "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw=="], @@ -5062,16 +4960,10 @@ "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], - "p-some": ["p-some@6.0.0", "", { "dependencies": { "aggregate-error": "^4.0.0", "p-cancelable": "^3.0.0" } }, "sha512-CJbQCKdfSX3fIh8/QKgS+9rjm7OBNUTmwWswAFQAhc8j1NR1dsEDETUEuVUtQHZpV+J03LqWBEwvu0g1Yn+TYg=="], - "p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="], "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], - "pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="], - - "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="], - "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], "package-manager-detector": ["package-manager-detector@1.7.0", "", {}, "sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ=="], @@ -5080,18 +4972,14 @@ "pagefind": ["pagefind@1.5.2", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.5.2", "@pagefind/darwin-x64": "1.5.2", "@pagefind/freebsd-x64": "1.5.2", "@pagefind/linux-arm64": "1.5.2", "@pagefind/linux-x64": "1.5.2", "@pagefind/windows-arm64": "1.5.2", "@pagefind/windows-x64": "1.5.2" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q=="], - "pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], "param-case": ["param-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A=="], - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - "parse-conflict-json": ["parse-conflict-json@5.0.1", "", { "dependencies": { "json-parse-even-better-errors": "^5.0.0", "just-diff": "^6.0.0", "just-diff-apply": "^5.2.0" } }, "sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ=="], "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], - "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], - "parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="], "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], @@ -5104,10 +4992,10 @@ "pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="], - "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], - "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-expression-matcher": ["path-expression-matcher@1.6.2", "", {}, "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ=="], @@ -5122,6 +5010,8 @@ "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + "path-to-regexp-updated": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -5162,7 +5052,9 @@ "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], - "pony-cause": ["pony-cause@1.1.1", "", {}, "sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g=="], + "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], + + "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], @@ -5184,8 +5076,6 @@ "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], - "posthog-node": ["posthog-node@5.17.2", "", { "dependencies": { "@posthog/core": "1.7.1" } }, "sha512-lz3YJOr0Nmiz0yHASaINEDHqoV+0bC3eD8aZAG+Ky292dAnVYul+ga/dMX8KCBXg8hHfKdxw0SztYD5j6dgUqQ=="], - "postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="], "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], @@ -5194,8 +5084,6 @@ "preact-render-to-string": ["preact-render-to-string@6.6.5", "", { "peerDependencies": { "preact": ">=10 || >= 11.0.0-0" } }, "sha512-O6MHzYNIKYaiSX3bOw0gGZfEbOmlIDtDfWwN1JJdc/T3ihzRT6tGGSEWE088dWrEDGa1u7101q+6fzQnO9XCPA=="], - "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], - "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], "pretty": ["pretty@2.0.0", "", { "dependencies": { "condense-newlines": "^0.2.1", "extend-shallow": "^2.0.1", "js-beautify": "^1.6.12" } }, "sha512-G9xUchgTEiNpormdYBl+Pha50gOUovT18IvAe7EYMZ1/f9W/WWMPRn+xI68yXNMUk3QXHDwo/1wV/4NejVNe1w=="], @@ -5208,6 +5096,8 @@ "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], + "process-ancestry": ["process-ancestry@0.1.0", "", {}, "sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg=="], + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], "proggy": ["proggy@4.0.0", "", {}, "sha512-MbA4R+WQT76ZBm/5JUpV9yqcJt92175+Y0Bodg3HgiXzrmKu7Ggq+bpn6y6wHH+gN9NcyKn3yg1+d47VaKwNAQ=="], @@ -5234,19 +5124,13 @@ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - "proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="], - "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], - "public-ip": ["public-ip@5.0.0", "", { "dependencies": { "dns-socket": "^4.2.2", "got": "^12.0.0", "is-ip": "^3.1.0" } }, "sha512-xaH3pZMni/R2BG7ZXXaWS9Wc9wFlhyDVJF47IJ+3ali0TGv+2PsckKxbmo+rnx3ZxiV2wblVhtdS3bohAP6GGw=="], - "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], - "pupa": ["pupa@3.3.0", "", { "dependencies": { "escape-goat": "^4.0.0" } }, "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA=="], - - "puppeteer": ["puppeteer@22.14.0", "", { "dependencies": { "@puppeteer/browsers": "2.3.0", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1312386", "puppeteer-core": "22.14.0" }, "bin": { "puppeteer": "lib/esm/puppeteer/node/cli.js" } }, "sha512-MGTR6/pM8zmWbTdazb6FKnwIihzsSEXBPH49mFFU96DNZpQOevCAZMnjBZGlZRGRzRK6aADCavR6SQtrbv5dQw=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "puppeteer-core": ["puppeteer-core@22.14.0", "", { "dependencies": { "@puppeteer/browsers": "2.3.0", "chromium-bidi": "0.6.2", "debug": "^4.3.5", "devtools-protocol": "0.0.1312386", "ws": "^8.18.0" } }, "sha512-rl4tOY5LcA3e374GAlsGGHc05HL3eGNf5rZ+uxkl6id9zVZKcwcp1Z+Nd6byb6WPiPeecT/dwz8f/iUm+AZQSw=="], + "pupa": ["pupa@3.3.0", "", { "dependencies": { "escape-goat": "^4.0.0" } }, "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA=="], "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="], @@ -5268,8 +5152,6 @@ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], - "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], "react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="], @@ -5278,11 +5160,9 @@ "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], - "react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="], - "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], - "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + "react-remove-scroll": ["react-remove-scroll@2.5.5", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.3", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", "use-sidecar": "^1.1.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], @@ -5338,10 +5218,6 @@ "rehype-format": ["rehype-format@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-format": "^1.0.0" } }, "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ=="], - "rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="], - - "rehype-minify-whitespace": ["rehype-minify-whitespace@6.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-minify-whitespace": "^1.0.0" } }, "sha512-Zk0pyQ06A3Lyxhe9vGtOtzz3Z0+qZ5+7icZ/PL/2x1SHPbKao5oB/g/rlc6BCTajqBb33JcOe71Ye1oFsuYbnw=="], - "rehype-parse": ["rehype-parse@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="], "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], @@ -5352,20 +5228,12 @@ "relateurl": ["relateurl@0.2.7", "", {}, "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog=="], - "remark": ["remark@15.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A=="], - "remark-directive": ["remark-directive@3.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-directive": "^3.0.0", "micromark-extension-directive": "^3.0.0", "unified": "^11.0.0" } }, "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A=="], - "remark-frontmatter": ["remark-frontmatter@5.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-frontmatter": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0", "unified": "^11.0.0" } }, "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ=="], - "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], - "remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="], - "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], - "remark-mdx-remove-esm": ["remark-mdx-remove-esm@1.3.2", "", { "dependencies": { "@types/mdast": "^4.0.4", "unist-util-remove": "^4.0.0" }, "peerDependencies": { "unified": "^11" } }, "sha512-BvL8VSdVXy9S7NlHP56nUJAHFc45h5E9HnHiLUGHe5tw3Yvm/3cVZvAzlkEEh2i+fkq2uKrf2xn5VmItBhMypA=="], - "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], @@ -5402,8 +5270,6 @@ "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], - "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], - "restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="], "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="], @@ -5422,21 +5288,23 @@ "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], + "robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], + "rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="], "rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="], "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], + "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], - "run-async": ["run-async@3.0.0", "", {}, "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q=="], - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], "s-js": ["s-js@0.4.9", "", {}, "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ=="], @@ -5448,18 +5316,16 @@ "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], - "safe-stable-stringify": ["safe-stable-stringify@1.1.1", "", {}, "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw=="], - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "sanitize-filename": ["sanitize-filename@1.6.4", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg=="], + "satteri": ["satteri@0.9.5", "", { "dependencies": { "@types/estree-jsx": "^1.0.5", "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "@types/unist": "^3.0.3" }, "optionalDependencies": { "@bruits/satteri-darwin-arm64": "0.9.5", "@bruits/satteri-darwin-x64": "0.9.5", "@bruits/satteri-linux-arm64-gnu": "0.9.5", "@bruits/satteri-linux-arm64-musl": "0.9.5", "@bruits/satteri-linux-x64-gnu": "0.9.5", "@bruits/satteri-linux-x64-musl": "0.9.5", "@bruits/satteri-wasm32-wasi": "0.9.5", "@bruits/satteri-win32-arm64-msvc": "0.9.5", "@bruits/satteri-win32-x64-msvc": "0.9.5" } }, "sha512-ZuWVl+vnM64y+/TtX8Kosv2c00W+hLQiiwnEL6H0UKVVrxFqMw4D2CJHHQaouVd89OAhtBBfjWLqhKi3TVUV4w=="], + "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], - "scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="], - "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="], "selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="], @@ -5480,18 +5346,20 @@ "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + "server-destroy": ["server-destroy@1.0.1", "", {}, "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ=="], + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], - "sharp-ico": ["sharp-ico@0.1.5", "", { "dependencies": { "decode-ico": "*", "ico-endec": "*", "sharp": "*" } }, "sha512-a3jODQl82NPp1d5OYb0wY+oFaPk7AvyxipIowCHk7pBsZCWgbe0yAkU2OOXdoH0ENyANhyOQbs9xkAiRHcF02Q=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], @@ -5514,9 +5382,7 @@ "sigstore": ["sigstore@4.1.1", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0", "@sigstore/sign": "^4.1.1", "@sigstore/tuf": "^4.0.2", "@sigstore/verify": "^3.1.1" } }, "sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w=="], - "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], - - "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + "simple-icons": ["simple-icons@13.21.0", "", {}, "sha512-LI5pVJPBv6oc79OMsffwb6kEqnmB8P1Cjg1crNUlhsxPETQ5UzbCKQdxU+7MW6+DD1qfPkla/vSKlLD4IfyXpQ=="], "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], @@ -5530,14 +5396,12 @@ "slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], + "slugify": ["slugify@1.6.9", "", {}, "sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg=="], + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], "smol-toml": ["smol-toml@1.7.0", "", {}, "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ=="], - "socket.io": ["socket.io@4.8.0", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA=="], - - "socket.io-adapter": ["socket.io-adapter@2.5.8", "", { "dependencies": { "debug": "~4.4.1", "ws": "~8.21.0" } }, "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw=="], - "socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="], "socket.io-parser": ["socket.io-parser@4.2.7", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg=="], @@ -5610,8 +5474,6 @@ "sst-win32-x86": ["sst-win32-x86@4.13.1", "", { "os": "win32", "cpu": "none" }, "sha512-YPxBVdac/MsrzwlC6pF0NrrvMcmfdBLYjv7MbzHc5jNh1FQ1WPh6bdWQqgv0KD9EQTNLLEkej0beydgUvcCWJg=="], - "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], @@ -5660,8 +5522,6 @@ "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], - "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - "stripe": ["stripe@18.0.0", "", { "dependencies": { "@types/node": ">=8.1.0", "qs": "^6.11.0" } }, "sha512-3Fs33IzKUby//9kCkCa1uRpinAoTvj6rJgQ2jrBEysoxEvfsclvXdna1amyEYbA2EKkjynuB4+L/kleCCaWTpA=="], "strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="], @@ -5674,6 +5534,8 @@ "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + "stylis": ["stylis@4.4.0", "", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="], + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], @@ -5686,18 +5548,20 @@ "sury": ["sury@11.0.0-alpha.4", "", { "peerDependencies": { "rescript": "12.x" }, "optionalPeers": ["rescript"] }, "sha512-oeG/GJWZvQCKtGPpLbu0yCZudfr5LxycDo5kh7SJmKHDPCsEPJssIZL2Eb4Tl7g9aPEvIDuRrkS+L0pybsMEMA=="], + "svgo": ["svgo@4.0.2", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng=="], + "system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="], "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], "tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="], + "takumi-js": ["takumi-js@2.4.1", "", { "dependencies": { "@takumi-rs/core": "2.4.1", "@takumi-rs/helpers": "2.4.1", "@takumi-rs/wasm": "2.4.1" } }, "sha512-8+iPvoxWp7ur962zvsb+kNK80HlnMjYGYjf6+oNIfcN8n91or3Zct48KlOmpdsWkK1ZWSWvxS1BqL1TcJigrbg=="], + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], "tar": ["tar@7.5.20", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ=="], - "tar-fs": ["tar-fs@2.1.5", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw=="], - "tar-stream": ["tar-stream@3.2.0", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg=="], "teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="], @@ -5716,8 +5580,6 @@ "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], - "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], - "tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="], "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], @@ -5728,6 +5590,8 @@ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + "tinyclip": ["tinyclip@0.1.15", "", {}, "sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A=="], + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], @@ -5742,8 +5606,6 @@ "tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="], - "to-data-view": ["to-data-view@1.1.0", "", {}, "sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ=="], - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "toad-cache": ["toad-cache@3.7.4", "", {}, "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg=="], @@ -5764,8 +5626,6 @@ "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], - "trim-trailing-lines": ["trim-trailing-lines@2.1.0", "", {}, "sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg=="], - "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="], @@ -5786,8 +5646,6 @@ "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], - "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], - "turbo": ["turbo@2.10.2", "", { "optionalDependencies": { "@turbo/darwin-64": "2.10.2", "@turbo/darwin-arm64": "2.10.2", "@turbo/linux-64": "2.10.2", "@turbo/linux-arm64": "2.10.2", "@turbo/windows-64": "2.10.2", "@turbo/windows-arm64": "2.10.2" }, "bin": { "turbo": "bin/turbo" } }, "sha512-wTExrNrRjB8qzIcg+ZLm0A3GFNLDsWNwdS/RBXB0FPrBDyzk3i96Yx+TxWZC7a0k1SIreFB8ciUbxjmEqTH8IQ=="], "turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="], @@ -5828,8 +5686,6 @@ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - "unbzip2-stream": ["unbzip2-stream@1.4.3", "", { "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" } }, "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg=="], - "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], "undici": ["undici@8.7.0", "", {}, "sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ=="], @@ -5846,22 +5702,16 @@ "unifont": ["unifont@0.5.2", "", { "dependencies": { "css-tree": "^3.0.0", "ofetch": "^1.4.1", "ohash": "^2.0.0" } }, "sha512-LzR4WUqzH9ILFvjLAUU7dK3Lnou/qd5kD+IakBtBK4S15/+x2y9VX+DcWQv6s551R6W+vzwgVS6tFg3XggGBgg=="], - "unist-builder": ["unist-builder@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg=="], - "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], - "unist-util-map": ["unist-util-map@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-HJs1tpkSmRJUzj6fskQrS5oYhBYlmtcvy4SepdDEEsL04FjBrgF0Mgggvxc1/qGBGgW7hRh9+UBK1aqTEnBpIA=="], - "unist-util-modify-children": ["unist-util-modify-children@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" } }, "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw=="], "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], - "unist-util-remove": ["unist-util-remove@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg=="], - "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], @@ -5894,9 +5744,7 @@ "uqr": ["uqr@0.1.3", "", {}, "sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA=="], - "urijs": ["urijs@1.19.11", "", {}, "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ=="], - - "urlpattern-polyfill": ["urlpattern-polyfill@10.0.0", "", {}, "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], @@ -5908,12 +5756,12 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "utility-types": ["utility-types@3.11.0", "", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="], - "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], "uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="], + "vali-date": ["vali-date@1.0.0", "", {}, "sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg=="], + "valibot": ["valibot@1.4.2", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg=="], "validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="], @@ -5926,8 +5774,6 @@ "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], - "vfile-matter": ["vfile-matter@5.0.1", "", { "dependencies": { "vfile": "^6.0.0", "yaml": "^2.0.0" } }, "sha512-o6roP82AiX0XfkyTHyRCMXgHfltUNlXSEqCIS80f+mbAyiQBE2fxtDVMtseyytGx75sihiJFo/zR6r/4LTs2Cw=="], - "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], "vite": ["vite@7.1.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw=="], @@ -6032,20 +5878,20 @@ "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], + "xdg-app-paths": ["xdg-app-paths@5.5.1", "", { "dependencies": { "os-paths": "^4.0.1", "xdg-portable": "^7.2.0" } }, "sha512-hI3flOB4PLZIy5prbtTpirobtPE2ZtZ52szO+2mM9Efp6ErM398La+C1lIpNWDfNoQk+6Lsi6nMcCwVB7pxeMQ=="], + "xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="], + "xdg-portable": ["xdg-portable@7.3.0", "", { "dependencies": { "os-paths": "^4.0.1" } }, "sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw=="], + "xml-naming": ["xml-naming@0.3.0", "", {}, "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ=="], "xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], - "xmlbuilder2": ["xmlbuilder2@4.0.3", "", { "dependencies": { "@oozcitak/dom": "^2.0.2", "@oozcitak/infra": "^2.0.2", "@oozcitak/util": "^10.0.0", "js-yaml": "^4.1.1" } }, "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA=="], - "xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="], - "xss": ["xss@1.0.15", "", { "dependencies": { "commander": "^2.20.3", "cssfilter": "0.0.10" }, "bin": { "xss": "bin/xss" } }, "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg=="], - "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -6068,10 +5914,6 @@ "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], - "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], - - "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], - "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], @@ -6176,9 +6018,7 @@ "@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bjYld/2KGPLt78kpqbya+fD4LYS7BqVQJyUjE3qAHrYB0FR2Q90BaWEVIBZaguTWXf/A8L6uG1zO1v9TxVlGWg=="], - "@alcalzone/ansi-tokenize/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "@alcalzone/ansi-tokenize/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + "@antfu/install-pkg/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], "@astrojs/cloudflare/vite": ["vite@6.4.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="], @@ -6190,21 +6030,37 @@ "@astrojs/markdown-remark/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], + "@astrojs/markdown-satteri/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.1", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q=="], + + "@astrojs/markdown-satteri/@astrojs/prism": ["@astrojs/prism@4.0.2", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA=="], + "@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.11", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.6", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.21.0", "smol-toml": "^1.6.0", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-hcaxX/5aC6lQgHeGh1i+aauvSwIT6cfyFjKWvExYSxUhZZBBdvCliOtu06gbQyhbe0pGJNoNmqNlQZ5zYUuIyQ=="], "@astrojs/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "@astrojs/node/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.1", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q=="], + + "@astrojs/node/astro": ["astro@7.1.3", "", { "dependencies": { "@astrojs/compiler-rs": "^0.3.1", "@astrojs/internal-helpers": "0.10.1", "@astrojs/markdown-satteri": "0.3.4", "@astrojs/telemetry": "3.3.3", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "am-i-vibing": "^0.4.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^2.0.1", "devalue": "^5.8.1", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.28.0", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "jsonc-parser": "^3.3.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^1.0.1", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unstorage": "^1.17.5", "vite": "^8.0.13", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0 || ^0.35.0" }, "peerDependencies": { "@astrojs/markdown-remark": "7.2.1" }, "optionalPeers": ["@astrojs/markdown-remark"], "bin": { "astro": "./bin/astro.mjs" } }, "sha512-4dhPyAAXthf3xLEYnG8SeL7yr/nTPPABfY7e9YF0yuO+vK9Xp+8Q5j4xzsmL3GueukQv4oNwGNTBepLOiDGeJA=="], + + "@astrojs/react/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.1", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q=="], + + "@astrojs/react/@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], + + "@astrojs/react/vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="], + "@astrojs/sitemap/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "@astrojs/solid-js/vite": ["vite@6.4.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="], "@astrojs/starlight/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], - "@asyncapi/parser/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + "@astrojs/vercel/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.1", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q=="], + + "@astrojs/vercel/@vercel/analytics": ["@vercel/analytics@1.6.1", "", { "peerDependencies": { "@remix-run/react": "^2", "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@remix-run/react", "@sveltejs/kit", "next", "react", "svelte", "vue", "vue-router"] }, "sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg=="], - "@asyncapi/parser/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], + "@astrojs/vercel/astro": ["astro@7.1.3", "", { "dependencies": { "@astrojs/compiler-rs": "^0.3.1", "@astrojs/internal-helpers": "0.10.1", "@astrojs/markdown-satteri": "0.3.4", "@astrojs/telemetry": "3.3.3", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "am-i-vibing": "^0.4.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^2.0.1", "devalue": "^5.8.1", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.28.0", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "jsonc-parser": "^3.3.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^1.0.1", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unstorage": "^1.17.5", "vite": "^8.0.13", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0 || ^0.35.0" }, "peerDependencies": { "@astrojs/markdown-remark": "7.2.1" }, "optionalPeers": ["@astrojs/markdown-remark"], "bin": { "astro": "./bin/astro.mjs" } }, "sha512-4dhPyAAXthf3xLEYnG8SeL7yr/nTPPABfY7e9YF0yuO+vK9Xp+8Q5j4xzsmL3GueukQv4oNwGNTBepLOiDGeJA=="], - "@asyncapi/parser/node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="], + "@astrojs/vercel/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], "@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], @@ -6368,10 +6224,6 @@ "@expressive-code/plugin-shiki/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], - "@fuma-translate/react/react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], - - "@fuma-translate/react/react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], - "@hey-api/json-schema-ref-parser/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], "@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], @@ -6380,10 +6232,6 @@ "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], - "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], - - "@jsx-email/cli/@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], - "@jsx-email/cli/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "@jsx-email/cli/esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="], @@ -6398,127 +6246,9 @@ "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], - "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - - "@mintlify/cli/chalk": ["chalk@5.2.0", "", {}, "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA=="], - - "@mintlify/cli/fs-extra": ["fs-extra@11.2.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw=="], - - "@mintlify/cli/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "@mintlify/cli/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], - - "@mintlify/cli/openid-client": ["openid-client@6.8.2", "", { "dependencies": { "jose": "^6.1.3", "oauth4webapi": "^3.8.4" } }, "sha512-uOvTCndr4udZsKihJ68H9bUICrriHdUVJ6Az+4Ns6cW55rwM5h0bjVIzDz2SxgOI84LKjFyjOFvERLzdTUROGA=="], - - "@mintlify/cli/react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], - - "@mintlify/cli/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "@mintlify/cli/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], - - "@mintlify/cli/yargs": ["yargs@17.7.1", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw=="], - - "@mintlify/cli/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - - "@mintlify/common/acorn": ["acorn@8.11.2", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w=="], - - "@mintlify/common/hast-util-to-html": ["hast-util-to-html@9.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA=="], - - "@mintlify/common/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "@mintlify/common/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], - - "@mintlify/common/mdast-util-gfm": ["mdast-util-gfm@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw=="], - - "@mintlify/common/mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.1.3", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ=="], - - "@mintlify/common/postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], - - "@mintlify/common/remark-gfm": ["remark-gfm@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA=="], - - "@mintlify/common/remark-mdx": ["remark-mdx@3.1.0", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA=="], - - "@mintlify/common/remark-rehype": ["remark-rehype@11.1.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ=="], - - "@mintlify/common/sucrase": ["sucrase@3.34.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "7.1.6", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw=="], - - "@mintlify/common/tailwindcss": ["tailwindcss@3.4.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.6", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og=="], - - "@mintlify/common/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], - - "@mintlify/common/unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="], - - "@mintlify/link-rot/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="], - - "@mintlify/link-rot/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], - - "@mintlify/mdx/@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.12", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.3", "@radix-ui/react-portal": "1.1.13", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jkrTdQVxnIB8fpn0NyyxW9CTB5aCXZZelVz5z+Xmii6g5WxMqS3fInNslZ63puP39+Puu4jYohUK31y3dT87gQ=="], - - "@mintlify/mdx/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], - - "@mintlify/mdx/react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], - - "@mintlify/mdx/react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], - - "@mintlify/mdx/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], + "@mapbox/node-pre-gyp/nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="], - "@mintlify/models/axios": ["axios@1.16.1", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A=="], - - "@mintlify/prebuild/chalk": ["chalk@5.3.0", "", {}, "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="], - - "@mintlify/prebuild/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="], - - "@mintlify/prebuild/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "@mintlify/prebuild/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], - - "@mintlify/prebuild/uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="], - - "@mintlify/previewing/chalk": ["chalk@5.2.0", "", {}, "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA=="], - - "@mintlify/previewing/chokidar": ["chokidar@3.5.3", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw=="], - - "@mintlify/previewing/express": ["express@4.22.0", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-c2iPh3xp5vvCLgaHK03+mWLFPhox7j1LwyxcZwFVApEv5i0X+IjPpbT50SJJwwLpdBVfp45AkK/v+AFgv/XlfQ=="], - - "@mintlify/previewing/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="], - - "@mintlify/previewing/got": ["got@13.0.0", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA=="], - - "@mintlify/previewing/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "@mintlify/previewing/react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], - - "@mintlify/previewing/tar": ["tar@7.5.15", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ=="], - - "@mintlify/previewing/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], - - "@mintlify/previewing/yargs": ["yargs@17.7.1", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw=="], - - "@mintlify/scraping/fs-extra": ["fs-extra@11.1.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ=="], - - "@mintlify/scraping/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "@mintlify/scraping/mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.1.3", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ=="], - - "@mintlify/scraping/remark-gfm": ["remark-gfm@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA=="], - - "@mintlify/scraping/remark-mdx": ["remark-mdx@3.0.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-3Pz3yPQ5Rht2pM5R+0J2MrGoBSrzf+tJG94N+t/ilfdh8YLyyKYtidAYwTveB20BoHAcwIopOUqhcmh2F7hGYA=="], - - "@mintlify/scraping/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], - - "@mintlify/scraping/yargs": ["yargs@17.7.1", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw=="], - - "@mintlify/scraping/zod": ["zod@3.24.0", "", {}, "sha512-Hz+wiY8yD0VLA2k/+nsg2Abez674dDGTai33SwNvMPuf9uIrBC9eFgIMQxBBbHFxVXi8W+5nX9DcAh9YNSQm/w=="], - - "@mintlify/validation/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "@mintlify/validation/object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - - "@mintlify/validation/uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="], - - "@mintlify/validation/zod": ["zod@3.24.0", "", {}, "sha512-Hz+wiY8yD0VLA2k/+nsg2Abez674dDGTai33SwNvMPuf9uIrBC9eFgIMQxBBbHFxVXi8W+5nX9DcAh9YNSQm/w=="], - - "@mintlify/validation/zod-to-json-schema": ["zod-to-json-schema@3.20.4", "", { "peerDependencies": { "zod": "^3.20.0" } }, "sha512-Un9+kInJ2Zt63n6Z7mLqBifzzPcOyX+b+Exuzf7L1+xqck9Q2EPByyTRduV3kmSPaXaRer1JCsucubpgL1fipg=="], + "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "@modelcontextprotocol/sdk/hono": ["hono@4.12.30", "", {}, "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog=="], @@ -6620,17 +6350,9 @@ "@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/www/@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.44.0", "", { "dependencies": { "@cloudflare/unenv-preset": "2.16.1", "miniflare": "4.20260708.1", "unenv": "2.0.0-rc.24", "wrangler": "4.110.0", "ws": "8.21.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0 || ^8.0.0" }, "bin": { "cf-vite": "bin/cf-vite" } }, "sha512-8wGGunqRcs34o4GRq0Rurp7GZg30xtLJeRGUU81a49r9zQRjlp3xIlsWr3nFlSCso4eE3cjZfiKC/2y116M4TQ=="], - - "@opencode-ai/www/@tailwindcss/vite": ["@tailwindcss/vite@4.3.2", "", { "dependencies": { "@tailwindcss/node": "4.3.2", "@tailwindcss/oxide": "4.3.2", "tailwindcss": "4.3.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA=="], - - "@opencode-ai/www/react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], - - "@opencode-ai/www/react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], + "@opencode-ai/www/@astrojs/cloudflare": ["@astrojs/cloudflare@14.1.4", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.1", "@astrojs/underscore-redirects": "1.0.3", "@cloudflare/vite-plugin": "^1.39.0", "piccolore": "^0.1.3", "vite": "^8.0.13" }, "peerDependencies": { "astro": "^7.0.0", "wrangler": "^4.83.0" } }, "sha512-Zyo1E/5/dmegmKODbwUzOd67euNd6oKcbllhAwe3uFFprA7mIFNkpF6yBBa78vIkpMNuU13MkBJHCLd+yJSuEA=="], - "@opencode-ai/www/tailwindcss": ["tailwindcss@4.3.2", "", {}, "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA=="], - - "@opencode-ai/www/vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="], + "@opencode-ai/www/astro": ["astro@7.1.3", "", { "dependencies": { "@astrojs/compiler-rs": "^0.3.1", "@astrojs/internal-helpers": "0.10.1", "@astrojs/markdown-satteri": "0.3.4", "@astrojs/telemetry": "3.3.3", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "am-i-vibing": "^0.4.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^2.0.1", "devalue": "^5.8.1", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.28.0", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "jsonc-parser": "^3.3.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^1.0.1", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unstorage": "^1.17.5", "vite": "^8.0.13", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0 || ^0.35.0" }, "peerDependencies": { "@astrojs/markdown-remark": "7.2.1" }, "optionalPeers": ["@astrojs/markdown-remark"], "bin": { "astro": "./bin/astro.mjs" } }, "sha512-4dhPyAAXthf3xLEYnG8SeL7yr/nTPPABfY7e9YF0yuO+vK9Xp+8Q5j4xzsmL3GueukQv4oNwGNTBepLOiDGeJA=="], "@opencode-ai/www/wrangler": ["wrangler@4.110.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "4.20260708.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260708.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260708.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-xZeXKYi7hxQRF5anL+v77RkufJNpF9f3Eqeyqq2QBsETpLZgh0Agj0jJ6JPtkbgn6ukZdh8OK5egsGPWIditgg=="], @@ -6664,289 +6386,145 @@ "@pierre/trees/react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], + "@poppinss/dumper/@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], + "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], "@protobuf-ts/plugin/typescript": ["typescript@3.9.10", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q=="], - "@puppeteer/browsers/tar-fs": ["tar-fs@3.1.3", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ=="], + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - "@radix-ui/react-accordion/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="], + "@scalar/types/nanoid": ["nanoid@5.1.16", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="], - "@radix-ui/react-accordion/@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-opfXRe6nnzyGmCDPx+l1Aqo/RbqWtQal2FnsBqF9hhePp6j0LsRoBaRxcMOlTv+uYTJVtWYZKg9t9wTe+BA/ZA=="], + "@scalar/types/type-fest": ["type-fest@5.8.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA=="], - "@radix-ui/react-accordion/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + "@scalar/types/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "@radix-ui/react-accordion/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], + "@sentry/bundler-plugin-core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], - "@radix-ui/react-accordion/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], + "@sentry/bundler-plugin-core/magic-string": ["magic-string@0.30.8", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ=="], - "@radix-ui/react-accordion/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], + "@sentry/cli/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], - "@radix-ui/react-accordion/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], + "@sentry/cli/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], - "@radix-ui/react-collapsible/@radix-ui/react-presence": ["@radix-ui/react-presence@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg=="], + "@sentry/cli/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "@radix-ui/react-collection/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + "@shikijs/engine-javascript/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], - "@radix-ui/react-collection/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], + "@shikijs/engine-oniguruma/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], - "@radix-ui/react-collection/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], + "@shikijs/langs/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], - "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + "@shikijs/primitive/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], - "@radix-ui/react-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="], + "@shikijs/stream/@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="], - "@radix-ui/react-dialog/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + "@shikijs/themes/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], - "@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], + "@shikijs/twoslash/@shikijs/core": ["@shikijs/core@4.3.1", "", { "dependencies": { "@shikijs/primitive": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA=="], - "@radix-ui/react-dialog/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-effect-event": "0.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA=="], + "@shikijs/twoslash/@shikijs/types": ["@shikijs/types@4.3.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="], - "@radix-ui/react-dialog/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q=="], + "@slack/bolt/express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], - "@radix-ui/react-dialog/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.12", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jjk/lqTeNL0azUx5ZYzVrl4NgaDIrdzTNE4mABV9yBFI7FQqN7pIgzV1bTleUezP2QiTGA1BFTqY8MegDgWX9A=="], + "@slack/bolt/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], - "@radix-ui/react-dialog/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], + "@slack/bolt/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], - "@radix-ui/react-dialog/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.13", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA=="], + "@slack/oauth/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], - "@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], + "@slack/socket-mode/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], - "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + "@slack/socket-mode/@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="], - "@radix-ui/react-dialog/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], + "@slack/socket-mode/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], - "@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="], + "@slack/web-api/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], - "@radix-ui/react-focus-scope/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="], + "@slack/web-api/eventemitter3": ["eventemitter3@3.1.2", "", {}, "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="], - "@radix-ui/react-navigation-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="], + "@slack/web-api/form-data": ["form-data@2.5.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA=="], - "@radix-ui/react-navigation-menu/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + "@slack/web-api/p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="], - "@radix-ui/react-navigation-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], + "@solidjs/start/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], - "@radix-ui/react-navigation-menu/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-effect-event": "0.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA=="], + "@solidjs/start/seroval": ["seroval@1.5.5", "", {}, "sha512-bSjOuPcwPKLSJNhr9+bZxA20nQxVle5J5MNsYRVE6cIg7KpRLXGupymePavu0jrxlPiPsr4xGZSB8yUY2sH2sw=="], - "@radix-ui/react-navigation-menu/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], + "@solidjs/start/seroval-plugins": ["seroval-plugins@1.5.5", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-+BDhqYM6CEn3x09v44dpa9p6974FuUB2dxk+Ctn04k0cO1Zt6QODTXfmEZK0eBaTe/fJBvP4NMGuNJ+R8T+QMg=="], - "@radix-ui/react-navigation-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], + "@solidjs/start/shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="], - "@radix-ui/react-navigation-menu/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], + "@solidjs/start/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="], - "@radix-ui/react-navigation-menu/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "@storybook/addon-docs/react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], - "@radix-ui/react-navigation-menu/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1wNZBggTDK3GRuuQ6nP4k2yi7a6l7I5qbMPbZcRsrGsGVead/f/d5FhEzUvqFs0bcrDLx7n1zKQ3JvLR6whaaw=="], + "@storybook/addon-docs/react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], - "@radix-ui/react-popover/@radix-ui/react-presence": ["@radix-ui/react-presence@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg=="], + "@storybook/csf-plugin/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], - "@radix-ui/react-popover/react-remove-scroll": ["react-remove-scroll@2.5.5", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.3", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", "use-sidecar": "^1.1.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw=="], + "@tailwindcss/node/lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], - "@radix-ui/react-popper/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], - "@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], - "@radix-ui/react-roving-focus/@radix-ui/react-collection": ["@radix-ui/react-collection@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-slot": "1.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], - "@radix-ui/react-roving-focus/@radix-ui/react-direction": ["@radix-ui/react-direction@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], - "@radix-ui/react-roving-focus/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="], + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], - "@radix-ui/react-scroll-area/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="], + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@radix-ui/react-scroll-area/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + "@tailwindcss/typography/postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], - "@radix-ui/react-scroll-area/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], + "@tanstack/directive-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - "@radix-ui/react-scroll-area/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], + "@tanstack/router-utils/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], - "@radix-ui/react-scroll-area/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "@tanstack/server-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - "@radix-ui/react-tabs/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="], + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], - "@radix-ui/react-tabs/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], + "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], - "@radix-ui/react-tabs/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], + "@vercel/cli-config/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="], - "@radix-ui/react-tabs/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], + "@vercel/cli-exec/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], - "@radix-ui/react-tabs/@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-is-hydrated": "0.1.1", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-40svmmugfM3mUN7VUDGVE1tQGOhyi8enlGD0CNJEcMM36C1f71PKM21DFgNHUfem0XnA+d8H8oN3Z9ZpJjSslg=="], + "@vercel/functions/@vercel/oidc": ["@vercel/oidc@3.8.0", "", { "dependencies": { "@vercel/cli-config": "0.2.0", "@vercel/cli-exec": "1.0.0", "jose": "^5.9.6" } }, "sha512-r00laGW6Pv778RoR6M2NxX91ycSj+PBwVo+fOb9Bif+F0IyUKt25zrvBzfEzQpeAzbqOgPZyQibEWDdDFApd+A=="], - "@radix-ui/react-tabs/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], + "@vercel/nft/acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], - "@radix-ui/react-toggle-group/@radix-ui/react-direction": ["@radix-ui/react-direction@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA=="], + "@vercel/nft/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - "@radix-ui/react-tooltip/@radix-ui/react-presence": ["@radix-ui/react-presence@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg=="], + "@vercel/routing-utils/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], - "@radix-ui/react-use-controllable-state/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="], + "@vercel/routing-utils/path-to-regexp": ["path-to-regexp@6.1.0", "", {}, "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw=="], - "@radix-ui/react-use-effect-event/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], - "@radix-ui/react-use-escape-keydown/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="], + "@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], - "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - "@sentry/bundler-plugin-core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], + "@vitest/mocker/@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="], - "@sentry/bundler-plugin-core/magic-string": ["magic-string@0.30.8", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ=="], + "@vscode/emmet-helper/jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="], - "@sentry/cli/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + "@vscode/emmet-helper/vscode-languageserver-types": ["vscode-languageserver-types@3.18.0", "", {}, "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g=="], - "@sentry/cli/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "ai-gateway-provider/@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZXe7nZQgliDdjz5ufH5RKpHWxbN72AzmzzKGbF/z+0K9GN5tUCnftrQRvTRFHA5jAzTapcm2BEevmGLVbMkW+A=="], - "@sentry/cli/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="], - "@shikijs/engine-javascript/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], + "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@shikijs/engine-oniguruma/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], + "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "@shikijs/langs/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], - - "@shikijs/primitive/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], - - "@shikijs/stream/@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="], - - "@shikijs/themes/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], - - "@shikijs/twoslash/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], - - "@shikijs/twoslash/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], - - "@slack/bolt/express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], - - "@slack/bolt/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], - - "@slack/bolt/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], - - "@slack/oauth/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], - - "@slack/socket-mode/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], - - "@slack/socket-mode/@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="], - - "@slack/socket-mode/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], - - "@slack/web-api/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], - - "@slack/web-api/eventemitter3": ["eventemitter3@3.1.2", "", {}, "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="], - - "@slack/web-api/form-data": ["form-data@2.5.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA=="], - - "@slack/web-api/p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="], - - "@solidjs/start/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], - - "@solidjs/start/seroval": ["seroval@1.5.5", "", {}, "sha512-bSjOuPcwPKLSJNhr9+bZxA20nQxVle5J5MNsYRVE6cIg7KpRLXGupymePavu0jrxlPiPsr4xGZSB8yUY2sH2sw=="], - - "@solidjs/start/seroval-plugins": ["seroval-plugins@1.5.5", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-+BDhqYM6CEn3x09v44dpa9p6974FuUB2dxk+Ctn04k0cO1Zt6QODTXfmEZK0eBaTe/fJBvP4NMGuNJ+R8T+QMg=="], - - "@solidjs/start/shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="], - - "@solidjs/start/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="], - - "@stoplight/better-ajv-errors/leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], - - "@stoplight/json/jsonc-parser": ["jsonc-parser@2.2.1", "", {}, "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w=="], - - "@stoplight/json-ref-readers/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], - - "@stoplight/json-ref-resolver/immer": ["immer@9.0.21", "", {}, "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA=="], - - "@stoplight/spectral-core/@stoplight/types": ["@stoplight/types@13.6.0", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ=="], - - "@stoplight/spectral-core/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], - - "@stoplight/spectral-core/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - - "@stoplight/spectral-functions/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], - - "@stoplight/spectral-parsers/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="], - - "@stoplight/yaml/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="], - - "@storybook/addon-docs/react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], - - "@storybook/addon-docs/react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], - - "@storybook/csf-plugin/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], - - "@tailwindcss/node/lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], - - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], - - "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], - - "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@tanstack/directive-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - - "@tanstack/directive-functions-plugin/@tanstack/router-utils": ["@tanstack/router-utils@1.133.19", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.5", "@babel/preset-typescript": "^7.27.1", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-WEp5D2gPxvlLDRXwD/fV7RXjYtqaqJNXKB/L6OyZEbT+9BG/Ib2d7oG9GSUZNNMGPGYAlhBUOi3xutySsk6rxA=="], - - "@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], - - "@tanstack/router-core/seroval": ["seroval@1.5.5", "", {}, "sha512-bSjOuPcwPKLSJNhr9+bZxA20nQxVle5J5MNsYRVE6cIg7KpRLXGupymePavu0jrxlPiPsr4xGZSB8yUY2sH2sw=="], - - "@tanstack/router-core/seroval-plugins": ["seroval-plugins@1.5.5", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-+BDhqYM6CEn3x09v44dpa9p6974FuUB2dxk+Ctn04k0cO1Zt6QODTXfmEZK0eBaTe/fJBvP4NMGuNJ+R8T+QMg=="], - - "@tanstack/router-generator/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - - "@tanstack/router-plugin/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - - "@tanstack/router-plugin/unplugin": ["unplugin@3.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.4", "webpack-virtual-modules": "^0.6.2" }, "peerDependencies": { "@farmfe/core": "*", "@rspack/core": "*", "bun-types-no-globals": "*", "esbuild": "*", "rolldown": "*", "rollup": "*", "unloader": "*", "vite": "*", "webpack": "*" }, "optionalPeers": ["@farmfe/core", "@rspack/core", "bun-types-no-globals", "esbuild", "rolldown", "rollup", "unloader", "vite", "webpack"] }, "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg=="], - - "@tanstack/router-plugin/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - - "@tanstack/router-utils/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], - - "@tanstack/server-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - - "@tanstack/start-client-core/seroval": ["seroval@1.5.5", "", {}, "sha512-bSjOuPcwPKLSJNhr9+bZxA20nQxVle5J5MNsYRVE6cIg7KpRLXGupymePavu0jrxlPiPsr4xGZSB8yUY2sH2sw=="], - - "@tanstack/start-plugin-core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - - "@tanstack/start-plugin-core/seroval": ["seroval@1.5.5", "", {}, "sha512-bSjOuPcwPKLSJNhr9+bZxA20nQxVle5J5MNsYRVE6cIg7KpRLXGupymePavu0jrxlPiPsr4xGZSB8yUY2sH2sw=="], - - "@tanstack/start-plugin-core/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - - "@tanstack/start-plugin-core/srvx": ["srvx@0.11.22", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ=="], - - "@tanstack/start-plugin-core/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - - "@tanstack/start-server-core/seroval": ["seroval@1.5.5", "", {}, "sha512-bSjOuPcwPKLSJNhr9+bZxA20nQxVle5J5MNsYRVE6cIg7KpRLXGupymePavu0jrxlPiPsr4xGZSB8yUY2sH2sw=="], - - "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], - - "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], - - "@vitejs/plugin-react/vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="], - - "@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], - - "@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - - "@vitest/mocker/@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="], - - "@vscode/emmet-helper/jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="], - - "@vscode/emmet-helper/vscode-languageserver-types": ["vscode-languageserver-types@3.18.0", "", {}, "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g=="], - - "aggregate-error/indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], - - "ai-gateway-provider/@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZXe7nZQgliDdjz5ufH5RKpHWxbN72AzmzzKGbF/z+0K9GN5tUCnftrQRvTRFHA5jAzTapcm2BEevmGLVbMkW+A=="], - - "ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="], - - "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - - "app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="], + "app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="], "app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], @@ -6984,11 +6562,41 @@ "babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], - "better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + "blume/@astrojs/mdx": ["@astrojs/mdx@7.0.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.1", "@astrojs/markdown-remark": "7.2.1", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.16.0", "es-module-lexer": "^2.0.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@astrojs/markdown-satteri": "^0.3.1", "astro": "^7.0.0" }, "optionalPeers": ["@astrojs/markdown-satteri"] }, "sha512-RxyIwU0uFam5ftwqKOjpIdhnFxZ/kEikeimLyQy3eGXbHT8WgRGzzesOIHVU8+m9TY8ag5WVOyvV24/GyqPdPQ=="], + + "blume/@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="], + + "blume/@pierre/diffs": ["@pierre/diffs@1.2.12", "", { "dependencies": { "@pierre/theme": "1.1.0", "@pierre/theming": "0.0.2", "@shikijs/transformers": "^3.0.0 || ^4.0.0", "diff": "9.0.0", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0 || ^4.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-pY/gmgWL03WnagqCyCnBi3QtRXUv4hCIY6FYqd5b1ZGaoI6a4Bsji8j+yRl2RfzPh/8Hf19rCl1GE80G6a1cLQ=="], + + "blume/@shikijs/transformers": ["@shikijs/transformers@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/types": "4.3.1" } }, "sha512-z6ir0bGDgWcF2FduktEfPgIsdOtIlDiLAjFBgBzE42Q9xHbkkIXZtORHzlLVB71iZP9elEcqKg6keajvOUwE2A=="], + + "blume/@tailwindcss/vite": ["@tailwindcss/vite@4.3.2", "", { "dependencies": { "@tailwindcss/node": "4.3.2", "@tailwindcss/oxide": "4.3.2", "tailwindcss": "4.3.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA=="], + + "blume/ai": ["ai@5.0.216", "", { "dependencies": { "@ai-sdk/gateway": "2.0.115", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.30", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W1QgTQS/2itHasAjqx0CVWfRojmnu1V4tJbqaHJf5hXckAZa9IrPDS9VNWBSgZjBhBE9//5M+D4FKdvAGcQBEw=="], + + "blume/astro": ["astro@7.1.3", "", { "dependencies": { "@astrojs/compiler-rs": "^0.3.1", "@astrojs/internal-helpers": "0.10.1", "@astrojs/markdown-satteri": "0.3.4", "@astrojs/telemetry": "3.3.3", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "am-i-vibing": "^0.4.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^2.0.1", "devalue": "^5.8.1", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.28.0", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "jsonc-parser": "^3.3.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^1.0.1", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unstorage": "^1.17.5", "vite": "^8.0.13", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0 || ^0.35.0" }, "peerDependencies": { "@astrojs/markdown-remark": "7.2.1" }, "optionalPeers": ["@astrojs/markdown-remark"], "bin": { "astro": "./bin/astro.mjs" } }, "sha512-4dhPyAAXthf3xLEYnG8SeL7yr/nTPPABfY7e9YF0yuO+vK9Xp+8Q5j4xzsmL3GueukQv4oNwGNTBepLOiDGeJA=="], + + "blume/dompurify": ["dompurify@3.4.12", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg=="], + + "blume/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], + + "blume/katex": ["katex@0.17.0", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw=="], + + "blume/marked": ["marked@18.0.7", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-iDVQ5ldaiKXn6b2JroX5kgRfmwgqolW7NpaEzTl1k/2Zh1njIEN9yniyLV/mOvWwtsE8OGgkjsCYvijuPk1dtA=="], + + "blume/node-html-parser": ["node-html-parser@9.0.0", "", { "dependencies": { "css-select": "^5.1.0", "entities": "^8.0.0" } }, "sha512-MhdaHPyxnyYu/sf0TpiRvDnTrkum0UKHC7FdbDGIUQNlx3I7xzwXoyV0eMUMv/XU+lkJT1glOUzpDPq7b2p1Ew=="], - "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "blume/react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], - "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "blume/react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], + + "blume/shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="], + + "blume/tailwindcss": ["tailwindcss@4.3.2", "", {}, "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA=="], + + "blume/typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + + "blume/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], @@ -7000,8 +6608,6 @@ "c12/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], - "chromium-bidi/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="], - "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -7018,13 +6624,19 @@ "config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - "cosmiconfig/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], - "cosmiconfig/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], + "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], - "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], - "degenerator/ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], + "d3-dsv/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], + + "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], "dir-compare/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], @@ -7056,11 +6668,17 @@ "electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], - "engine.io/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + "epub-gen-memory/css-select": ["css-select@4.3.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", "domhandler": "^4.3.1", "domutils": "^2.8.0", "nth-check": "^2.0.1" } }, "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ=="], + + "epub-gen-memory/dom-serializer": ["dom-serializer@1.4.1", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" } }, "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag=="], + + "epub-gen-memory/domhandler": ["domhandler@4.3.1", "", { "dependencies": { "domelementtype": "^2.2.0" } }, "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ=="], - "engine.io/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + "epub-gen-memory/domutils": ["domutils@2.8.0", "", { "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", "domhandler": "^4.2.0" } }, "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A=="], - "error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + "epub-gen-memory/htmlparser2": ["htmlparser2@7.2.0", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.2", "domutils": "^2.8.0", "entities": "^3.0.1" } }, "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog=="], + + "epub-gen-memory/mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], "esbuild-plugin-copy/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -7074,8 +6692,6 @@ "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "favicons/xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], - "fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], @@ -7084,87 +6700,31 @@ "fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - "fumadocs-core/js-yaml": ["js-yaml@5.2.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.mjs" } }, "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw=="], - - "fumadocs-core/shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="], - - "fumadocs-mdx/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - - "fumadocs-mdx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], - - "fumadocs-mdx/js-yaml": ["js-yaml@5.2.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.mjs" } }, "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw=="], - - "fumadocs-mdx/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], - - "fumadocs-mdx/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - - "fumadocs-ui/@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-opfXRe6nnzyGmCDPx+l1Aqo/RbqWtQal2FnsBqF9hhePp6j0LsRoBaRxcMOlTv+uYTJVtWYZKg9t9wTe+BA/ZA=="], - - "fumadocs-ui/@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.12", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.3", "@radix-ui/react-portal": "1.1.13", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jkrTdQVxnIB8fpn0NyyxW9CTB5aCXZZelVz5z+Xmii6g5WxMqS3fInNslZ63puP39+Puu4jYohUK31y3dT87gQ=="], - - "fumadocs-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], - - "fumadocs-ui/motion": ["motion@12.42.2", "", { "dependencies": { "framer-motion": "^12.42.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q=="], - - "fumadocs-ui/react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], - - "fumadocs-ui/react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], - - "fumadocs-ui/shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="], - "gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], "gcp-metadata/gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], "gcp-metadata/google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], - "get-uri/data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], - "gitlab-ai-provider/openai": ["openai@6.48.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-KhVp+FyV50QrXNextvL9hIU5l6ox5HYuKQjGVk7lIqprgJol90+dQXWONV6S1lRWsKA1bXjrow8RsUT14M1hNA=="], "gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - "got/@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], - - "h3-v2/rou3": ["rou3@0.8.1", "", {}, "sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA=="], - - "h3-v2/srvx": ["srvx@0.11.22", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ=="], - "html-minifier-terser/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], "html-minifier-terser/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - - "ink/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "ink/indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], - - "ink/react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], - - "ink/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - - "ink/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], - - "ink-spinner/cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - - "inquirer/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], - - "is-online/got": ["got@12.6.1", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ=="], - - "is-online/p-timeout": ["p-timeout@5.1.0", "", {}, "sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew=="], - "js-beautify/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "js-beautify/nopt": ["nopt@7.2.1", "", { "dependencies": { "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w=="], - "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + "jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "keytar/node-addon-api": ["node-addon-api@4.3.0", "", {}, "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="], + "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], @@ -7172,6 +6732,12 @@ "md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="], + "mermaid/dompurify": ["dompurify@3.4.12", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg=="], + + "mermaid/katex": ["katex@0.16.47", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="], + + "mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "miniflare/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], @@ -7188,12 +6754,6 @@ "motion/framer-motion": ["framer-motion@12.42.2", "", { "dependencies": { "motion-dom": "^12.42.2", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw=="], - "next-mdx-remote-client/react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], - - "next-mdx-remote-client/react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], - - "next-mdx-remote-client/serialize-error": ["serialize-error@13.0.1", "", { "dependencies": { "non-error": "^0.1.0", "type-fest": "^5.4.1" } }, "sha512-bBZaRwLH9PN5HbLCjPId4dP5bNGEtumcErgOX952IsvOhVPrm3/AeK1y0UHA/QaPG701eg0yEnOKsCOC6X/kaA=="], - "nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="], "nitro/undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], @@ -7212,20 +6772,16 @@ "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - "oxc-parser/@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], + "ow/dot-prop": ["dot-prop@6.0.1", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA=="], - "p-any/p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], + "oxc-parser/@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], - "p-some/p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], - "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "parse-json/json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], - "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], @@ -7246,8 +6802,6 @@ "postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], - "prebuild-install/node-abi": ["node-abi@3.94.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g=="], - "pretty-format/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], @@ -7256,24 +6810,8 @@ "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], - - "proxy-agent/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], - - "public-ip/got": ["got@12.6.1", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ=="], - - "rc/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - - "react-reconciler/react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], - - "react-reconciler/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], - "readdir-glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], - "restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - - "restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "roarr/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], @@ -7282,8 +6820,6 @@ "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], - "sharp-ico/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], - "shiki/@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="], "shiki/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], @@ -7292,10 +6828,6 @@ "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], - "socket.io/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], - - "socket.io/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - "solid-transition-size/@corvu/utils": ["@corvu/utils@0.3.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.7" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-ZWlyWEE8qV9+CB9OAyo2bTrZGXQN9ZeM+JfYv89zoR+lRACKTDuoOZEdiyL8Uc7U5dUSH1uTqKhTTnaHWb+wZA=="], "sort-keys/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], @@ -7304,8 +6836,6 @@ "sst/jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="], - "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], - "storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -7316,11 +6846,9 @@ "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - - "tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + "svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], - "tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], "temp/rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="], @@ -7336,7 +6864,7 @@ "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "unbzip2-stream/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "unicode-trie/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], "unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], @@ -7380,10 +6908,6 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "xmlbuilder2/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], - - "xss/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - "yaml-language-server/prettier": ["prettier@3.9.5", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg=="], "yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="], @@ -7426,6 +6950,10 @@ "@astrojs/markdown-remark/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + "@astrojs/markdown-satteri/@astrojs/internal-helpers/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], + + "@astrojs/markdown-satteri/@astrojs/internal-helpers/shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="], + "@astrojs/mdx/@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.6", "", {}, "sha512-GOle7smBWKfMSP8osUIGOlB5kaHdQLV3foCsf+5Q9Wsuu+C6Fs3Ez/ttXmhjZ1HkSgsogcM1RXSjjOVieHq16Q=="], "@astrojs/mdx/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="], @@ -7434,363 +6962,331 @@ "@astrojs/mdx/@astrojs/markdown-remark/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], - "@astrojs/starlight/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "@astrojs/node/@astrojs/internal-helpers/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], - "@asyncapi/parser/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "@astrojs/node/@astrojs/internal-helpers/shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="], - "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@astrojs/node/astro/@astrojs/telemetry": ["@astrojs/telemetry@3.3.3", "", { "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", "package-manager-detector": "^1.6.0" } }, "sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ=="], - "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@astrojs/node/astro/@capsizecss/unpack": ["@capsizecss/unpack@4.0.1", "", { "dependencies": { "fontkitten": "^1.0.3" } }, "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ=="], - "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@astrojs/node/astro/@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], + "@astrojs/node/astro/cookie": ["cookie@2.0.1", "", {}, "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + "@astrojs/node/astro/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], - "@aws-sdk/client-lambda/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], + "@astrojs/node/astro/es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], - "@aws-sdk/client-lambda/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + "@astrojs/node/astro/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], - "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.775.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6ESVxwCbGm7WZ17kY1fjmxQud43vzJFoLd4bmlR+idQSWdqlzGDYdcfzpjDKTcivdtNrVYmFvcH1JBUwCRAZhw=="], + "@astrojs/node/astro/fontace": ["fontace@0.4.1", "", { "dependencies": { "fontkitten": "^1.0.2" } }, "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw=="], - "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.775.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/types": "3.775.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/util-stream": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-PjDQeDH/J1S0yWV32wCj2k5liRo0ssXMseCBEkCsD3SqsU8o5cU82b0hMX4sAib/RkglCSZqGO0xMiN0/7ndww=="], + "@astrojs/node/astro/get-tsconfig": ["get-tsconfig@5.0.0-beta.4", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ=="], - "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.782.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/credential-provider-env": "3.775.0", "@aws-sdk/credential-provider-http": "3.775.0", "@aws-sdk/credential-provider-process": "3.775.0", "@aws-sdk/credential-provider-sso": "3.782.0", "@aws-sdk/credential-provider-web-identity": "3.782.0", "@aws-sdk/nested-clients": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/credential-provider-imds": "^4.0.2", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-wd4KdRy2YjLsE4Y7pz00470Iip06GlRHkG4dyLW7/hFMzEO2o7ixswCWp6J2VGZVAX64acknlv2Q0z02ebjmhw=="], + "@astrojs/node/astro/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], - "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.775.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-A6k68H9rQp+2+7P7SGO90Csw6nrUEm0Qfjpn9Etc4EboZhhCLs9b66umUsTsSBHus4FDIe5JQxfCUyt1wgNogg=="], + "@astrojs/node/astro/magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="], - "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.782.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.782.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/token-providers": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-1y1ucxTtTIGDSNSNxriQY8msinilhe9gGvQpUDYW9gboyC7WQJPDw66imy258V6osdtdi+xoHzVCbCz3WhosMQ=="], + "@astrojs/node/astro/neotraverse": ["neotraverse@1.0.1", "", {}, "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w=="], - "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.782.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/nested-clients": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-xCna0opVPaueEbJoclj5C6OpDNi0Gynj+4d7tnuXGgQhTHPyAz8ZyClkVqpi5qvHTgxROdUEDxWqEO5jqRHZHQ=="], + "@astrojs/node/astro/p-limit": ["p-limit@7.3.1", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-0trZaiG7Y7kN/Egy9a8j47t9osC0Tch4PaIWd9yGF6bvmlk7muExRvGNYb8sXBwEKMoNKsbNN9P8EefuQekE4Q=="], - "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], + "@astrojs/node/astro/p-queue": ["p-queue@9.3.3", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA=="], - "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + "@astrojs/node/astro/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], - "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], + "@astrojs/node/astro/shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="], - "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + "@astrojs/node/astro/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], - "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], + "@astrojs/node/astro/unifont": ["unifont@0.7.4", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg=="], - "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + "@astrojs/node/astro/unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], + "@astrojs/node/astro/vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + "@astrojs/node/astro/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="], + "@astrojs/node/astro/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Qzq7zj9yXUgAAJEbbmqRhm0jmUndl8nHG0AbxFEfCfQRVZWL96Qzx0mf8lYwT9hIMrXncLwy31HOthmbXwFRwQ=="], + "@astrojs/react/@astrojs/internal-helpers/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="], + "@astrojs/react/@astrojs/internal-helpers/shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="], - "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], + "@astrojs/react/@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], - "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + "@astrojs/react/@vitejs/plugin-react/react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], + "@astrojs/react/vite/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + "@astrojs/starlight/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], + "@astrojs/vercel/@astrojs/internal-helpers/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], - "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + "@astrojs/vercel/@astrojs/internal-helpers/shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="], - "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], + "@astrojs/vercel/astro/@astrojs/telemetry": ["@astrojs/telemetry@3.3.3", "", { "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", "package-manager-detector": "^1.6.0" } }, "sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ=="], - "@aws-sdk/credential-providers/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + "@astrojs/vercel/astro/@capsizecss/unpack": ["@capsizecss/unpack@4.0.1", "", { "dependencies": { "fontkitten": "^1.0.3" } }, "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ=="], - "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], + "@astrojs/vercel/astro/@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="], - "@aws-sdk/nested-clients/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + "@astrojs/vercel/astro/cookie": ["cookie@2.0.1", "", {}, "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w=="], - "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], + "@astrojs/vercel/astro/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], - "@aws-sdk/token-providers/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + "@astrojs/vercel/astro/es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], - "@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.4.1", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg=="], + "@astrojs/vercel/astro/fontace": ["fontace@0.4.1", "", { "dependencies": { "fontkitten": "^1.0.2" } }, "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw=="], - "@azure/core-xml/fast-xml-parser/strnum": ["strnum@2.4.1", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg=="], + "@astrojs/vercel/astro/get-tsconfig": ["get-tsconfig@5.0.0-beta.4", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ=="], - "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@astrojs/vercel/astro/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], - "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.16", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw=="], + "@astrojs/vercel/astro/magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="], - "@electron/fuses/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + "@astrojs/vercel/astro/neotraverse": ["neotraverse@1.0.1", "", {}, "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w=="], - "@electron/notarize/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + "@astrojs/vercel/astro/p-limit": ["p-limit@7.3.1", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-0trZaiG7Y7kN/Egy9a8j47t9osC0Tch4PaIWd9yGF6bvmlk7muExRvGNYb8sXBwEKMoNKsbNN9P8EefuQekE4Q=="], - "@electron/universal/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + "@astrojs/vercel/astro/p-queue": ["p-queue@9.3.3", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA=="], - "@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.1.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA=="], + "@astrojs/vercel/astro/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], - "@electron/windows-sign/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + "@astrojs/vercel/astro/shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="], - "@expressive-code/plugin-shiki/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], + "@astrojs/vercel/astro/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], - "@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], + "@astrojs/vercel/astro/unifont": ["unifont@0.7.4", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg=="], - "@expressive-code/plugin-shiki/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], + "@astrojs/vercel/astro/unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="], - "@expressive-code/plugin-shiki/shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], + "@astrojs/vercel/astro/vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="], - "@expressive-code/plugin-shiki/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], + "@astrojs/vercel/astro/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], - "@expressive-code/plugin-shiki/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + "@astrojs/vercel/astro/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "@fuma-translate/react/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "@astrojs/vercel/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], - "@hey-api/json-schema-ref-parser/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "@astrojs/vercel/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], - "@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "@astrojs/vercel/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], - "@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@astrojs/vercel/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], - "@jsx-email/cli/@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], + "@astrojs/vercel/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], - "@jsx-email/cli/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], + "@astrojs/vercel/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], - "@jsx-email/cli/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], + "@astrojs/vercel/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], - "@jsx-email/cli/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="], + "@astrojs/vercel/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], - "@jsx-email/cli/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="], + "@astrojs/vercel/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], - "@jsx-email/cli/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="], + "@astrojs/vercel/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], - "@jsx-email/cli/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="], + "@astrojs/vercel/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], - "@jsx-email/cli/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="], + "@astrojs/vercel/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], - "@jsx-email/cli/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="], + "@astrojs/vercel/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], - "@jsx-email/cli/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="], + "@astrojs/vercel/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], - "@jsx-email/cli/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="], + "@astrojs/vercel/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], - "@jsx-email/cli/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="], + "@astrojs/vercel/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], - "@jsx-email/cli/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="], + "@astrojs/vercel/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], - "@jsx-email/cli/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="], + "@astrojs/vercel/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], - "@jsx-email/cli/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="], + "@astrojs/vercel/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], - "@jsx-email/cli/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="], + "@astrojs/vercel/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], - "@jsx-email/cli/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="], + "@astrojs/vercel/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], - "@jsx-email/cli/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="], + "@astrojs/vercel/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], - "@jsx-email/cli/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="], + "@astrojs/vercel/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], - "@jsx-email/cli/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="], + "@astrojs/vercel/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], - "@jsx-email/cli/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="], + "@astrojs/vercel/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], - "@jsx-email/cli/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="], - - "@jsx-email/cli/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="], - - "@jsx-email/cli/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], - - "@jsx-email/cli/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + "@astrojs/vercel/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], - "@jsx-email/cli/tailwindcss/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "@jsx-email/cli/tailwindcss/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], - - "@jsx-email/cli/tailwindcss/object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - - "@jsx-email/cli/vite/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], - - "@jsx-email/cli/vite/rollup": ["rollup@3.30.0", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA=="], - - "@jsx-email/doiuse-email/htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - - "@malept/flatpak-bundler/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - - "@mintlify/cli/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - - "@mintlify/cli/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "@mintlify/cli/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], - - "@mintlify/cli/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], - - "@mintlify/cli/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - - "@mintlify/cli/openid-client/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], - - "@mintlify/cli/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], - "@mintlify/common/hast-util-to-html/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], - "@mintlify/common/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], - "@mintlify/common/mdast-util-gfm/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], - "@mintlify/common/mdast-util-mdx-jsx/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@mintlify/common/remark-gfm/mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + "@aws-sdk/client-lambda/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], - "@mintlify/common/sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "@aws-sdk/client-lambda/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@mintlify/common/sucrase/glob": ["glob@7.1.6", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA=="], + "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.775.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6ESVxwCbGm7WZ17kY1fjmxQud43vzJFoLd4bmlR+idQSWdqlzGDYdcfzpjDKTcivdtNrVYmFvcH1JBUwCRAZhw=="], - "@mintlify/common/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.775.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/types": "3.775.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/util-stream": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-PjDQeDH/J1S0yWV32wCj2k5liRo0ssXMseCBEkCsD3SqsU8o5cU82b0hMX4sAib/RkglCSZqGO0xMiN0/7ndww=="], - "@mintlify/common/tailwindcss/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.782.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/credential-provider-env": "3.775.0", "@aws-sdk/credential-provider-http": "3.775.0", "@aws-sdk/credential-provider-process": "3.775.0", "@aws-sdk/credential-provider-sso": "3.782.0", "@aws-sdk/credential-provider-web-identity": "3.782.0", "@aws-sdk/nested-clients": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/credential-provider-imds": "^4.0.2", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-wd4KdRy2YjLsE4Y7pz00470Iip06GlRHkG4dyLW7/hFMzEO2o7ixswCWp6J2VGZVAX64acknlv2Q0z02ebjmhw=="], - "@mintlify/common/tailwindcss/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.775.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-A6k68H9rQp+2+7P7SGO90Csw6nrUEm0Qfjpn9Etc4EboZhhCLs9b66umUsTsSBHus4FDIe5JQxfCUyt1wgNogg=="], - "@mintlify/common/tailwindcss/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.782.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.782.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/token-providers": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-1y1ucxTtTIGDSNSNxriQY8msinilhe9gGvQpUDYW9gboyC7WQJPDw66imy258V6osdtdi+xoHzVCbCz3WhosMQ=="], - "@mintlify/common/tailwindcss/object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.782.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/nested-clients": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-xCna0opVPaueEbJoclj5C6OpDNi0Gynj+4d7tnuXGgQhTHPyAz8ZyClkVqpi5qvHTgxROdUEDxWqEO5jqRHZHQ=="], - "@mintlify/common/tailwindcss/postcss": ["postcss@8.5.19", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ=="], + "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], - "@mintlify/common/tailwindcss/sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@mintlify/common/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], - "@mintlify/link-rot/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@mintlify/link-rot/unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], - "@mintlify/link-rot/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@mintlify/link-rot/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], + "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], - "@mintlify/mdx/@radix-ui/react-popover/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="], + "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@mintlify/mdx/@radix-ui/react-popover/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="], - "@mintlify/mdx/@radix-ui/react-popover/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Qzq7zj9yXUgAAJEbbmqRhm0jmUndl8nHG0AbxFEfCfQRVZWL96Qzx0mf8lYwT9hIMrXncLwy31HOthmbXwFRwQ=="], - "@mintlify/mdx/@radix-ui/react-popover/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-effect-event": "0.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="], - "@mintlify/mdx/@radix-ui/react-popover/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q=="], + "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], - "@mintlify/mdx/@radix-ui/react-popover/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.12", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jjk/lqTeNL0azUx5ZYzVrl4NgaDIrdzTNE4mABV9yBFI7FQqN7pIgzV1bTleUezP2QiTGA1BFTqY8MegDgWX9A=="], + "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@mintlify/mdx/@radix-ui/react-popover/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], - "@mintlify/mdx/@radix-ui/react-popover/@radix-ui/react-popper": ["@radix-ui/react-popper@1.3.3", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.11", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-rect": "1.1.2", "@radix-ui/react-use-size": "1.1.2", "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mS7dGpyjv6b+gsDjLF7e0ia1W4Im1B1hSCy2yuXlHuvnZxHKagfDaobt/KAKt27EpZMit2pss8eJBVyVjEWM+g=="], + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@mintlify/mdx/@radix-ui/react-popover/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.13", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA=="], + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], - "@mintlify/mdx/@radix-ui/react-popover/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@mintlify/mdx/@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], - "@mintlify/mdx/@radix-ui/react-popover/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], + "@aws-sdk/credential-providers/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@mintlify/mdx/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], + "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], - "@mintlify/mdx/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], + "@aws-sdk/nested-clients/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@mintlify/mdx/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], - "@mintlify/mdx/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], + "@aws-sdk/token-providers/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@mintlify/mdx/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], + "@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.4.1", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg=="], - "@mintlify/mdx/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], + "@azure/core-xml/fast-xml-parser/strnum": ["strnum@2.4.1", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg=="], - "@mintlify/mdx/shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "@mintlify/mdx/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], + "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.16", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw=="], - "@mintlify/mdx/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + "@electron/fuses/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - "@mintlify/models/axios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + "@electron/notarize/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - "@mintlify/prebuild/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + "@electron/universal/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - "@mintlify/prebuild/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.1.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA=="], - "@mintlify/prebuild/unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "@electron/windows-sign/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - "@mintlify/prebuild/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], + "@expressive-code/plugin-shiki/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], - "@mintlify/prebuild/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], + "@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], - "@mintlify/previewing/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "@expressive-code/plugin-shiki/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], - "@mintlify/previewing/express/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + "@expressive-code/plugin-shiki/shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], - "@mintlify/previewing/express/body-parser": ["body-parser@1.20.6", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g=="], + "@expressive-code/plugin-shiki/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], - "@mintlify/previewing/express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + "@expressive-code/plugin-shiki/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], - "@mintlify/previewing/express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + "@hey-api/json-schema-ref-parser/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "@mintlify/previewing/express/cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], + "@jsx-email/cli/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], - "@mintlify/previewing/express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "@jsx-email/cli/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], - "@mintlify/previewing/express/finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], + "@jsx-email/cli/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="], - "@mintlify/previewing/express/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + "@jsx-email/cli/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="], - "@mintlify/previewing/express/merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + "@jsx-email/cli/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="], - "@mintlify/previewing/express/path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], + "@jsx-email/cli/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="], - "@mintlify/previewing/express/qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], + "@jsx-email/cli/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="], - "@mintlify/previewing/express/range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + "@jsx-email/cli/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="], - "@mintlify/previewing/express/send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + "@jsx-email/cli/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="], - "@mintlify/previewing/express/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + "@jsx-email/cli/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="], - "@mintlify/previewing/express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + "@jsx-email/cli/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="], - "@mintlify/previewing/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + "@jsx-email/cli/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="], - "@mintlify/previewing/got/@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="], + "@jsx-email/cli/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="], - "@mintlify/previewing/got/@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="], + "@jsx-email/cli/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="], - "@mintlify/previewing/got/cacheable-lookup": ["cacheable-lookup@7.0.0", "", {}, "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w=="], + "@jsx-email/cli/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="], - "@mintlify/previewing/got/cacheable-request": ["cacheable-request@10.2.14", "", { "dependencies": { "@types/http-cache-semantics": "^4.0.2", "get-stream": "^6.0.1", "http-cache-semantics": "^4.1.1", "keyv": "^4.5.3", "mimic-response": "^4.0.0", "normalize-url": "^8.0.0", "responselike": "^3.0.0" } }, "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ=="], + "@jsx-email/cli/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="], - "@mintlify/previewing/got/form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="], + "@jsx-email/cli/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="], - "@mintlify/previewing/got/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + "@jsx-email/cli/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="], - "@mintlify/previewing/got/http2-wrapper": ["http2-wrapper@2.2.1", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" } }, "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ=="], + "@jsx-email/cli/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="], - "@mintlify/previewing/got/lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="], + "@jsx-email/cli/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="], - "@mintlify/previewing/got/p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], + "@jsx-email/cli/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="], - "@mintlify/previewing/got/responselike": ["responselike@3.0.0", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg=="], + "@jsx-email/cli/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="], - "@mintlify/previewing/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "@jsx-email/cli/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], - "@mintlify/previewing/tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "@jsx-email/cli/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - "@mintlify/previewing/unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "@jsx-email/cli/tailwindcss/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - "@mintlify/previewing/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], + "@jsx-email/cli/tailwindcss/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], - "@mintlify/previewing/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], + "@jsx-email/cli/tailwindcss/object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - "@mintlify/previewing/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "@jsx-email/cli/vite/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], - "@mintlify/scraping/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + "@jsx-email/cli/vite/rollup": ["rollup@3.30.0", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA=="], - "@mintlify/scraping/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "@jsx-email/doiuse-email/htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - "@mintlify/scraping/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "@malept/flatpak-bundler/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - "@mintlify/validation/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "@mapbox/node-pre-gyp/nopt/abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="], "@octokit/auth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], @@ -7904,65 +7400,81 @@ "@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], - "@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], + "@opencode-ai/www/@astrojs/cloudflare/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.1", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare": ["miniflare@4.20260708.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", "workerd": "1.20260708.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-c94O9zRDISdqO18EHt6l0iF/fWgWt8p18PJvRsA/L/NJZ9Cfke3s/F5Blg1XXF7WDutVRzWVWy8Vy4LaT5ifsA=="], + "@opencode-ai/www/@astrojs/cloudflare/@astrojs/underscore-redirects": ["@astrojs/underscore-redirects@1.0.3", "", {}, "sha512-cxnGSw+sJigBLdX4TMSZKkzV6C3gMLJMucDk2W+n281Xhie68T2/9f1+1NMNDCZsc5i0FED7Qt5I10g2O9wtZg=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/node": ["@tailwindcss/node@4.3.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "5.21.6", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.2" } }, "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg=="], + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.45.1", "", { "dependencies": { "@cloudflare/unenv-preset": "2.16.1", "miniflare": "4.20260714.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260714.1", "wrangler": "4.112.0", "ws": "8.21.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0 || ^8.0.0" }, "bin": { "cf-vite": "bin/cf-vite" } }, "sha512-C+iDpO9pVH7IqrjdYtUV+obcTAdpiNk0OSinGEZZyd2ZWyGVjGk7iJ2p3xpMBpZuTa1I4hfAECNp0yN/8f3PIQ=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.2", "@tailwindcss/oxide-darwin-arm64": "4.3.2", "@tailwindcss/oxide-darwin-x64": "4.3.2", "@tailwindcss/oxide-freebsd-x64": "4.3.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", "@tailwindcss/oxide-linux-x64-musl": "4.3.2", "@tailwindcss/oxide-wasm32-wasi": "4.3.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" } }, "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag=="], + "@opencode-ai/www/@astrojs/cloudflare/vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="], - "@opencode-ai/www/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "@opencode-ai/www/astro/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.1", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q=="], - "@opencode-ai/www/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], + "@opencode-ai/www/astro/@astrojs/telemetry": ["@astrojs/telemetry@3.3.3", "", { "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", "package-manager-detector": "^1.6.0" } }, "sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ=="], - "@opencode-ai/www/wrangler/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], + "@opencode-ai/www/astro/@capsizecss/unpack": ["@capsizecss/unpack@4.0.1", "", { "dependencies": { "fontkitten": "^1.0.3" } }, "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ=="], - "@opencode-ai/www/wrangler/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + "@opencode-ai/www/astro/@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="], - "@opencode-ai/www/wrangler/miniflare": ["miniflare@4.20260708.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", "workerd": "1.20260708.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-c94O9zRDISdqO18EHt6l0iF/fWgWt8p18PJvRsA/L/NJZ9Cfke3s/F5Blg1XXF7WDutVRzWVWy8Vy4LaT5ifsA=="], + "@opencode-ai/www/astro/cookie": ["cookie@2.0.1", "", {}, "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w=="], - "@opencode-ai/www/wrangler/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="], + "@opencode-ai/www/astro/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], - "@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@opencode-ai/www/astro/es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], - "@oxc-parser/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@opencode-ai/www/astro/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], - "@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], + "@opencode-ai/www/astro/fontace": ["fontace@0.4.1", "", { "dependencies": { "fontkitten": "^1.0.2" } }, "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw=="], - "@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], + "@opencode-ai/www/astro/get-tsconfig": ["get-tsconfig@5.0.0-beta.4", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ=="], - "@pierre/diffs/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "@opencode-ai/www/astro/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], - "@pierre/trees/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "@opencode-ai/www/astro/magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="], + + "@opencode-ai/www/astro/neotraverse": ["neotraverse@1.0.1", "", {}, "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w=="], + + "@opencode-ai/www/astro/p-limit": ["p-limit@7.3.1", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-0trZaiG7Y7kN/Egy9a8j47t9osC0Tch4PaIWd9yGF6bvmlk7muExRvGNYb8sXBwEKMoNKsbNN9P8EefuQekE4Q=="], + + "@opencode-ai/www/astro/p-queue": ["p-queue@9.3.3", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA=="], + + "@opencode-ai/www/astro/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], - "@radix-ui/react-accordion/@radix-ui/react-collapsible/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "@opencode-ai/www/astro/shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="], - "@radix-ui/react-accordion/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "@opencode-ai/www/astro/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], - "@radix-ui/react-accordion/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + "@opencode-ai/www/astro/unifont": ["unifont@0.7.4", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg=="], - "@radix-ui/react-accordion/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "@opencode-ai/www/astro/unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="], - "@radix-ui/react-dialog/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "@opencode-ai/www/astro/vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="], - "@radix-ui/react-dialog/@radix-ui/react-portal/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "@opencode-ai/www/astro/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], - "@radix-ui/react-dialog/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "@opencode-ai/www/astro/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "@radix-ui/react-navigation-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + "@opencode-ai/www/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], - "@radix-ui/react-scroll-area/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + "@opencode-ai/www/wrangler/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], - "@radix-ui/react-tabs/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "@opencode-ai/www/wrangler/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], - "@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + "@opencode-ai/www/wrangler/miniflare": ["miniflare@4.20260708.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", "workerd": "1.20260708.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-c94O9zRDISdqO18EHt6l0iF/fWgWt8p18PJvRsA/L/NJZ9Cfke3s/F5Blg1XXF7WDutVRzWVWy8Vy4LaT5ifsA=="], - "@radix-ui/react-tabs/@radix-ui/react-roving-focus/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + "@opencode-ai/www/wrangler/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="], - "@radix-ui/react-tabs/@radix-ui/react-roving-focus/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@radix-ui/react-tabs/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "@oxc-parser/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + + "@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], + + "@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], + + "@pierre/diffs/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "@pierre/trees/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "@sentry/bundler-plugin-core/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], @@ -7976,6 +7488,8 @@ "@shikijs/stream/@shikijs/core/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], + "@shikijs/twoslash/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A=="], + "@slack/bolt/express/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], "@slack/bolt/express/body-parser": ["body-parser@1.20.6", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g=="], @@ -8024,8 +7538,6 @@ "@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="], - "@stoplight/spectral-core/minimatch/brace-expansion": ["brace-expansion@1.1.16", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw=="], - "@storybook/addon-docs/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "@storybook/csf-plugin/unplugin/webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], @@ -8052,11 +7564,23 @@ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], - "@tanstack/directive-functions-plugin/@tanstack/router-utils/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], + "@vercel/cli-exec/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "@vercel/cli-exec/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "@vercel/cli-exec/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "@vercel/cli-exec/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], - "@tanstack/router-plugin/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + "@vercel/cli-exec/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - "@tanstack/router-plugin/unplugin/webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + "@vercel/cli-exec/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "@vercel/cli-exec/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "@vercel/functions/@vercel/oidc/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], + + "@vercel/routing-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], @@ -8116,204 +7640,148 @@ "babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "better-opn/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], - - "better-opn/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], - - "better-opn/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - - "builder-util/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - - "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "conf/dot-prop/type-fest": ["type-fest@5.8.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA=="], - - "cosmiconfig/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "dir-compare/minimatch/brace-expansion": ["brace-expansion@1.1.16", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw=="], - - "dir-compare/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - - "dmg-builder/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "duplexer2/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], - - "duplexer2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - - "duplexer2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - - "editorconfig/minimatch/brace-expansion": ["brace-expansion@2.1.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA=="], + "blume/@astrojs/mdx/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.1", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q=="], - "electron-updater/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "blume/@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@7.2.1", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.1", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-jPVNIqTvk+yKviikszv/Y1U4jGUSKpp/Nw48QZV4qjWgp70j4Lkq3lhSDRbWwCfgKvEyO9GHuVbV1dM2WYXy1w=="], - "electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + "blume/@astrojs/mdx/acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], - "engine.io/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "blume/@astrojs/mdx/es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], - "engine.io/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "blume/@astrojs/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - "esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "blume/@clack/prompts/@clack/core": ["@clack/core@1.4.3", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ=="], - "filelist/minimatch/brace-expansion": ["brace-expansion@2.1.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA=="], + "blume/@pierre/diffs/@pierre/theme": ["@pierre/theme@1.1.0", "", {}, "sha512-GC2OWTAfTIIWWYhPCygwG8t2EtePQkRfON4MI2rwIkJylmiyqIttJID2dCL8sUD8cNdEvYkEyfEHHKMeCiDLoQ=="], - "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "blume/@pierre/diffs/@pierre/theming": ["@pierre/theming@0.0.2", "", { "peerDependencies": { "@pierre/theme": "^1.1.0", "@shikijs/themes": "^3.0.0 || ^4.0.0", "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0", "shiki": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["@pierre/theme", "@shikijs/themes", "react", "react-dom", "shiki"] }, "sha512-QM1M4stXfnzfaE8I8YbjXSApV8c+2dBsXJj8eYg9WTpBR/cTmCZIcfGnN4p13iRrYu2Br/R/OJfEL7uR8Qjctw=="], - "fumadocs-core/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "blume/@pierre/diffs/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], - "fumadocs-core/shiki/@shikijs/core": ["@shikijs/core@4.3.1", "", { "dependencies": { "@shikijs/primitive": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA=="], + "blume/@shikijs/transformers/@shikijs/core": ["@shikijs/core@4.3.1", "", { "dependencies": { "@shikijs/primitive": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA=="], - "fumadocs-core/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ=="], + "blume/@shikijs/transformers/@shikijs/types": ["@shikijs/types@4.3.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="], - "fumadocs-core/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg=="], + "blume/@tailwindcss/vite/@tailwindcss/node": ["@tailwindcss/node@4.3.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "5.21.6", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.2" } }, "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg=="], - "fumadocs-core/shiki/@shikijs/langs": ["@shikijs/langs@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ=="], + "blume/@tailwindcss/vite/@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.2", "@tailwindcss/oxide-darwin-arm64": "4.3.2", "@tailwindcss/oxide-darwin-x64": "4.3.2", "@tailwindcss/oxide-freebsd-x64": "4.3.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", "@tailwindcss/oxide-linux-x64-musl": "4.3.2", "@tailwindcss/oxide-wasm32-wasi": "4.3.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" } }, "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag=="], - "fumadocs-core/shiki/@shikijs/themes": ["@shikijs/themes@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA=="], + "blume/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.115", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.30", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-BLN5eLjzg6mKkbxAf6oP8Z7mEyRihrDzBJr7QlNPZTFvgZJ15peoflUJuskTCZFaisqelj1P4Se1pHY8QBBgBA=="], - "fumadocs-core/shiki/@shikijs/types": ["@shikijs/types@4.3.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="], + "blume/ai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], - "fumadocs-mdx/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + "blume/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-NCJ9JKow5ENAgEZxzvEvF20thwDiH+hutvzmrUDbloRX0azpJHNst8+7pZIVryYhLM9wgpT5/ShTSjPTFhkxEQ=="], - "fumadocs-mdx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + "blume/astro/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.1", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q=="], - "fumadocs-mdx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + "blume/astro/@astrojs/telemetry": ["@astrojs/telemetry@3.3.3", "", { "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", "package-manager-detector": "^1.6.0" } }, "sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ=="], - "fumadocs-mdx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + "blume/astro/@capsizecss/unpack": ["@capsizecss/unpack@4.0.1", "", { "dependencies": { "fontkitten": "^1.0.3" } }, "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ=="], - "fumadocs-mdx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + "blume/astro/cookie": ["cookie@2.0.1", "", {}, "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w=="], - "fumadocs-mdx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + "blume/astro/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], - "fumadocs-mdx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + "blume/astro/es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], - "fumadocs-mdx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + "blume/astro/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], - "fumadocs-mdx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + "blume/astro/fontace": ["fontace@0.4.1", "", { "dependencies": { "fontkitten": "^1.0.2" } }, "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw=="], - "fumadocs-mdx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + "blume/astro/get-tsconfig": ["get-tsconfig@5.0.0-beta.4", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ=="], - "fumadocs-mdx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + "blume/astro/magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="], - "fumadocs-mdx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + "blume/astro/neotraverse": ["neotraverse@1.0.1", "", {}, "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w=="], - "fumadocs-mdx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + "blume/astro/p-limit": ["p-limit@7.3.1", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-0trZaiG7Y7kN/Egy9a8j47t9osC0Tch4PaIWd9yGF6bvmlk7muExRvGNYb8sXBwEKMoNKsbNN9P8EefuQekE4Q=="], - "fumadocs-mdx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + "blume/astro/p-queue": ["p-queue@9.3.3", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA=="], - "fumadocs-mdx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + "blume/astro/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], - "fumadocs-mdx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + "blume/astro/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], - "fumadocs-mdx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + "blume/astro/unifont": ["unifont@0.7.4", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg=="], - "fumadocs-mdx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + "blume/astro/unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="], - "fumadocs-mdx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + "blume/astro/vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="], - "fumadocs-mdx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + "blume/astro/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], - "fumadocs-mdx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + "blume/astro/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "fumadocs-mdx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + "blume/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "fumadocs-mdx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + "blume/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], - "fumadocs-mdx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + "blume/node-html-parser/entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="], - "fumadocs-mdx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + "blume/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - "fumadocs-mdx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + "blume/shiki/@shikijs/core": ["@shikijs/core@4.3.1", "", { "dependencies": { "@shikijs/primitive": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA=="], - "fumadocs-mdx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + "blume/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ=="], - "fumadocs-mdx/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "blume/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg=="], - "fumadocs-ui/@radix-ui/react-collapsible/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="], + "blume/shiki/@shikijs/langs": ["@shikijs/langs@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ=="], - "fumadocs-ui/@radix-ui/react-collapsible/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + "blume/shiki/@shikijs/themes": ["@shikijs/themes@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA=="], - "fumadocs-ui/@radix-ui/react-collapsible/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], + "blume/shiki/@shikijs/types": ["@shikijs/types@4.3.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="], - "fumadocs-ui/@radix-ui/react-collapsible/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], + "builder-util/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "fumadocs-ui/@radix-ui/react-collapsible/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], + "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - "fumadocs-ui/@radix-ui/react-collapsible/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "fumadocs-ui/@radix-ui/react-collapsible/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "fumadocs-ui/@radix-ui/react-popover/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="], + "conf/dot-prop/type-fest": ["type-fest@5.8.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA=="], - "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], + "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], - "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-effect-event": "0.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA=="], + "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], - "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q=="], + "d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], - "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.12", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jjk/lqTeNL0azUx5ZYzVrl4NgaDIrdzTNE4mABV9yBFI7FQqN7pIgzV1bTleUezP2QiTGA1BFTqY8MegDgWX9A=="], + "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], - "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], + "dir-compare/minimatch/brace-expansion": ["brace-expansion@1.1.16", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw=="], - "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-popper": ["@radix-ui/react-popper@1.3.3", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.11", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-rect": "1.1.2", "@radix-ui/react-use-size": "1.1.2", "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mS7dGpyjv6b+gsDjLF7e0ia1W4Im1B1hSCy2yuXlHuvnZxHKagfDaobt/KAKt27EpZMit2pss8eJBVyVjEWM+g=="], + "dir-compare/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.13", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA=="], + "dmg-builder/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], + "duplexer2/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], - "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], + "duplexer2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - "fumadocs-ui/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + "duplexer2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "fumadocs-ui/motion/framer-motion": ["framer-motion@12.42.2", "", { "dependencies": { "motion-dom": "^12.42.2", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw=="], + "editorconfig/minimatch/brace-expansion": ["brace-expansion@2.1.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA=="], - "fumadocs-ui/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "electron-updater/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "fumadocs-ui/shiki/@shikijs/core": ["@shikijs/core@4.3.1", "", { "dependencies": { "@shikijs/primitive": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA=="], + "electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], - "fumadocs-ui/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ=="], + "epub-gen-memory/dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], - "fumadocs-ui/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg=="], + "epub-gen-memory/htmlparser2/entities": ["entities@3.0.1", "", {}, "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q=="], - "fumadocs-ui/shiki/@shikijs/langs": ["@shikijs/langs@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ=="], + "esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - "fumadocs-ui/shiki/@shikijs/themes": ["@shikijs/themes@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA=="], + "filelist/minimatch/brace-expansion": ["brace-expansion@2.1.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA=="], - "fumadocs-ui/shiki/@shikijs/types": ["@shikijs/types@4.3.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "gcp-metadata/gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "ink/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], - - "inquirer/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], - - "is-online/got/@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="], - - "is-online/got/@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="], - - "is-online/got/cacheable-lookup": ["cacheable-lookup@7.0.0", "", {}, "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w=="], - - "is-online/got/cacheable-request": ["cacheable-request@10.2.14", "", { "dependencies": { "@types/http-cache-semantics": "^4.0.2", "get-stream": "^6.0.1", "http-cache-semantics": "^4.1.1", "keyv": "^4.5.3", "mimic-response": "^4.0.0", "normalize-url": "^8.0.0", "responselike": "^3.0.0" } }, "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ=="], - - "is-online/got/form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="], - - "is-online/got/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], - - "is-online/got/http2-wrapper": ["http2-wrapper@2.2.1", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" } }, "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ=="], - - "is-online/got/lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="], - - "is-online/got/p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], - - "is-online/got/responselike": ["responselike@3.0.0", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg=="], - "js-beautify/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "js-beautify/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], @@ -8322,104 +7790,42 @@ "js-beautify/nopt/abbrev": ["abbrev@2.0.0", "", {}, "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ=="], + "jszip/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "jszip/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "lazystream/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "mermaid/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + "motion/framer-motion/motion-dom": ["motion-dom@12.42.2", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA=="], "motion/framer-motion/motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], - "next-mdx-remote-client/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - - "next-mdx-remote-client/serialize-error/type-fest": ["type-fest@5.8.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA=="], - "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], - "public-ip/got/@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="], - - "public-ip/got/@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="], - - "public-ip/got/cacheable-lookup": ["cacheable-lookup@7.0.0", "", {}, "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w=="], - - "public-ip/got/cacheable-request": ["cacheable-request@10.2.14", "", { "dependencies": { "@types/http-cache-semantics": "^4.0.2", "get-stream": "^6.0.1", "http-cache-semantics": "^4.1.1", "keyv": "^4.5.3", "mimic-response": "^4.0.0", "normalize-url": "^8.0.0", "responselike": "^3.0.0" } }, "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ=="], - - "public-ip/got/form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="], - - "public-ip/got/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], - - "public-ip/got/http2-wrapper": ["http2-wrapper@2.2.1", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" } }, "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ=="], - - "public-ip/got/lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="], - - "public-ip/got/p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], - - "public-ip/got/responselike": ["responselike@3.0.0", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg=="], - "readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.1.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA=="], - "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - "rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "rimraf/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], "rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "sharp-ico/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], - - "sharp-ico/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], - - "sharp-ico/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], - - "sharp-ico/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], - - "sharp-ico/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], - - "sharp-ico/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], - - "sharp-ico/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], - - "sharp-ico/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], - - "sharp-ico/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], - - "sharp-ico/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], - - "sharp-ico/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], - - "sharp-ico/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], - - "sharp-ico/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], - - "sharp-ico/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], - - "sharp-ico/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], - - "sharp-ico/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], - - "sharp-ico/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], - - "sharp-ico/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], - - "sharp-ico/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], - - "socket.io/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "socket.io/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - "storybook/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "temp/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "tw-to-css/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], @@ -8442,6 +7848,8 @@ "vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "vitest/vite/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + "vscode-languageserver/vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], @@ -8498,8 +7906,6 @@ "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "xmlbuilder2/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -8574,6 +7980,20 @@ "@astrojs/cloudflare/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260708.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA=="], + "@astrojs/markdown-satteri/@astrojs/internal-helpers/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "@astrojs/markdown-satteri/@astrojs/internal-helpers/shiki/@shikijs/core": ["@shikijs/core@4.3.1", "", { "dependencies": { "@shikijs/primitive": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA=="], + + "@astrojs/markdown-satteri/@astrojs/internal-helpers/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ=="], + + "@astrojs/markdown-satteri/@astrojs/internal-helpers/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg=="], + + "@astrojs/markdown-satteri/@astrojs/internal-helpers/shiki/@shikijs/langs": ["@shikijs/langs@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ=="], + + "@astrojs/markdown-satteri/@astrojs/internal-helpers/shiki/@shikijs/themes": ["@shikijs/themes@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA=="], + + "@astrojs/markdown-satteri/@astrojs/internal-helpers/shiki/@shikijs/types": ["@shikijs/types@4.3.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="], + "@astrojs/mdx/@astrojs/markdown-remark/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "@astrojs/mdx/@astrojs/markdown-remark/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], @@ -8586,7 +8006,285 @@ "@astrojs/mdx/@astrojs/markdown-remark/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], - "@astrojs/mdx/@astrojs/markdown-remark/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + "@astrojs/mdx/@astrojs/markdown-remark/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + + "@astrojs/node/@astrojs/internal-helpers/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "@astrojs/node/@astrojs/internal-helpers/shiki/@shikijs/core": ["@shikijs/core@4.3.1", "", { "dependencies": { "@shikijs/primitive": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA=="], + + "@astrojs/node/@astrojs/internal-helpers/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ=="], + + "@astrojs/node/@astrojs/internal-helpers/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg=="], + + "@astrojs/node/@astrojs/internal-helpers/shiki/@shikijs/langs": ["@shikijs/langs@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ=="], + + "@astrojs/node/@astrojs/internal-helpers/shiki/@shikijs/themes": ["@shikijs/themes@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA=="], + + "@astrojs/node/@astrojs/internal-helpers/shiki/@shikijs/types": ["@shikijs/types@4.3.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="], + + "@astrojs/node/astro/@astrojs/telemetry/is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="], + + "@astrojs/node/astro/@clack/prompts/@clack/core": ["@clack/core@1.4.3", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ=="], + + "@astrojs/node/astro/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "@astrojs/node/astro/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "@astrojs/node/astro/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "@astrojs/node/astro/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "@astrojs/node/astro/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "@astrojs/node/astro/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "@astrojs/node/astro/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "@astrojs/node/astro/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "@astrojs/node/astro/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "@astrojs/node/astro/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "@astrojs/node/astro/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "@astrojs/node/astro/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "@astrojs/node/astro/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "@astrojs/node/astro/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "@astrojs/node/astro/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "@astrojs/node/astro/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "@astrojs/node/astro/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "@astrojs/node/astro/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "@astrojs/node/astro/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "@astrojs/node/astro/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "@astrojs/node/astro/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "@astrojs/node/astro/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "@astrojs/node/astro/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "@astrojs/node/astro/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "@astrojs/node/astro/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "@astrojs/node/astro/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + + "@astrojs/node/astro/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "@astrojs/node/astro/p-queue/p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="], + + "@astrojs/node/astro/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@astrojs/node/astro/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@astrojs/node/astro/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@astrojs/node/astro/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@astrojs/node/astro/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@astrojs/node/astro/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@astrojs/node/astro/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@astrojs/node/astro/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@astrojs/node/astro/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@astrojs/node/astro/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@astrojs/node/astro/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@astrojs/node/astro/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@astrojs/node/astro/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@astrojs/node/astro/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@astrojs/node/astro/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@astrojs/node/astro/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@astrojs/node/astro/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@astrojs/node/astro/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@astrojs/node/astro/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "@astrojs/node/astro/shiki/@shikijs/core": ["@shikijs/core@4.3.1", "", { "dependencies": { "@shikijs/primitive": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA=="], + + "@astrojs/node/astro/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ=="], + + "@astrojs/node/astro/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg=="], + + "@astrojs/node/astro/shiki/@shikijs/langs": ["@shikijs/langs@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ=="], + + "@astrojs/node/astro/shiki/@shikijs/themes": ["@shikijs/themes@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA=="], + + "@astrojs/node/astro/shiki/@shikijs/types": ["@shikijs/types@4.3.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="], + + "@astrojs/node/astro/unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], + + "@astrojs/node/astro/unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + + "@astrojs/node/astro/unstorage/h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], + + "@astrojs/node/astro/unstorage/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], + + "@astrojs/react/@astrojs/internal-helpers/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "@astrojs/react/@astrojs/internal-helpers/shiki/@shikijs/core": ["@shikijs/core@4.3.1", "", { "dependencies": { "@shikijs/primitive": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA=="], + + "@astrojs/react/@astrojs/internal-helpers/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ=="], + + "@astrojs/react/@astrojs/internal-helpers/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg=="], + + "@astrojs/react/@astrojs/internal-helpers/shiki/@shikijs/langs": ["@shikijs/langs@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ=="], + + "@astrojs/react/@astrojs/internal-helpers/shiki/@shikijs/themes": ["@shikijs/themes@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA=="], + + "@astrojs/react/@astrojs/internal-helpers/shiki/@shikijs/types": ["@shikijs/types@4.3.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="], + + "@astrojs/react/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "@astrojs/react/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "@astrojs/react/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "@astrojs/react/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "@astrojs/react/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "@astrojs/react/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "@astrojs/react/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "@astrojs/react/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "@astrojs/react/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "@astrojs/react/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "@astrojs/react/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "@astrojs/react/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "@astrojs/react/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "@astrojs/react/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "@astrojs/react/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "@astrojs/react/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "@astrojs/react/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "@astrojs/react/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "@astrojs/react/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "@astrojs/react/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "@astrojs/react/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "@astrojs/react/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "@astrojs/react/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "@astrojs/react/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "@astrojs/react/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "@astrojs/react/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + + "@astrojs/vercel/@astrojs/internal-helpers/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "@astrojs/vercel/@astrojs/internal-helpers/shiki/@shikijs/core": ["@shikijs/core@4.3.1", "", { "dependencies": { "@shikijs/primitive": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA=="], + + "@astrojs/vercel/@astrojs/internal-helpers/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ=="], + + "@astrojs/vercel/@astrojs/internal-helpers/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg=="], + + "@astrojs/vercel/@astrojs/internal-helpers/shiki/@shikijs/langs": ["@shikijs/langs@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ=="], + + "@astrojs/vercel/@astrojs/internal-helpers/shiki/@shikijs/themes": ["@shikijs/themes@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA=="], + + "@astrojs/vercel/@astrojs/internal-helpers/shiki/@shikijs/types": ["@shikijs/types@4.3.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="], + + "@astrojs/vercel/astro/@astrojs/telemetry/is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="], + + "@astrojs/vercel/astro/@clack/prompts/@clack/core": ["@clack/core@1.4.3", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ=="], + + "@astrojs/vercel/astro/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "@astrojs/vercel/astro/p-queue/p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="], + + "@astrojs/vercel/astro/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@astrojs/vercel/astro/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@astrojs/vercel/astro/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@astrojs/vercel/astro/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@astrojs/vercel/astro/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@astrojs/vercel/astro/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@astrojs/vercel/astro/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@astrojs/vercel/astro/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@astrojs/vercel/astro/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@astrojs/vercel/astro/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@astrojs/vercel/astro/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@astrojs/vercel/astro/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@astrojs/vercel/astro/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@astrojs/vercel/astro/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@astrojs/vercel/astro/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@astrojs/vercel/astro/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@astrojs/vercel/astro/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@astrojs/vercel/astro/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@astrojs/vercel/astro/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "@astrojs/vercel/astro/shiki/@shikijs/core": ["@shikijs/core@4.3.1", "", { "dependencies": { "@shikijs/primitive": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA=="], + + "@astrojs/vercel/astro/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ=="], + + "@astrojs/vercel/astro/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg=="], + + "@astrojs/vercel/astro/shiki/@shikijs/langs": ["@shikijs/langs@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ=="], + + "@astrojs/vercel/astro/shiki/@shikijs/themes": ["@shikijs/themes@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA=="], + + "@astrojs/vercel/astro/shiki/@shikijs/types": ["@shikijs/types@4.3.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="], + + "@astrojs/vercel/astro/unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], + + "@astrojs/vercel/astro/unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + + "@astrojs/vercel/astro/unstorage/h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], + + "@astrojs/vercel/astro/unstorage/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], @@ -8608,10 +8306,6 @@ "@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "@inquirer/core/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "@inquirer/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@jsx-email/cli/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "@jsx-email/cli/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -8660,70 +8354,6 @@ "@jsx-email/cli/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "@mintlify/cli/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "@mintlify/cli/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@mintlify/common/remark-gfm/mdast-util-gfm/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], - - "@mintlify/common/sucrase/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - - "@mintlify/common/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "@mintlify/common/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - - "@mintlify/common/tailwindcss/sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - - "@mintlify/mdx/@radix-ui/react-popover/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], - - "@mintlify/mdx/@radix-ui/react-popover/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.11", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Kdil9BB1rIFC/khmf4hC35bn8701AJcizTU7G7cUbEbk5XqqbjDuHW60uUfKqO5WojjZcbAW51Q7P0hRmMLw8A=="], - - "@mintlify/mdx/@radix-ui/react-popover/@radix-ui/react-popper/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], - - "@mintlify/mdx/@radix-ui/react-popover/@radix-ui/react-popper/@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.2", "", { "dependencies": { "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw=="], - - "@mintlify/mdx/@radix-ui/react-popover/@radix-ui/react-popper/@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w=="], - - "@mintlify/mdx/@radix-ui/react-popover/@radix-ui/react-popper/@radix-ui/rect": ["@radix-ui/rect@1.1.2", "", {}, "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA=="], - - "@mintlify/mdx/@radix-ui/react-popover/@radix-ui/react-portal/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], - - "@mintlify/mdx/@radix-ui/react-popover/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], - - "@mintlify/models/axios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - - "@mintlify/previewing/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - - "@mintlify/previewing/express/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "@mintlify/previewing/express/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - - "@mintlify/previewing/express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - - "@mintlify/previewing/express/body-parser/qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], - - "@mintlify/previewing/express/body-parser/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], - - "@mintlify/previewing/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "@mintlify/previewing/express/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - - "@mintlify/previewing/express/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], - - "@mintlify/previewing/express/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "@mintlify/previewing/got/cacheable-request/mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="], - - "@mintlify/previewing/got/cacheable-request/normalize-url": ["normalize-url@8.1.1", "", {}, "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ=="], - - "@mintlify/previewing/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "@mintlify/previewing/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@mintlify/scraping/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "@mintlify/scraping/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@octokit/auth-app/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/auth-app/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], @@ -8816,39 +8446,137 @@ "@opencode-ai/updates/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260708.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA=="], - "@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="], + "@opencode-ai/www/@astrojs/cloudflare/@astrojs/internal-helpers/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], + + "@opencode-ai/www/@astrojs/cloudflare/@astrojs/internal-helpers/shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare": ["miniflare@4.20260714.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", "workerd": "1.20260714.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-MYlTCLdWCPqvrYY2uLwOjXwmglXuiHE3TGGkbOW4BwjUPa1r07E0iuHwrNDIs/sxK21r+o90Jx58AV2KeNdJZw=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/workerd": ["workerd@1.20260714.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260714.1", "@cloudflare/workerd-darwin-arm64": "1.20260714.1", "@cloudflare/workerd-linux-64": "1.20260714.1", "@cloudflare/workerd-linux-arm64": "1.20260714.1", "@cloudflare/workerd-windows-64": "1.20260714.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-oIbQzfdyl9UQUnG6XLegcSq0Mgt/7WKDbFOoqGgOWCS+/fhyGB460uKEgdAQQ9RHCO/ttcNCX/KiMIQzdoeu3Q=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler": ["wrangler@4.112.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "4.20260714.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260714.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260714.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-5H+XUD0TySCv1LuktFHDIEOkboH2nTfQs+35L+USt3MtntjDTMVIJprLgQcL2WBjulOyjxpd1vyTiSTJVW5MjQ=="], + + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + + "@opencode-ai/www/astro/@astrojs/telemetry/is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="], + + "@opencode-ai/www/astro/@clack/prompts/@clack/core": ["@clack/core@1.4.3", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "@opencode-ai/www/astro/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + "@opencode-ai/www/astro/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], + "@opencode-ai/www/astro/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="], + "@opencode-ai/www/astro/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/node/enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="], + "@opencode-ai/www/astro/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.2", "", { "os": "android", "cpu": "arm64" }, "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA=="], + "@opencode-ai/www/astro/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w=="], + "@opencode-ai/www/astro/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ=="], + "@opencode-ai/www/astro/p-queue/p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA=="], + "@opencode-ai/www/astro/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w=="], + "@opencode-ai/www/astro/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw=="], + "@opencode-ai/www/astro/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA=="], + "@opencode-ai/www/astro/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w=="], + "@opencode-ai/www/astro/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw=="], + "@opencode-ai/www/astro/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.2", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw=="], + "@opencode-ai/www/astro/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ=="], + "@opencode-ai/www/astro/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ=="], + "@opencode-ai/www/astro/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@opencode-ai/www/astro/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@opencode-ai/www/astro/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@opencode-ai/www/astro/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@opencode-ai/www/astro/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@opencode-ai/www/astro/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@opencode-ai/www/astro/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@opencode-ai/www/astro/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@opencode-ai/www/astro/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@opencode-ai/www/astro/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@opencode-ai/www/astro/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "@opencode-ai/www/astro/shiki/@shikijs/core": ["@shikijs/core@4.3.1", "", { "dependencies": { "@shikijs/primitive": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA=="], + + "@opencode-ai/www/astro/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ=="], + + "@opencode-ai/www/astro/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg=="], + + "@opencode-ai/www/astro/shiki/@shikijs/langs": ["@shikijs/langs@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ=="], + + "@opencode-ai/www/astro/shiki/@shikijs/themes": ["@shikijs/themes@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA=="], + + "@opencode-ai/www/astro/shiki/@shikijs/types": ["@shikijs/types@4.3.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="], + + "@opencode-ai/www/astro/unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], + + "@opencode-ai/www/astro/unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + + "@opencode-ai/www/astro/unstorage/h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], + + "@opencode-ai/www/astro/unstorage/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], "@opencode-ai/www/wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], @@ -8916,8 +8644,6 @@ "@opencode-ai/www/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260708.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA=="], - "@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], - "@sentry/bundler-plugin-core/glob/minimatch/brace-expansion": ["brace-expansion@2.1.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA=="], "@sentry/bundler-plugin-core/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -8942,7 +8668,7 @@ "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="], - "@stoplight/spectral-core/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "@vercel/cli-exec/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -8966,43 +8692,149 @@ "babel-plugin-module-resolver/glob/path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], - "dir-compare/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "blume/@astrojs/mdx/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@4.0.2", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA=="], - "editorconfig/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "blume/@shikijs/transformers/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A=="], - "engine.io/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "blume/@tailwindcss/vite/@tailwindcss/node/enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="], - "esbuild-plugin-copy/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "blume/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.2", "", { "os": "android", "cpu": "arm64" }, "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA=="], - "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "blume/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w=="], + + "blume/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ=="], + + "blume/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA=="], + + "blume/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w=="], + + "blume/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw=="], + + "blume/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA=="], + + "blume/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w=="], + + "blume/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw=="], + + "blume/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.2", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw=="], + + "blume/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ=="], + + "blume/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ=="], + + "blume/ai/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], + + "blume/astro/@astrojs/telemetry/is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="], + + "blume/astro/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "blume/astro/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "blume/astro/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "blume/astro/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "blume/astro/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "blume/astro/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "blume/astro/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "blume/astro/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "blume/astro/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "blume/astro/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "blume/astro/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "blume/astro/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "blume/astro/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "blume/astro/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "blume/astro/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "blume/astro/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "blume/astro/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "blume/astro/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "blume/astro/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "blume/astro/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "blume/astro/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "blume/astro/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "blume/astro/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "blume/astro/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "blume/astro/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "blume/astro/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + + "blume/astro/p-queue/p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="], - "fumadocs-core/shiki/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A=="], + "blume/astro/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], - "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "blume/astro/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], - "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.11", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Kdil9BB1rIFC/khmf4hC35bn8701AJcizTU7G7cUbEbk5XqqbjDuHW60uUfKqO5WojjZcbAW51Q7P0hRmMLw8A=="], + "blume/astro/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], - "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-popper/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "blume/astro/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], - "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-popper/@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.2", "", { "dependencies": { "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw=="], + "blume/astro/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], - "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-popper/@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w=="], + "blume/astro/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], - "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-popper/@radix-ui/rect": ["@radix-ui/rect@1.1.2", "", {}, "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA=="], + "blume/astro/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], - "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-portal/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "blume/astro/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], - "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "blume/astro/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], - "fumadocs-ui/motion/framer-motion/motion-dom": ["motion-dom@12.42.2", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA=="], + "blume/astro/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], - "fumadocs-ui/motion/framer-motion/motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], + "blume/astro/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], - "fumadocs-ui/shiki/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A=="], + "blume/astro/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], - "is-online/got/cacheable-request/mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="], + "blume/astro/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], - "is-online/got/cacheable-request/normalize-url": ["normalize-url@8.1.1", "", {}, "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ=="], + "blume/astro/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "blume/astro/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "blume/astro/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "blume/astro/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "blume/astro/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "blume/astro/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "blume/astro/unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], + + "blume/astro/unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + + "blume/astro/unstorage/h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], + + "blume/astro/unstorage/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], + + "blume/shiki/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A=="], + + "dir-compare/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "editorconfig/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "esbuild-plugin-copy/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "js-beautify/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], @@ -9016,10 +8848,6 @@ "pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], - "public-ip/got/cacheable-request/mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="], - - "public-ip/got/cacheable-request/normalize-url": ["normalize-url@8.1.1", "", {}, "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ=="], - "readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "rimraf/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], @@ -9028,10 +8856,6 @@ "rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "sharp-ico/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], - - "socket.io/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "temp/rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "tw-to-css/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -9040,6 +8864,58 @@ "unplugin/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "vitest/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "vitest/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "vitest/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "@astrojs/cloudflare/wrangler/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], @@ -9080,23 +8956,37 @@ "@astrojs/cloudflare/wrangler/miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], - "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="], + "@astrojs/markdown-satteri/@astrojs/internal-helpers/shiki/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A=="], - "@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "@astrojs/node/@astrojs/internal-helpers/shiki/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A=="], + + "@astrojs/node/astro/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], + + "@astrojs/node/astro/shiki/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A=="], + + "@astrojs/node/astro/unstorage/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + + "@astrojs/node/astro/unstorage/h3/cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], + + "@astrojs/node/astro/unstorage/h3/crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], + + "@astrojs/react/@astrojs/internal-helpers/shiki/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A=="], - "@mintlify/cli/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@astrojs/vercel/@astrojs/internal-helpers/shiki/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A=="], - "@mintlify/common/sucrase/glob/minimatch/brace-expansion": ["brace-expansion@1.1.16", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw=="], + "@astrojs/vercel/astro/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], - "@mintlify/common/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "@astrojs/vercel/astro/shiki/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A=="], - "@mintlify/previewing/express/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "@astrojs/vercel/astro/unstorage/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - "@mintlify/previewing/express/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "@astrojs/vercel/astro/unstorage/h3/cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], - "@mintlify/previewing/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@astrojs/vercel/astro/unstorage/h3/crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], - "@mintlify/scraping/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="], + + "@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "@opencode-ai/desktop/@actions/artifact/@actions/core/@actions/exec/@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="], @@ -9138,75 +9028,99 @@ "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], - "@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260708.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-HXFCvhS1wpg3uXO0CLUwmwC41i2loM5FSK69EUchOBpmYBAXxT1oHLm6EOA5lqhTk5Mu9kjRiQYxa1GwKPwfJg=="], + "@opencode-ai/www/@astrojs/cloudflare/@astrojs/internal-helpers/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "@opencode-ai/www/@astrojs/cloudflare/@astrojs/internal-helpers/shiki/@shikijs/core": ["@shikijs/core@4.3.1", "", { "dependencies": { "@shikijs/primitive": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA=="], + + "@opencode-ai/www/@astrojs/cloudflare/@astrojs/internal-helpers/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ=="], + + "@opencode-ai/www/@astrojs/cloudflare/@astrojs/internal-helpers/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg=="], + + "@opencode-ai/www/@astrojs/cloudflare/@astrojs/internal-helpers/shiki/@shikijs/langs": ["@shikijs/langs@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ=="], + + "@opencode-ai/www/@astrojs/cloudflare/@astrojs/internal-helpers/shiki/@shikijs/themes": ["@shikijs/themes@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA=="], - "@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260708.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JVlJaKDoRTVKSroHIlf8g3UCPjKj4iDbMZE2CNYht5qQ+2rL0FAUiVlV82G3BqKnnw9kHYnnsMzC08b9zVtdzA=="], + "@opencode-ai/www/@astrojs/cloudflare/@astrojs/internal-helpers/shiki/@shikijs/types": ["@shikijs/types@4.3.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="], - "@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260708.1", "", { "os": "linux", "cpu": "x64" }, "sha512-3daE60YdD7YX0Jtuzc9DE/r/qMkmx8ZvHTkF8Mzmp3F5tbzlV0DAzmu5PFUPF2WuvtKbAhZKbvC2cHmWpQYxnA=="], + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], - "@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260708.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-VLdNYOx5Hj+9C6isy0ACWZsbMtSxex2DIJWEe7cZxUdlphZ58ZT8zxNXK8yunFiowd34hn3VwGMopdvdj8lvmA=="], + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], - "@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260708.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA=="], + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260714.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZWXqAN8G7Cx9hMRQuk+59ziJhR3j1F4iO+Qs8aHdfKZ3Dq5Yi/57xvkJTgCGBnW1YU/L78r8f6HEy51bwbTpNw=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260714.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tueWxWC3wyCbMG6zRAxsMXX0YLgrRWbiAPYFQ2uJ7dUH8G+5E7UTWaQS9B1HdJ0bpKFW1NWxhs1o2noKVFSUYg=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260714.1", "", { "os": "linux", "cpu": "x64" }, "sha512-1VChTZRb0l0F7R4e1G5RtLKV4oFi6x+rQgxh2+yu887j3l/3TLgatuv1L8/5zhc9gKEhATTxOh0e52Rtd9dDWQ=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260714.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-rMm3G+NirG2UdgHIRDdF1asNC6FqgIzZzkRG+VDhhDGcVxAQwvrMT1E38BivEvHr3G04MB4AfhcOczX0+GtRkQ=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260714.1", "", { "os": "win32", "cpu": "x64" }, "sha512-cGqnU3Hg2YZS/k3SAqrMp1DjpdsyFde72tWltdl6ZT9+SFz/Zrk/8gyTU1TcxC4YApXeNVH5TyU5cOGPgUJ0pg=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260708.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-HXFCvhS1wpg3uXO0CLUwmwC41i2loM5FSK69EUchOBpmYBAXxT1oHLm6EOA5lqhTk5Mu9kjRiQYxa1GwKPwfJg=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260708.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JVlJaKDoRTVKSroHIlf8g3UCPjKj4iDbMZE2CNYht5qQ+2rL0FAUiVlV82G3BqKnnw9kHYnnsMzC08b9zVtdzA=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260708.1", "", { "os": "linux", "cpu": "x64" }, "sha512-3daE60YdD7YX0Jtuzc9DE/r/qMkmx8ZvHTkF8Mzmp3F5tbzlV0DAzmu5PFUPF2WuvtKbAhZKbvC2cHmWpQYxnA=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260708.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-VLdNYOx5Hj+9C6isy0ACWZsbMtSxex2DIJWEe7cZxUdlphZ58ZT8zxNXK8yunFiowd34hn3VwGMopdvdj8lvmA=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260708.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], - "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "@opencode-ai/www/@astrojs/cloudflare/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + + "@opencode-ai/www/astro/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], + + "@opencode-ai/www/astro/shiki/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A=="], + + "@opencode-ai/www/astro/unstorage/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + + "@opencode-ai/www/astro/unstorage/h3/cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], + + "@opencode-ai/www/astro/unstorage/h3/crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], @@ -9264,6 +9178,26 @@ "babel-plugin-module-resolver/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "blume/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], + + "blume/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], + + "blume/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + + "blume/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], + + "blume/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], + + "blume/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "blume/astro/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], + + "blume/astro/unstorage/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + + "blume/astro/unstorage/h3/cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], + + "blume/astro/unstorage/h3/crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], + "js-beautify/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "js-beautify/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], @@ -9286,11 +9220,99 @@ "@astrojs/cloudflare/wrangler/miniflare/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], - "@mintlify/common/sucrase/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], - "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], + "@opencode-ai/www/@astrojs/cloudflare/@astrojs/internal-helpers/shiki/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], @@ -9307,5 +9329,7 @@ "rimraf/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "temp/rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], } } diff --git a/packages/core/src/plugin/skill/opencode.md b/packages/core/src/plugin/skill/opencode.md index 669f162d7baa..913082d3f1cf 100644 --- a/packages/core/src/plugin/skill/opencode.md +++ b/packages/core/src/plugin/skill/opencode.md @@ -68,7 +68,7 @@ every field, examples, config locations, and links to dedicated feature guides. For any request to migrate OpenCode configuration, agents, commands, skills, plugins, integrations, or other behavior from V1 to V2, read the full [migration guide](https://v2.opencode.ai/docs/migrate-v1) before acting. In -the repository, its source is `packages/docs/migrate-v1.mdx`. +the repository, its source is `packages/www/content/docs/(Get started)/migrate-v1.mdx`. V1 config files and `.opencode/` definitions are intended to remain compatible. The only intentional breaking changes are the server API and plugin API. Native diff --git a/packages/docs/AGENTS.md b/packages/docs/AGENTS.md deleted file mode 100644 index 8d82c5265541..000000000000 --- a/packages/docs/AGENTS.md +++ /dev/null @@ -1,22 +0,0 @@ -# V2 documentation guide - -## Structure - -- This directory is a standalone Mintlify site deployed from `packages/docs` on the `dev` branch. -- Write documentation in MDX. Every page should have `title` and `description` frontmatter. -- `docs.json` owns site configuration and navigation. Add, move, or remove its page entries whenever the corresponding MDX pages change. -- Put static files in `assets/` and reference them with root-relative paths such as `/assets/example.svg`. -- The API endpoint reference is generated by Mintlify from `openapi.json`; do not duplicate endpoint documentation as hand-written MDX. -- Keep documentation aligned with the V2 packages. Do not use `packages/opencode` as the source of truth unless the task explicitly concerns V1. - -## Local development - -- At the start of documentation work, launch `bun dev` from `packages/docs` using the shell tool with `background: true`. Never run the dev server in a foreground shell call and do not poll the process; wait for the background completion notification. -- Preview the site at `http://localhost:3333`. Mintlify does not expose a host option and binds the preview to all network interfaces. The server reloads changes to MDX and `docs.json` automatically. -- Use the running preview to verify navigation, links, Mintlify components, code blocks, and desktop and mobile layout. - -## Validation - -- Run `bun validate` from `packages/docs` after making documentation or configuration changes. -- Run `bun broken-links` from `packages/docs` when pages, navigation, headings, or links change. -- Treat validation errors and broken internal links as blockers. Also verify external links relevant to the change when practical. diff --git a/packages/docs/README.md b/packages/docs/README.md deleted file mode 100644 index 1aff8cf6dc30..000000000000 --- a/packages/docs/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# OpenCode documentation - -The V2 documentation is a Mintlify site deployed from `packages/docs` on the `dev` branch. - -## Local preview - -From this directory, run: - -```bash -bun dev -``` - -The preview opens at `http://localhost:3333` and reloads when MDX or `docs.json` changes. - -Validate changes before opening a pull request: - -```bash -bun validate -bun broken-links -``` - -The V2 theme token reference is generated from -`packages/tui/src/theme/v2/schema.ts`. Regenerate it after schema changes: - -```bash -bun run generate -``` - -`bun validate` checks that the committed snippet is current. The repository's -generation workflow also refreshes it on pushes to `dev`, so Mintlify always -receives the generated MDX as part of the published docs tree. - -The hosted preview is available at [opencode.mintlify.site](https://opencode.mintlify.site). diff --git a/packages/docs/api/index.mdx b/packages/docs/api/index.mdx deleted file mode 100644 index 102ff30324e2..000000000000 --- a/packages/docs/api/index.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: "API Reference" -description: "OpenCode HTTP API." ---- - -The endpoint reference is generated from the current OpenCode V2 [OpenAPI specification](/openapi.json). diff --git a/packages/docs/assets/favicon.svg b/packages/docs/assets/favicon.svg deleted file mode 100644 index 1beb80483d87..000000000000 --- a/packages/docs/assets/favicon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/packages/docs/assets/logo-dark.svg b/packages/docs/assets/logo-dark.svg deleted file mode 100644 index 9812b2308d93..000000000000 --- a/packages/docs/assets/logo-dark.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/packages/docs/assets/logo-light.svg b/packages/docs/assets/logo-light.svg deleted file mode 100644 index 0fa652b24b9a..000000000000 --- a/packages/docs/assets/logo-light.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/packages/docs/build/client.mdx b/packages/docs/build/client.mdx deleted file mode 100644 index 0465dd6fed21..000000000000 --- a/packages/docs/build/client.mdx +++ /dev/null @@ -1,183 +0,0 @@ ---- -title: "Client" -description: "Connect an application to the OpenCode HTTP API." ---- - -`@opencode-ai/client` is the generated TypeScript client for the OpenCode HTTP -API. Use it when your application connects to an OpenCode server over the -network. Its types and methods are generated from the same contract as the -[API reference](/api). - - - The V2 API and client are beta. Method names, inputs, and outputs may change - before the stable release. - - -## Install - -```sh -bun add @opencode-ai/client@next -``` - -## Create a client - -Create a client with the server URL, then call methods grouped by API resource: - -```ts -import { OpenCode } from "@opencode-ai/client" - -const client = OpenCode.make({ - baseUrl: "http://localhost:4096", -}) - -const session = await client.session.create({ - location: { directory: "/workspace" }, -}) - -await client.session.prompt({ - sessionID: session.id, - text: "Review the current changes", -}) -``` - -## Headers and requests - -Pass default authentication or application headers to `OpenCode.make` with -`headers`. You can also supply a custom `fetch` implementation. Each operation -accepts request options as its final argument for an `AbortSignal` or -per-request headers. - -```ts -const client = OpenCode.make({ - baseUrl: "https://opencode.example.com", - headers: { - authorization: `Bearer ${process.env.OPENCODE_TOKEN}`, - }, -}) - -await client.session.list(undefined, { - signal: AbortSignal.timeout(10_000), -}) -``` - -## Stream events - -Streaming endpoints return async iterables: - -```ts -for await (const event of client.event.subscribe()) { - console.log(event.type) -} -``` - -## Local background service - -The main client entrypoints are browser-compatible and do not include local -process management. In a Node application, import the native Promise service -API from `@opencode-ai/client/service`. - -- `Service.discover()` returns a healthy registered endpoint without starting - a process. -- `Service.ensure()` returns a compatible service, starting one when needed. -- `Service.stop()` stops the exact registered service instance. -- `Service.headers(endpoint)` creates the authentication headers for a client. - -```ts -import { OpenCode } from "@opencode-ai/client" -import { Service } from "@opencode-ai/client/service" - -const endpoint = await Service.ensure() -const client = OpenCode.make({ - baseUrl: endpoint.url, - headers: Service.headers(endpoint), -}) - -const health = await client.health.get() -``` - -`Service.ensure()` accepts an optional registration file, required version, -service command, and `onStart` callback: - -```ts -const endpoint = await Service.ensure({ - file: "/var/run/opencode/service.json", - version: "2.0.0", - command: ["opencode", "serve", "--service"], - onStart(reason, previousVersion) { - console.log(reason, previousVersion) - }, -}) -``` - -Omit these options to use the standard registration path and -`opencode serve --service` command. - -## Effect - -OpenCode provides a first-class Effect client through the -`@opencode-ai/client/effect` entrypoint. It returns typed Effects and Streams -and decodes responses into OpenCode schema values. - -```sh -bun add @opencode-ai/client@next effect -``` - -### Create a client - -```ts -import { AbsolutePath, Location, OpenCode } from "@opencode-ai/client/effect" -import { Effect } from "effect" -import { FetchHttpClient } from "effect/unstable/http" - -const program = Effect.gen(function* () { - const client = yield* OpenCode.make({ baseUrl: "http://localhost:4096" }) - const session = yield* client.session.create({ - location: Location.Ref.make({ - directory: AbsolutePath.make("/workspace"), - }), - }) - - return yield* client.session.get({ sessionID: session.id }) -}) - -const session = await Effect.runPromise( - program.pipe(Effect.provide(FetchHttpClient.layer)), -) -``` - -Streaming operations, including `client.event.subscribe()` and -`client.session.log(...)`, return Effect `Stream` values. - -### Local background service - -The Node-only `@opencode-ai/client/effect/service` entrypoint exposes the same -operations as Effect values. Add `@effect/platform-node` and provide its -filesystem layer when running them. - -```sh -bun add @effect/platform-node -``` - -```ts -import { NodeFileSystem } from "@effect/platform-node" -import { OpenCode } from "@opencode-ai/client/effect" -import { Service } from "@opencode-ai/client/effect/service" -import { Effect } from "effect" -import { FetchHttpClient } from "effect/unstable/http" - -const program = Effect.gen(function* () { - const endpoint = yield* Service.ensure() - const client = yield* OpenCode.make({ - baseUrl: endpoint.url, - headers: Service.headers(endpoint), - }) - return yield* client.health.get() -}) - -const health = await Effect.runPromise( - program.pipe( - Effect.provide(FetchHttpClient.layer), - Effect.provide(NodeFileSystem.layer), - ), -) -``` diff --git a/packages/docs/build/index.mdx b/packages/docs/build/index.mdx deleted file mode 100644 index b374ccc75b08..000000000000 --- a/packages/docs/build/index.mdx +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: "Build" -description: "Build on the engine used by millions daily." -mode: "wide" ---- - - - - Build plugins that add tools, integrations, commands, agents, and custom behavior while keeping the rest of OpenCode - intact. - - - Connect to OpenCode with the same client used by the TUI and desktop app, then build any interface, workflow, or agent - experience around it. - - - Embed OpenCode directly into your application and build a completely custom agent, interface, or developer product - around it. - - - - - The plugin API, client, and SDK are still being finalized during beta and may change before OpenCode 2.0 is stable. - diff --git a/packages/docs/build/plugins.mdx b/packages/docs/build/plugins.mdx deleted file mode 100644 index e358a3ccae9f..000000000000 --- a/packages/docs/build/plugins.mdx +++ /dev/null @@ -1,446 +0,0 @@ ---- -title: "Plugins" -description: "Extend OpenCode with plugins." ---- - -Plugins extend OpenCode in-process. They can transform agents, models, commands, -integrations, references, skills, and tools; intercept model requests and tool -execution; and call a subset of the V2 client. - - - The V2 plugin API is beta. Entrypoints, hooks, draft shapes, and configuration may change before the stable release. - Use the `/v2` exports described on this page. - - -## Load plugins - -Plugins can be loaded from npm packages, explicit local paths, or config -directories. Each module must have one default export containing a unique -plugin `id` and a `setup` function. - -### Configuration - -Add ordered entries to the `plugins` field in `opencode.json(c)`: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "plugins": [ - "opencode-acme-plugin@1.2.0", - "@acme/opencode-plugin", - "./plugins/local.ts", - { - "package": "./plugins/reviewer.ts", - "options": { - "agent": "reviewer", - "strict": true, - }, - }, - ], -} -``` - -A string is either a package specifier or a local path. Local paths must start -with `./` or `../` and resolve relative to the configuration file containing -the entry. Absolute paths and `file://` URLs are also supported. Both scoped -packages and versioned package specifiers are supported. - -Use the object form to pass JSON configuration to the plugin. OpenCode passes -`options` unchanged as `ctx.options`; omitted options become an empty object. -The plugin owns validation and defaults for its options. - -See [Config](/config#locations) for configuration locations and precedence. -Entries from all applicable files are processed from lowest to highest -precedence rather than replacing the entire array. - -### Local discovery - -OpenCode automatically scans this directory in every discovered OpenCode config -directory: - -```text -.opencode/plugins/ -``` - -The equivalent global directory is `~/.config/opencode/plugins/`. Direct `.ts` -and `.js` children are loaded. An immediate child directory is also loaded as a -package when OpenCode can resolve a string `exports`, `module`, or `main` -entrypoint, or an `index.ts` or `index.js` file. - -A `plugins/` directory beside a project-root `opencode.json` is not discovered -automatically. Put it under `.opencode/`, or add its file explicitly with a -relative config entry. - -### Enable and disable - -A string beginning with `-` disables plugins by their exported `id`. `*` -matches every ID, and a suffix of `.*` matches an ID prefix. Directives are -applied in order: - -```jsonc title="opencode.jsonc" -{ - "plugins": ["./plugins/reviewer.ts", "-acme.reviewer", "-opencode.provider.*", "opencode.provider.openai"], -} -``` - -Package specifiers and local paths locate plugin modules; they are not disable -selectors. Use the `id` from the plugin's default export to disable it. A later -ID entry re-enables a loaded or built-in plugin. Explicit config directives run -after local auto-discovery, so they can disable discovered plugins by ID. - -User plugins are activated in configured order between OpenCode's internal -plugin phases. Hooks run sequentially in registration order, and later hooks -observe earlier mutations. Do not depend on the internal phase ordering while -the API is beta. - -### Installation and dependencies - -OpenCode installs bare package entries and their production dependencies into -an isolated cache. Package installation does not run lifecycle scripts. -Published packages should expose their plugin entrypoint and include every -runtime import in `dependencies`. - -Local files and local package directories are imported directly. OpenCode does -**not** install their dependencies. Install dependencies in a `package.json` -visible from the plugin file, for example: - -```sh -cd .opencode -bun add @opencode-ai/plugin@next -``` - -Match the plugin package version to the OpenCode release you target. - -Configuration and discovered plugin files under watched config directories are -reloaded when they change. Reloading replaces the active plugin generation and -releases its scoped registrations. Restart OpenCode after changing an npm -package version or a local dependency when no watched file changed. - -## Create a plugin - -Export the result of `Plugin.define` as the module default: - -```ts title=".opencode/plugins/reviewer.ts" -import { Plugin } from "@opencode-ai/plugin/v2" - -export default Plugin.define({ - id: "acme.reviewer", - setup: async (ctx) => { - const description = - typeof ctx.options.description === "string" ? ctx.options.description : "Reviews code for regressions" - - await ctx.agent.transform((agents) => { - agents.update("reviewer", (agent) => { - agent.description = description - agent.mode = "subagent" - }) - }) - }, -}) -``` - -`setup` runs each time the plugin is activated. Register long-lived behavior -during setup; do not wait there on an infinite event stream. It may return a -synchronous or asynchronous cleanup function. OpenCode awaits that cleanup -when the plugin is disabled, reloaded, or shut down: - -```ts -setup: async (ctx) => { - const controller = new AbortController() - const task = synchronize(ctx, controller.signal) - - return async () => { - controller.abort() - await task - } -} -``` - -Hook registrations are released automatically with the same plugin scope. Use -the returned cleanup for resources the plugin owns, such as timers, watchers, -connections, and background tasks. - -### Context - -The plugin context is essentially an [OpenCode server client](/build/client). -Its read and action methods use the same inputs and responses as the client. It -adds plugin-only methods for transforms, runtime hooks, reloads, registrations, -and plugin options. - -| Capability | Available operations | -| ---------------------- | -------------------------------------------------------------------------------------------- | -| `ctx.agent` | `list`, `get`, `transform`, `reload` | -| `ctx.catalog.provider` | `list`, `get` | -| `ctx.catalog.model` | `list`, `get`, `default` | -| `ctx.catalog` | `transform`, `reload` | -| `ctx.command` | `list`, `transform`, `reload` | -| `ctx.integration` | `list`, `get`, `connect`, `attempt`, `transform`, `reload`, and connection lookup/resolution | -| `ctx.plugin` | `list` currently active plugin IDs | -| `ctx.reference` | `list`, `transform`, `reload` | -| `ctx.session` | `create`, `get`, `prompt`, `command`, `synthetic`, `interrupt`, and `hook` | -| `ctx.skill` | `list`, `transform`, `reload` | -| `ctx.tool` | `transform` and `hook` | -| `ctx.aisdk` | `hook` | -| `ctx.event` | `subscribe` to the current public server event stream | -| `ctx.options` | Readonly options from the matching config object | - -### Transform hooks - -Transform hooks let a plugin modify how OpenCode is configured. Use them to add -or remove definitions, override settings, choose defaults, and provide tools or -other sources. - -| Transform | Draft operations | -| ----------------------- | ------------------------------------------------------------------------------------------------------- | -| `agent.transform` | `list`, `get`, `default`, `update`, `remove` | -| `catalog.transform` | Provider `list`, `get`, `update`, `remove`; model `get`, `update`, `remove`; default model `get`, `set` | -| `command.transform` | `list`, `get`, `update`, `remove` | -| `integration.transform` | Integration `list`, `get`, `update`, `remove`; method `list`, `update`, `remove` | -| `reference.transform` | `add`, `remove`, `list` | -| `skill.transform` | `source`, `list` | -| `tool.transform` | `add` | - -Here's an example that keeps models synced from a remote source: - -```js title=".opencode/plugins/remote-models.js" -import { Plugin } from "@opencode-ai/plugin/v2" - -export default Plugin.define({ - id: "acme.remote-models", - setup: async (ctx) => { - let models = [] - - await ctx.catalog.transform((catalog) => { - for (const model of models) { - catalog.model.update(model.providerID, model.id, (draft) => Object.assign(draft, model)) - } - }) - - const refresh = async () => { - const response = await fetch("https://example.com/opencode/models.json", { - signal: AbortSignal.timeout(10_000), - }) - if (!response.ok) return - models = await response.json() - await ctx.catalog.reload() - } - - await refresh() - const timer = setInterval(() => void refresh().catch(console.error), 60_000) - return () => clearInterval(timer) - }, -}) -``` - -`ctx.catalog.reload()` replays every catalog transform to derive the new -catalog. Each plugin's logic remains composed with the others, so a later -plugin can still modify models added by an earlier one. The catalog updates -without restarting OpenCode. - -### Runtime hooks - -Runtime hooks intercept live operations. Their event objects expose specific -mutable fields: - -| Hook | Mutable fields | -| ------------------------------------------- | ------------------------------------------------------------------------------ | -| `ctx.aisdk.hook("sdk", callback)` | `sdk`, after inspecting `model`, `package`, and `options` | -| `ctx.aisdk.hook("language", callback)` | `language`, after inspecting `model`, `sdk`, and `options` | -| `ctx.session.hook("request", callback)` | `system`, `messages`, and the `tools` record immediately before model dispatch | -| `ctx.tool.hook("execute.before", callback)` | `input`, before the selected tool executes | -| `ctx.tool.hook("execute.after", callback)` | Terminal `content`, `metadata`, and `outputPaths`; `error` on failure | - -For example, remove a tool from selected model requests and normalize another -tool's input: - -```ts title=".opencode/plugins/guards.ts" -import { Plugin } from "@opencode-ai/plugin/v2" - -export default Plugin.define({ - id: "acme.guards", - setup: async (ctx) => { - await ctx.session.hook("request", (event) => { - delete event.tools.write - }) - - await ctx.tool.hook("execute.before", (event) => { - if (event.tool !== "lookup" || typeof event.input !== "object" || event.input === null) return - event.input = { ...event.input, source: "plugin" } - }) - }, -}) -``` - -A hook failure fails the operation it intercepts. Keep runtime hooks fast and -handle expected errors inside the callback. - -## Examples - -### Add a tool - -Create an executable tool with `Tool.make`, then register it with a name -and registration options. Define its input with JSON Schema and use an async -executor: - -```js title=".opencode/plugins/greeting.js" -import { Plugin } from "@opencode-ai/plugin/v2" -import { Tool } from "@opencode-ai/plugin/v2/tool" - -export default Plugin.define({ - id: "acme.greeting", - setup: async (ctx) => { - await ctx.tool.transform((tools) => { - tools.add( - "greeting", - Tool.make({ - description: "Create a greeting", - input: { - type: "object", - properties: { - name: { type: "string" }, - }, - required: ["name"], - additionalProperties: false, - }, - output: { - type: "object", - properties: { greeting: { type: "string" } }, - required: ["greeting"], - additionalProperties: false, - }, - execute: async ({ name }) => { - const text = `Hello, ${name}!` - return { - output: { greeting: text }, - content: text, - } - }, - }), - ) - }) - }, -}) -``` - -Unsupported characters in tool names are normalized to underscores. Namespace -segments must begin with a letter, contain at most 64 letters, digits, -underscores, or hyphens, and are joined with dots. Pass the optional third -argument to `tools.add` to configure the registration with -`{ namespace, codemode }`: - -- `namespace` prefixes and groups the exposed tool name. -- `codemode` defaults to `true` and makes the tool available through the - `execute` CodeMode tool. Set `codemode: false` to expose it directly to the - provider. - -The executor receives a second context argument containing `sessionID`, -`agent`, `messageID`, `callID`, and `progress`. A tool with `output` -must return `output`; Effect and Standard Schema codecs validate it, while raw -JSON Schema definitions enforce JSON compatibility only. A tool -without `output` returns model-visible `content` instead. - -### Add a command - -```js title=".opencode/plugins/review-command.js" -import { Plugin } from "@opencode-ai/plugin/v2" - -export default Plugin.define({ - id: "acme.review-command", - setup: async (ctx) => { - await ctx.command.transform((commands) => { - commands.update("review", (command) => { - command.description = "Review the current changes" - command.template = "Review the current changes for correctness and missing tests." - }) - }) - }, -}) -``` - -### Set the default model - -```js title=".opencode/plugins/default-model.js" -import { Plugin } from "@opencode-ai/plugin/v2" - -export default Plugin.define({ - id: "acme.default-model", - setup: async (ctx) => { - await ctx.catalog.transform((catalog) => { - catalog.model.default.set("anthropic", "claude-sonnet-4-5") - }) - }, -}) -``` - -## Publish a package - -A package plugin uses the same default export as a local plugin. A minimal -manifest is: - -```json title="package.json" -{ - "name": "opencode-acme-plugin", - "version": "1.0.0", - "type": "module", - "exports": "./src/index.ts", - "dependencies": { - "@opencode-ai/plugin": "next" - } -} -``` - -Use versions compatible with the OpenCode release you target and test the -installed package, not only a workspace-linked copy. Because the plugin API is -beta, publish compatible plugin updates when V2 entrypoints or contracts -change. - -## Verify loading - -List active plugin IDs through the V2 API: - -```sh -opencode2 api get /api/plugin -``` - -If a plugin is absent, check the server log described in -[Troubleshooting](/troubleshooting#read-logs). Invalid modules and setup failures are -logged; one failing package does not prevent unrelated valid packages from -being resolved. - -## Effect - -OpenCode provides a first-class Effect API for plugins through the -`@opencode-ai/plugin/v2/effect` entrypoint. Install `effect` alongside the -plugin package and export an `effect` function instead of `setup`: - -```sh -bun add @opencode-ai/plugin@next effect -``` - -```ts title=".opencode/plugins/reviewer-effect.ts" -import { Plugin } from "@opencode-ai/plugin/v2/effect" -import { Effect } from "effect" - -export default Plugin.define({ - id: "acme.reviewer-effect", - effect: (ctx) => - Effect.gen(function* () { - yield* ctx.agent.transform((agents) => { - agents.update("reviewer", (agent) => { - agent.description = "Reviews code for regressions" - agent.mode = "subagent" - }) - }) - }), -}) -``` - -Context operations return Effects. The plugin effect is scoped, so finalizers, -fibers, and registrations are released when the plugin reloads or unloads. -OpenCode does not expose its private Core services to the plugin; use the -capabilities on `ctx`. - -Typed tools can use `Schema` from `effect` and `Tool.make` from -`@opencode-ai/plugin/v2/effect/tool`. Effect and Promise plugins use the same -`tools.add(name, tool, options?)` registration shape. Effect executors -return an Effect and may fail with the typed tool failure channel. diff --git a/packages/docs/build/sdk.mdx b/packages/docs/build/sdk.mdx deleted file mode 100644 index afc29dca0b4b..000000000000 --- a/packages/docs/build/sdk.mdx +++ /dev/null @@ -1,84 +0,0 @@ ---- -title: "SDK" -description: "Embed OpenCode directly in your application." ---- - -We're working on a general-purpose SDK for embedding OpenCode directly inside -your application. The regular SDK is coming soon. - -An Effect-native version is available now for applications built with Effect. -Its current documentation is below. For other applications, run OpenCode as a -server and use the [TypeScript client](/build/client) in the meantime. - -## Effect - -`@opencode-ai/sdk-next` hosts OpenCode in-process. Unlike the -[network client](/build/client), it assembles the OpenCode server and routes API -calls through its HTTP router in memory. It opens no HTTP listener and adds no -network hop between the client and server. - - - The V2 SDK is beta and currently private to the OpenCode workspace. It is not - published for external installation yet, and its package name and API may - change before release. - - -## Create a host - -`OpenCode.create()` creates a scoped host. Closing its Effect Scope releases -the router, location services, fibers, and scoped plugin registrations. - -```ts -import { - AbsolutePath, - Location, - OpenCode, -} from "@opencode-ai/sdk-next" -import { Effect } from "effect" - -const program = Effect.scoped( - Effect.gen(function* () { - const opencode = yield* OpenCode.create() - const session = yield* opencode.sessions.create({ - location: Location.Ref.make({ - directory: AbsolutePath.make("/workspace"), - }), - }) - - return yield* opencode.sessions.get({ sessionID: session.id }) - }), -) - -const session = await Effect.runPromise(program) -``` - -The embedded host uses the same routes, middleware, codecs, errors, and schema -values as `@opencode-ai/client/effect`. It exposes the full generated client and -adds the convenience aliases `sessions` and `events` for the session and event -groups. - -## Use as a service - -Use `OpenCode.layer` when the host should be provided through Effect dependency -injection: - -```ts -import { OpenCode } from "@opencode-ai/sdk-next" -import { Effect } from "effect" - -const program = Effect.gen(function* () { - const opencode = yield* OpenCode.Service - return yield* opencode.sessions.active() -}) - -const active = await Effect.runPromise( - program.pipe(Effect.provide(OpenCode.layer)), -) -``` - -## Register plugins - -Call `opencode.plugin(...)` to register an embedded V2 plugin. Embedded plugins -use the same discovery and location-scoped activation path as configured -plugins. The SDK also exports `Tool` for plugin-defined tools. See the -[Plugins guide](/build/plugins) for the plugin shape and available hooks. diff --git a/packages/docs/docs.json b/packages/docs/docs.json deleted file mode 100644 index a9349f29639f..000000000000 --- a/packages/docs/docs.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "$schema": "https://mintlify.com/docs.json", - "theme": "mint", - "name": "OpenCode", - "description": "OpenCode documentation.", - "colors": { - "primary": "#3B7DD8", - "light": "#3B7DD8", - "dark": "#FAB283" - }, - "favicon": "/assets/favicon.svg", - "logo": { - "light": "/assets/logo-light.svg", - "dark": "/assets/logo-dark.svg" - }, - "navigation": { - "tabs": [ - { - "tab": "Docs", - "groups": [ - { - "group": "Get started", - "pages": ["index", "migrate-v1", "config", "troubleshooting"] - }, - { - "group": "Configure", - "pages": [ - "providers", - "models", - "agents", - "permissions", - "sharing", - "snapshots", - "commands", - "skills", - "instructions", - "mcp-servers", - "attachments", - "compaction", - "warming", - "themes", - "formatters", - "lsp", - "references" - ] - } - ] - }, - { - "tab": "Build", - "pages": ["build/index", "build/plugins", "build/client", "build/sdk"] - }, - { - "tab": "API", - "groups": [ - { - "group": "Overview", - "pages": ["api/index"] - }, - { - "group": "Endpoints", - "openapi": "openapi.json" - } - ] - } - ], - "global": {} - }, - "contextual": { - "options": ["copy", "view", "chatgpt", "claude", "mcp", "cursor", "vscode"] - }, - "footer": { - "socials": { - "github": "https://github.com/anomalyco/opencode", - "x": "https://x.com/opencode" - } - } -} diff --git a/packages/docs/lsp.mdx b/packages/docs/lsp.mdx deleted file mode 100644 index f7b683dc1e19..000000000000 --- a/packages/docs/lsp.mdx +++ /dev/null @@ -1,105 +0,0 @@ ---- -title: "LSP" -description: "" ---- - -Language Server Protocol (LSP) integrations can provide code diagnostics, -symbols, definitions, references, and other language-aware context. - - - OpenCode V2 does not yet have an LSP runtime or built-in language servers. - The `lsp` configuration is accepted and preserved, but it does not currently - start or download servers, expose an LSP tool, or add diagnostics to file tool - results. - - -## Built-in servers - -There are no built-in LSP servers in the current V2 implementation. Setting -`lsp` to `true` declares that built-ins should be enabled, but has no runtime -effect until V2 provides a server registry and LSP runtime. - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "lsp": true -} -``` - -## Configuration - -The `lsp` field accepts a boolean or an object keyed by server name: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "lsp": { - "custom-typescript": { - "command": ["typescript-language-server", "--stdio"], - "extensions": [".ts", ".tsx"], - "env": { - "TSS_LOG": "-level verbose" - }, - "initialization": { - "preferences": { - "importModuleSpecifierPreference": "relative" - } - } - } - } -} -``` - -Each enabled server entry has this shape: - -| Property | Type | Required | Description | -| --- | --- | --- | --- | -| `command` | `string[]` | Yes | Executable followed by any arguments. | -| `extensions` | `string[]` | No | File extensions associated with the server, including the leading dot. | -| `disabled` | `boolean` | No | Disables the entry when `true`. | -| `env` | `Record` | No | Environment variables for the server process. The property is named `env`, not `environment`. | -| `initialization` | `Record` | No | Server-specific options for the LSP `initialize` request. | - -The only entry that may omit `command` is the disable-only form: - -```jsonc -{ - "lsp": { - "typescript": { - "disabled": true - } - } -} -``` - -Server names are arbitrary. The V2 schema permits `extensions` to be omitted, -including for a custom server, although a future runtime will need a way to -associate that server with files. - -## Disable LSP - -Omit `lsp` when no configuration is needed. Set it to `false` to explicitly -disable the whole integration, including when a lower-priority configuration -set it to `true` or supplied an object: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "lsp": false -} -``` - -Use `{ "disabled": true }` under a server name to disable one server while -retaining the object form. `OPENCODE_DISABLE_LSP_DOWNLOAD` is not used by V2; -V2 currently performs no automatic LSP downloads. - -## Current usage - -V2 loads and validates the configuration shape for compatibility and future -integration. It does not currently use LSP when reading, writing, editing, or -patching files, and those tools do not notify a language server or return LSP -diagnostics. - -For reliable feedback today, have the agent run the project's lint, typecheck, -test, or compiler commands. Record those commands in an `AGENTS.md` file or a -skill so the agent knows when and where to run them. diff --git a/packages/docs/package.json b/packages/docs/package.json deleted file mode 100644 index b8a9ead7cae2..000000000000 --- a/packages/docs/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/package.json", - "name": "@opencode-ai/docs", - "private": true, - "scripts": { - "dev": "bun run generate && bun --bun mint dev --no-open --port 3333", - "generate": "bun script/generate-theme-tokens.ts", - "check:generated": "bun script/generate-theme-tokens.ts --check", - "validate": "bun run check:generated && bun --bun mint validate", - "broken-links": "bun --bun mint broken-links" - }, - "devDependencies": { - "effect": "catalog:", - "mint": "4.2.666", - "prettier": "3.6.2" - } -} diff --git a/packages/docs/permissions.mdx b/packages/docs/permissions.mdx deleted file mode 100644 index 6b9659deb28f..000000000000 --- a/packages/docs/permissions.mdx +++ /dev/null @@ -1,209 +0,0 @@ ---- -title: "Permissions" -description: "" ---- - -Permissions control whether an agent may perform an action on a resource. V2 -configuration uses the `permissions` field and an ordered array of rules. - - - The V1 object syntax uses different field and action names. Do not use - `permission`, `bash`, or `task` in V2 configuration; use `permissions`, - `shell`, and `subagent`. - - -## Rule schema - -Each rule has three required string fields: - -```jsonc -{ - "$schema": "https://opencode.ai/config.json", - "permissions": [ - { "action": "*", "resource": "*", "effect": "ask" }, - { "action": "read", "resource": "*", "effect": "allow" }, - { "action": "read", "resource": "*.env", "effect": "deny" }, - { "action": "shell", "resource": "git status *", "effect": "allow" }, - { "action": "shell", "resource": "git push *", "effect": "deny" }, - { "action": "edit", "resource": "packages/docs/*.mdx", "effect": "allow" } - ] -} -``` - -- `action` matches a tool permission action. -- `resource` matches the value the tool is trying to use, such as a path, - command, URL, query, or agent ID. -- `effect` is `"allow"`, `"deny"`, or `"ask"`. - -`allow` proceeds without prompting, `deny` blocks the operation, and `ask` -waits for a user decision. If no rule matches, the result is `ask`. - -## Matching and order - -Both `action` and `resource` support simple wildcards: - -- `*` matches zero or more characters, including `/`. -- `?` matches exactly one character. -- All other characters are literal. - -Matches cover the entire value. Slashes are normalized, and matching is -case-insensitive on Windows. For shell convenience, a pattern ending in -`" *"` also matches the command without arguments: `"git status *"` matches -both `git status` and `git status --short`. - -The **last matching rule wins**. Put broad rules first and exceptions later. -Rules from lower-priority configuration files are loaded first. OpenCode then -appends all global rules before agent-specific rules, so a matching agent rule -overrides a global rule. - -Some operations check several resources at once, such as a patch touching -multiple files. OpenCode denies the operation if any resource resolves to -`deny`; otherwise it asks if any resolves to `ask`; otherwise it allows it. - -## Actions and resources - -V2 action names are strings, so plugins may introduce additional actions. The -current built-in actions use these resources: - -| Action | Resource matched | -| --- | --- | -| `read` | Location-relative path for an internal file or directory; canonical absolute path for an external target | -| `edit` | Target path for `edit`, `write`, and `patch`; all three tools share this action | -| `glob` | The requested glob pattern | -| `grep` | The requested regular expression, not the search path | -| `shell` | The complete raw shell command string | -| `subagent` | The target agent ID | -| `skill` | The skill ID | -| `question` | `*` | -| `webfetch` | The requested URL | -| `websearch` | The search query | -| `external_directory` | A canonical external directory boundary, normally ending in `/*` | -| `_` | `*` for an MCP tool; unsupported characters in both names become `_` | -| `execute` | `*`; controls availability of the Code Mode dispatcher, while each nested tool still enforces its own permission | - -Built-in agent policy also reserves `plan_enter` and `plan_exit` for plan-mode -transitions. `doom_loop` and `lsp` are not current V2 Core permission actions. - -## External directories - -An external path requires a separate `external_directory` decision before the -tool's own `read` or `edit` decision. This applies to external paths used by -`read`, `edit`, `write`, and `patch`, and to an external `shell` working -directory. - -```jsonc -{ - "$schema": "https://opencode.ai/config.json", - "permissions": [ - { - "action": "external_directory", - "resource": "~/projects/reference/*", - "effect": "allow" - }, - { - "action": "read", - "resource": "~/projects/reference/*", - "effect": "allow" - }, - { - "action": "edit", - "resource": "~/projects/reference/*", - "effect": "deny" - } - ] -} -``` - -For `external_directory`, `read`, and `edit` resources, a leading `~`, `~/`, -`$HOME`, or `$HOME/` is expanded when configuration loads. Shell resources are -raw command text and are **not** home-expanded. - - - `shell` runs with the host user's filesystem, process, and network authority. - Its resource is raw text, not a parsed command. External command arguments - produce only best-effort warnings; `external_directory` is enforced for the - working directory, not every path embedded in a command. Prefer a narrow - shell allowlist over patterns intended to identify every dangerous command. - - -Relative mutation paths cannot escape the active Location, and symlink escapes -from inside it are rejected. Explicit external paths are canonicalized before -matching, so authorize only trusted directory boundaries. - -## Defaults - -The evaluator's fallback is `ask`, but shipped agents include ordered defaults: - -| Agent | Effective default policy | -| --- | --- | -| `build` | Allows most actions; asks for external directories and `.env` reads; allows questions and entering plan mode; denies exiting plan mode | -| `plan` | Uses the same base, allows questions and exiting plan mode, and denies edits except OpenCode plan files | -| `general` | Uses the base policy but cannot launch another subagent; questions and plan transitions remain denied | -| `explore` | Denies everything except `read`, `glob`, `grep`, `webfetch`, and `websearch`; cannot launch subagents and asks for external directories | -| Hidden maintenance agents | Deny all actions | - -The base read rules are ordered as follows: - -```jsonc -[ - { "action": "read", "resource": "*", "effect": "allow" }, - { "action": "read", "resource": "*.env", "effect": "ask" }, - { "action": "read", "resource": "*.env.*", "effect": "ask" }, - { "action": "read", "resource": "*.env.example", "effect": "allow" } -] -``` - -OpenCode also permits its managed tool-output and temporary directories where -needed. These exceptions do not grant general external-directory access. - -## Agent overrides - -Configure shared policy at the top level and append narrower rules to a named -agent under `agents..permissions`: - -```jsonc -{ - "$schema": "https://opencode.ai/config.json", - "permissions": [ - { "action": "shell", "resource": "*", "effect": "ask" }, - { "action": "shell", "resource": "git diff *", "effect": "allow" }, - { "action": "shell", "resource": "git status *", "effect": "allow" } - ], - "agents": { - "reviewer": { - "description": "Review code without changing it", - "mode": "subagent", - "permissions": [ - { "action": "edit", "resource": "*", "effect": "deny" }, - { "action": "shell", "resource": "git diff *", "effect": "allow" }, - { "action": "shell", "resource": "git status *", "effect": "allow" } - ] - } - } -} -``` - -Agent rules do not replace the global array; they are appended after it. A -custom subagent executes with its own permissions, not a permission subset -derived from the parent agent. - -## Approval choices - -When an `ask` rule matches, clients can reply with: - -- **Allow once** (`once`): approve only the pending request. -- **Allow always** (`always`): approve this request and save the patterns - proposed by the tool for the current project. -- **Reject** (`reject`): reject the request. Rejecting also rejects other - pending permission requests in the same session; clients may attach feedback. - -Saved approvals are durable and project-scoped. They are additional `allow` -rules, but they can never override a configured `deny`. The proposed saved -pattern may be broader than the displayed resource: several tools propose `*`, -shell proposes the exact command text, and skills and subagents propose their -IDs. Review the confirmation carefully and remove saved approvals that are no -longer needed. - -For non-interactive runs, `opencode2 run --auto` replies `once` to permission -requests. It does not save approvals, and explicit `deny` rules remain enforced. -Without `--auto`, a non-interactive run rejects permission requests. diff --git a/packages/docs/references.mdx b/packages/docs/references.mdx deleted file mode 100644 index ca904f7df228..000000000000 --- a/packages/docs/references.mdx +++ /dev/null @@ -1,177 +0,0 @@ ---- -title: "References" -description: "" ---- - -References give OpenCode named access to directories outside the current -project. Use them for documentation, shared libraries, examples, or source from -another repository. - -Configure references by alias in `opencode.json` or `opencode.jsonc`: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "references": { - "docs": { - "path": "../product-docs", - "description": "Use for product behavior and terminology" - }, - "effect": { - "repository": "Effect-TS/effect", - "branch": "main", - "description": "Use for Effect implementation details" - } - } -} -``` - -## Local directories - -Use `path` for a local directory: - -```jsonc -{ - "references": { - "design-system": { - "path": "../design-system", - "description": "Use when working with components or design tokens" - } - } -} -``` - -Relative paths resolve from the directory containing the config file that -defines them. Absolute paths and home-relative paths such as `~/docs` are also -supported. - -The string shorthand is useful when no other fields are needed: - -```jsonc -{ - "references": { - "docs": "../docs", - "shared": "~/work/shared" - } -} -``` - - - A shorthand string is treated as a local path only when it starts with `.`, - `/`, or `~`. Use `./docs`, not `docs`; a bare `docs` value is interpreted as - a Git repository. - - -## Git repositories - -Use `repository` for a remote Git repository. GitHub `owner/repo` shorthand, -Git URLs, host/path forms, and SCP-style remotes are supported. - -```jsonc -{ - "references": { - "effect": { - "repository": "Effect-TS/effect", - "branch": "main" - }, - "internal-sdk": { - "repository": "git@gitlab.example.com:platform/sdk.git", - "branch": "release/v2" - } - } -} -``` - -Without `branch`, OpenCode checks out and refreshes the remote's default -branch. Branch names may contain letters, numbers, `/`, `_`, `.`, and `-`, but -cannot start with `-` or contain `..`. Local `file:` repositories are not -supported. - -Git references also support shorthand: - -```jsonc -{ - "references": { - "effect": "Effect-TS/effect", - "sdk": "gitlab.com/platform/sdk" - } -} -``` - -### Cloning and storage - -OpenCode normalizes a remote and stores one checkout under its global data -directory at `opencode/repos//`. On a typical Linux -installation, for example, `Effect-TS/effect` is stored at: - -```text -~/.local/share/opencode/repos/github.com/Effect-TS/effect -``` - -Missing repositories are cloned. Existing checkouts are fetched and reset to -the requested branch, or to the remote default branch when `branch` is omitted. -Materialization runs asynchronously when references load or reload, so a new -reference can appear before its checkout is ready. Clone and refresh failures -are logged and do not stop other references from loading. - - - The cache has one checkout per normalized remote, not one per branch. Do not - configure the same repository at multiple branches; only one branch can be - exposed. Avoid editing cached checkouts because a refresh resets them. - - -## Description and visibility - -`description` tells agents when a reference is relevant. References with a -description are included in agent instructions with their alias and resolved -path. References without one remain available in `@` autocomplete but are not -advertised automatically. - -Set `hidden` to `true` to remove a reference from TUI `@` autocomplete: - -```jsonc -{ - "references": { - "internal": { - "path": "../internal", - "description": "Use for internal service behavior", - "hidden": true - } - } -} -``` - -`hidden` controls only autocomplete visibility. It does not remove the -reference from the reference API or agent instructions when a description is -present. - -## Use references - -Type `@` in the TUI and select a reference alias to attach its root directory: - -```text -Compare the current implementation with @effect -``` - -The attachment provides a non-recursive listing of the root's immediate files -and directories. V2 currently attaches references by root alias; -`@alias/path` is not a reference-specific file browser. Ask the agent to -inspect a particular path when more detail is needed. - -References do not grant extra tool permissions. Access outside the active -Location remains subject to the agent's normal tool rules and the -`external_directory` permission. Editing a reference additionally requires the -applicable edit permission. - -## Fields - -| Field | Local | Git | Description | -| --- | --- | --- | --- | -| `path` | Required | No | Local directory path | -| `repository` | No | Required | Remote Git repository | -| `branch` | No | Optional | Branch to fetch and check out | -| `description` | Optional | Optional | Guidance describing when agents should use it | -| `hidden` | Optional | Optional | Hide it from TUI `@` autocomplete | - -An alias cannot be empty or contain `/`, `\`, whitespace, a backtick, or a -comma. diff --git a/packages/www/.gitignore b/packages/www/.gitignore index 311f12a41816..4b62e6071646 100644 --- a/packages/www/.gitignore +++ b/packages/www/.gitignore @@ -1,6 +1,4 @@ -.source -.tanstack -.wrangler -dist -node_modules -worker-configuration.d.ts +.blume/ +dist/ +node_modules/ +.blume-verify/ diff --git a/packages/www/AGENTS.md b/packages/www/AGENTS.md new file mode 100644 index 000000000000..aecbd9cc1a46 --- /dev/null +++ b/packages/www/AGENTS.md @@ -0,0 +1,22 @@ +# Website and documentation guide + +## Structure + +- This package owns the `opencode.ai` website. +- Add custom marketing routes as Astro files under `pages/`. +- The whole project is mounted at `/v2/` by `deployment.base`, and documentation lives under `content/docs/` at `/v2/docs` through `basePath` in `blume.config.ts`. +- Write documentation in MDX. Every page should have `title` and `description` frontmatter. +- Use parenthesized content folders for sidebar groups that must not add a URL segment. Keep ungrouped top-level pages directly under `content/docs/`. +- Put static files in `public/` and reference them with root-relative paths. +- The API reference is generated from `openapi.json`; do not duplicate endpoint documentation as hand-written MDX. +- Keep documentation aligned with the V2 packages. Do not use `packages/opencode` as the source of truth unless the task explicitly concerns V1. + +## Local development + +- Run `bun dev` from this package and preview the site at `http://localhost:3000/v2/`. +- Verify both custom marketing routes and documentation routes after changing shared navigation or layout code. + +## Validation + +- Run `bun typecheck`, `bun validate`, and `bun run build` from this package after documentation or configuration changes. +- Treat validation and build errors as blockers. diff --git a/packages/www/README.md b/packages/www/README.md index e9411d6607f3..cac200b4f5ab 100644 --- a/packages/www/README.md +++ b/packages/www/README.md @@ -1,6 +1,10 @@ # OpenCode website -The server-rendered `opencode.ai` website. It uses TanStack Start on Cloudflare Workers and serves the V2 documentation with Fumadocs. +The OpenCode V2 website, powered by Blume and deployed with Wrangler at `https://opencode.ai/v2/`. Blume mounts the documentation at `/v2/docs`, and `https://v2.opencode.ai` redirects to the same deployment. + +Wrangler deploys the site through Blume's Cloudflare server adapter. Documentation pages are prerendered, while custom dynamic routes and endpoints can run in the Worker. Production uses `opencode-www` at `opencode.ai/v2/`; dev uses `opencode-www-dev` at `dev.opencode.ai/v2/`. The `v2.opencode.ai` alias is handled by a Cloudflare Redirect Rule outside this project. + +The `deploy-www` GitHub workflow deploys the `dev` branch to the dev Worker and the `v2` branch to the production Worker. ## Development @@ -10,13 +14,12 @@ From this directory, run: bun dev ``` -The site opens at `http://localhost:3000`; documentation is available at `http://localhost:3000/docs`. +The site opens at `http://localhost:3000/v2/`; documentation is available at `http://localhost:3000/v2/docs`. ## Verification ```bash bun typecheck +bun validate bun run build ``` - -The existing `packages/web`, `packages/docs`, and `packages/stats/app` deployments remain in place until their routes have been migrated and verified. diff --git a/packages/www/blume.config.ts b/packages/www/blume.config.ts new file mode 100644 index 000000000000..3fbe5ffc3303 --- /dev/null +++ b/packages/www/blume.config.ts @@ -0,0 +1,43 @@ +import { defineConfig } from "blume" + +export default defineConfig({ + title: "OpenCode", + description: "The open source AI coding agent.", + basePath: "/docs", + logo: { + image: { + light: "/assets/logo-light.svg", + dark: "/assets/logo-dark.svg", + alt: "OpenCode", + }, + text: "", + href: "/", + }, + content: { + root: "content/docs", + }, + github: { + owner: "anomalyco", + repo: "opencode", + branch: "dev", + dir: "packages/www", + }, + navigation: { + tabs: [ + { label: "Docs", path: "/" }, + { label: "Build", path: "/build" }, + { label: "API", path: "/api" }, + ], + }, + openapi: { + enabled: true, + route: "/api", + spec: "./openapi.json", + }, + deployment: { + adapter: "cloudflare", + base: "/v2/", + output: "server", + site: process.env.BLUME_ENV === "dev" ? "https://dev.opencode.ai" : "https://opencode.ai", + }, +}) diff --git a/packages/www/components.ts b/packages/www/components.ts new file mode 100644 index 000000000000..3af286bf60df --- /dev/null +++ b/packages/www/components.ts @@ -0,0 +1,8 @@ +import { defineComponents } from "blume" +import ThemeTokens from "./snippets/generated/theme-tokens.mdx" + +export default defineComponents({ + mdx: { + ThemeTokens, + }, +}) diff --git a/packages/docs/agents.mdx b/packages/www/content/docs/(Configure)/agents.mdx similarity index 99% rename from packages/docs/agents.mdx rename to packages/www/content/docs/(Configure)/agents.mdx index ca7ee80a2075..ab11c6470cd6 100644 --- a/packages/docs/agents.mdx +++ b/packages/www/content/docs/(Configure)/agents.mdx @@ -227,10 +227,10 @@ shell commands, `edit` for all edit/write/patch tools, and `subagent` for child agents. Other tools generally use their tool name, such as `read`, `glob`, `grep`, `webfetch`, `websearch`, and `skill`. - + Put broad wildcard rules first and exceptions afterward. For example, deny all subagents first, then allow `explore`. - + `~` and `$HOME` are expanded in filesystem resources for `read`, `edit`, and `external_directory`. Shell resources are raw command text and are not @@ -274,10 +274,10 @@ The V2 schema accepts per-agent request `headers` and JSON `body` overlays: } ``` - + The current V2 session runner preserves these overlays on the agent definition but does not yet apply them to model requests. Configure effective request settings on the provider, model, or model variant instead. Do not use legacy top-level agent fields such as `temperature`, `top_p`, `prompt`, `permission`, `tools`, `disable`, or `maxSteps` in new V2 configuration. - + diff --git a/packages/docs/attachments.mdx b/packages/www/content/docs/(Configure)/attachments.mdx similarity index 98% rename from packages/docs/attachments.mdx rename to packages/www/content/docs/(Configure)/attachments.mdx index 7f97859bf420..789ed19d641c 100644 --- a/packages/docs/attachments.mdx +++ b/packages/www/content/docs/(Configure)/attachments.mdx @@ -17,12 +17,12 @@ and other binary prompt attachments are not currently included in the model request. Some clients may let you select a PDF, but V2 does not yet make that PDF visible to the model. - + Use a model that supports image input before attaching an image. OpenCode passes supported image media to the selected provider, but the provider and model still enforce their own formats, dimensions, file counts, and size limits. A text-only model may reject the request. - + ## Add attachments @@ -131,12 +131,12 @@ All fields are optional: | `max_height` | `2000` | Maximum height in pixels. Must be a positive integer. | | `max_base64_bytes` | `5242880` | Maximum byte length of the Base64-encoded image string. Must be a positive integer. | - + In the current V2 runtime, these settings apply to image media produced by the built-in `read` tool. Images attached directly through the TUI, desktop, web, CLI, or API bypass this normalization. Resize direct attachments before adding them if the provider requires smaller media. - + The `read` tool recognizes PNG, JPEG, GIF, and WebP by their contents and will ingest at most 20 MiB of source image bytes. It decodes the image and compares diff --git a/packages/docs/commands.mdx b/packages/www/content/docs/(Configure)/commands.mdx similarity index 99% rename from packages/docs/commands.mdx rename to packages/www/content/docs/(Configure)/commands.mdx index 0f939819fce7..8b9e28e70657 100644 --- a/packages/docs/commands.mdx +++ b/packages/www/content/docs/(Configure)/commands.mdx @@ -137,10 +137,10 @@ project location and inserts its combined output into the template. Argument interpolation happens first, so avoid placing untrusted arguments inside shell interpolations. - + Shell interpolations run when the command is evaluated, outside the agent's tool permission flow. Only use commands from sources you trust. - + No other template interpolation is performed. In particular, an `@path` written into a stored template remains ordinary prompt text; V2 does not diff --git a/packages/docs/compaction.mdx b/packages/www/content/docs/(Configure)/compaction.mdx similarity index 100% rename from packages/docs/compaction.mdx rename to packages/www/content/docs/(Configure)/compaction.mdx diff --git a/packages/docs/formatters.mdx b/packages/www/content/docs/(Configure)/formatters.mdx similarity index 98% rename from packages/docs/formatters.mdx rename to packages/www/content/docs/(Configure)/formatters.mdx index 984c5a92971a..498b6bd1a5a1 100644 --- a/packages/docs/formatters.mdx +++ b/packages/www/content/docs/(Configure)/formatters.mdx @@ -6,10 +6,10 @@ description: "" OpenCode V2 accepts formatter configuration, but it does not yet include a formatter runtime. File writes and edits are not automatically formatted. - + V2 currently has no built-in formatters. The built-in formatter list and automatic post-edit formatting documented for V1 do not apply to V2. - + ## Configuration diff --git a/packages/docs/instructions.mdx b/packages/www/content/docs/(Configure)/instructions.mdx similarity index 98% rename from packages/docs/instructions.mdx rename to packages/www/content/docs/(Configure)/instructions.mdx index e7bebb42cc8f..526a74e2ddf1 100644 --- a/packages/docs/instructions.mdx +++ b/packages/www/content/docs/(Configure)/instructions.mdx @@ -43,10 +43,10 @@ If the Location is outside the project root, only the global file is loaded. Setting `OPENCODE_DISABLE_PROJECT_CONFIG=1` also skips project `AGENTS.md` discovery but does not disable the global file. - + Current V2 discovery only recognizes `AGENTS.md`. The `CLAUDE.md` fallback and related precedence described by older OpenCode documentation do not apply. - + ### Nested instructions @@ -80,13 +80,13 @@ Configuration is loaded from global through project-local files. If more than one config defines `instructions`, the highest-precedence, closest config's entire array is selected; arrays are not merged. - + V2 currently parses and retains this field but does not resolve its entries into instruction sources. Local files, glob patterns, and HTTP or HTTPS URLs in `instructions` therefore do not reach the model yet. Use `AGENTS.md` for active V2 instructions. URL fetching and timeout behavior documented for V1 are not supported by the current V2 implementation. - + See [Config](/config) for config locations and general precedence. diff --git a/packages/www/content/docs/(docs)/lsp.mdx b/packages/www/content/docs/(Configure)/lsp.mdx similarity index 98% rename from packages/www/content/docs/(docs)/lsp.mdx rename to packages/www/content/docs/(Configure)/lsp.mdx index f7b683dc1e19..9d8a69bcefb8 100644 --- a/packages/www/content/docs/(docs)/lsp.mdx +++ b/packages/www/content/docs/(Configure)/lsp.mdx @@ -6,12 +6,12 @@ description: "" Language Server Protocol (LSP) integrations can provide code diagnostics, symbols, definitions, references, and other language-aware context. - + OpenCode V2 does not yet have an LSP runtime or built-in language servers. The `lsp` configuration is accepted and preserved, but it does not currently start or download servers, expose an LSP tool, or add diagnostics to file tool results. - + ## Built-in servers diff --git a/packages/docs/mcp-servers.mdx b/packages/www/content/docs/(Configure)/mcp-servers.mdx similarity index 100% rename from packages/docs/mcp-servers.mdx rename to packages/www/content/docs/(Configure)/mcp-servers.mdx diff --git a/packages/www/content/docs/(Configure)/meta.ts b/packages/www/content/docs/(Configure)/meta.ts new file mode 100644 index 000000000000..9ba5316f5168 --- /dev/null +++ b/packages/www/content/docs/(Configure)/meta.ts @@ -0,0 +1,24 @@ +import { defineMeta } from "blume" + +export default defineMeta({ + title: "Configure", + pages: [ + "providers", + "models", + "agents", + "permissions", + "sharing", + "snapshots", + "commands", + "skills", + "instructions", + "mcp-servers", + "attachments", + "compaction", + "warming", + "themes", + "formatters", + "lsp", + "references", + ], +}) diff --git a/packages/docs/models.mdx b/packages/www/content/docs/(Configure)/models.mdx similarity index 100% rename from packages/docs/models.mdx rename to packages/www/content/docs/(Configure)/models.mdx diff --git a/packages/www/content/docs/(docs)/permissions.mdx b/packages/www/content/docs/(Configure)/permissions.mdx similarity index 99% rename from packages/www/content/docs/(docs)/permissions.mdx rename to packages/www/content/docs/(Configure)/permissions.mdx index 6b9659deb28f..c9e464be230b 100644 --- a/packages/www/content/docs/(docs)/permissions.mdx +++ b/packages/www/content/docs/(Configure)/permissions.mdx @@ -6,11 +6,11 @@ description: "" Permissions control whether an agent may perform an action on a resource. V2 configuration uses the `permissions` field and an ordered array of rules. - + The V1 object syntax uses different field and action names. Do not use `permission`, `bash`, or `task` in V2 configuration; use `permissions`, `shell`, and `subagent`. - + ## Rule schema @@ -118,13 +118,13 @@ For `external_directory`, `read`, and `edit` resources, a leading `~`, `~/`, `$HOME`, or `$HOME/` is expanded when configuration loads. Shell resources are raw command text and are **not** home-expanded. - + `shell` runs with the host user's filesystem, process, and network authority. Its resource is raw text, not a parsed command. External command arguments produce only best-effort warnings; `external_directory` is enforced for the working directory, not every path embedded in a command. Prefer a narrow shell allowlist over patterns intended to identify every dangerous command. - + Relative mutation paths cannot escape the active Location, and symlink escapes from inside it are rejected. Explicit external paths are canonicalized before diff --git a/packages/docs/providers.mdx b/packages/www/content/docs/(Configure)/providers.mdx similarity index 98% rename from packages/docs/providers.mdx rename to packages/www/content/docs/(Configure)/providers.mdx index a88eca912cfe..d23f82410d46 100644 --- a/packages/docs/providers.mdx +++ b/packages/www/content/docs/(Configure)/providers.mdx @@ -55,7 +55,7 @@ When several credential sources exist, OpenCode uses the stored credential first } ``` -Do not commit API keys or authorization headers to your repository. +Do not commit API keys or authorization headers to your repository. ## Configure diff --git a/packages/www/content/docs/(docs)/references.mdx b/packages/www/content/docs/(Configure)/references.mdx similarity index 98% rename from packages/www/content/docs/(docs)/references.mdx rename to packages/www/content/docs/(Configure)/references.mdx index ca904f7df228..72e589585522 100644 --- a/packages/www/content/docs/(docs)/references.mdx +++ b/packages/www/content/docs/(Configure)/references.mdx @@ -56,11 +56,11 @@ The string shorthand is useful when no other fields are needed: } ``` - + A shorthand string is treated as a local path only when it starts with `.`, `/`, or `~`. Use `./docs`, not `docs`; a bare `docs` value is interpreted as a Git repository. - + ## Git repositories @@ -114,11 +114,11 @@ Materialization runs asynchronously when references load or reload, so a new reference can appear before its checkout is ready. Clone and refresh failures are logged and do not stop other references from loading. - + The cache has one checkout per normalized remote, not one per branch. Do not configure the same repository at multiple branches; only one branch can be exposed. Avoid editing cached checkouts because a refresh resets them. - + ## Description and visibility diff --git a/packages/docs/sharing.mdx b/packages/www/content/docs/(Configure)/sharing.mdx similarity index 97% rename from packages/docs/sharing.mdx rename to packages/www/content/docs/(Configure)/sharing.mdx index 666d527bc04a..6fa6f48da235 100644 --- a/packages/docs/sharing.mdx +++ b/packages/www/content/docs/(Configure)/sharing.mdx @@ -7,10 +7,10 @@ Session sharing is not yet available in OpenCode V2. V2 does not currently publish sessions, upload conversation history to a sharing service, or create public links. - + The V2 TUI registers `/share`, but it currently only reports that sharing is unavailable. There is no functional share/unshare command or server API endpoint. - + ## Configuration diff --git a/packages/docs/skills.mdx b/packages/www/content/docs/(Configure)/skills.mdx similarity index 100% rename from packages/docs/skills.mdx rename to packages/www/content/docs/(Configure)/skills.mdx diff --git a/packages/docs/snapshots.mdx b/packages/www/content/docs/(Configure)/snapshots.mdx similarity index 98% rename from packages/docs/snapshots.mdx rename to packages/www/content/docs/(Configure)/snapshots.mdx index 6a697b04d6a0..6b14a9cf7716 100644 --- a/packages/docs/snapshots.mdx +++ b/packages/www/content/docs/(Configure)/snapshots.mdx @@ -58,10 +58,10 @@ Running `/undo` again moves the staged boundary to an earlier user message. Open immediately before the first undo as the redo baseline, so repeated undos form one wider staged revert rather than a redo stack. - + Sending a new prompt while an undo is staged commits the revert. The hidden message range is removed from the active session history, the currently reverted files are kept, and redo is no longer available. - + ## Redo @@ -103,6 +103,6 @@ are not included. Review the staged file summary and your Git diff before continuing. Commit or back up important work independently before using undo on a dirty worktree. - + `/undo` and `/redo` are interactive TUI commands. The non-interactive `run` command does not provide them. - + diff --git a/packages/docs/themes.mdx b/packages/www/content/docs/(Configure)/themes.mdx similarity index 97% rename from packages/docs/themes.mdx rename to packages/www/content/docs/(Configure)/themes.mdx index 49f41768f603..be126d5d6ed5 100644 --- a/packages/docs/themes.mdx +++ b/packages/www/content/docs/(Configure)/themes.mdx @@ -3,8 +3,6 @@ title: "Themes" description: "Choose a built-in TUI theme or create a custom color scheme." --- -import ThemeTokens from "/snippets/generated/theme-tokens.mdx" - OpenCode includes built-in light and dark themes and can load custom themes from your global configuration or a project directory. The default theme is `opencode`. @@ -41,10 +39,10 @@ path under `$XDG_CONFIG_HOME`: } ``` - + Theme selection applies to the full-screen TUI. Direct interactive runs use colors derived from the terminal palette and honor only the color mode. - + ## Built-in themes @@ -90,11 +88,11 @@ supported. V2 themes organize colors into hue scales and semantic tokens. Set `version` to `2` and define at least one of `light` or `dark`: - + Native V2 custom theme files are not loaded directly by the current beta. Existing custom files use the V1 format and are migrated to these tokens at runtime. This reference tracks the native V2 schema while direct file loading is completed. - + By default, a theme inherits OpenCode's complete theme, so you only need to define overrides. Set `mergeMode` to `true` to inherit one mode from the other diff --git a/packages/docs/warming.mdx b/packages/www/content/docs/(Configure)/warming.mdx similarity index 100% rename from packages/docs/warming.mdx rename to packages/www/content/docs/(Configure)/warming.mdx diff --git a/packages/www/content/docs/(docs)/agents.mdx b/packages/www/content/docs/(docs)/agents.mdx deleted file mode 100644 index d5668402db4b..000000000000 --- a/packages/www/content/docs/(docs)/agents.mdx +++ /dev/null @@ -1,283 +0,0 @@ ---- -title: "Agents" -description: "" ---- - -Agents combine a system prompt, model preference, tool permissions, and display -metadata into a reusable assistant profile. OpenCode includes agents for common -workflows, and you can override them or add your own in configuration or -Markdown files. - -## Built-in agents - -| Agent | Mode | Purpose | -| --- | --- | --- | -| **Build** (`build`) | `primary` | Default coding agent. Tools are allowed by default, sensitive environment-file reads ask for approval, and access outside the workspace asks for approval. | -| **Plan** (`plan`) | `primary` | Planning agent. File edits are denied except for OpenCode plan files. Shell commands are not generally denied. | -| **General** (`general`) | `subagent` | General-purpose research and multi-step work. It has broad tool access but cannot launch more subagents. | -| **Explore** (`explore`) | `subagent` | Read-only code and web exploration using `read`, `glob`, `grep`, `webfetch`, and `websearch`. | - -OpenCode also has hidden `compaction`, `title`, and `summary` system agents. -They run internal maintenance tasks and are not selectable. There is no built-in -`scout` agent in V2. - -You can override a built-in agent with an entry of the same ID. Set -`disabled: true` to remove one. - -## Default agent - -Set the primary agent used when a session has not selected one: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "default_agent": "reviewer" -} -``` - -The configured agent must exist, must not have `mode: "subagent"`, and must not -be hidden. If it is unavailable, OpenCode falls back to `build`, then to the -first visible agent that can run as a primary agent. This selection does not -rewrite the agent already stored on an existing session. - -## Modes - -An agent's `mode` controls where it can run: - -| Mode | Behavior | -| --- | --- | -| `primary` | Can be selected as the main agent for a session. It cannot be launched as a subagent. | -| `subagent` | Can run in a child session through the `subagent` tool, but cannot be selected as the main agent. | -| `all` | Can be used either way. This is the default for a custom agent when `mode` is omitted. | - -In the TUI, press Tab and Shift+Tab to cycle -through visible primary and `all` agents, or use `/agents` to choose one. - -Subagents run in child sessions with fresh context. A primary agent can invoke -one with the `subagent` tool, either in the foreground or in the background. -You can also `@` mention a visible subagent to ask the current agent to delegate -work to it: - -```text -@explore find where authentication errors are handled -``` - -The parent agent's `subagent` permission controls which agents it may launch. -The child currently uses its own configured permissions, not a restricted copy -of the parent's permissions. - -## Configure agents - -### Markdown files - -The recommended file locations are: - -```text -~/.config/opencode/agents/.md -.opencode/agents/.md -``` - -OpenCode discovers project `.opencode` directories from the current directory -up to the project root. The path below `agents/` becomes the agent ID, so -`.opencode/agents/team/reviewer.md` defines `team/reviewer`. - -Frontmatter uses the same fields as an entry under `agents`. The Markdown body -becomes `system`: - -```md title=".opencode/agents/reviewer.md" ---- -description: Reviews changes without modifying files -mode: subagent -model: anthropic/claude-sonnet-4-5#high -color: "#ff6b6b" -steps: 8 -permissions: - - action: edit - resource: "*" - effect: deny - - action: shell - resource: "*" - effect: deny ---- - -Review for correctness, security, regressions, and missing tests. -List findings in severity order with file and line references. -``` - -### JSON or JSONC - -Use the `agents` field in any [OpenCode configuration file](/docs/config): - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "default_agent": "reviewer", - "agents": { - "reviewer": { - "description": "Reviews changes for correctness, security, and missing tests", - "mode": "all", - "model": "anthropic/claude-sonnet-4-5#high", - "system": "Review the current changes. Report findings before any summary.", - "color": "#ff6b6b", - "steps": 8, - "permissions": [ - { "action": "edit", "resource": "*", "effect": "deny" }, - { "action": "shell", "resource": "*", "effect": "deny" } - ] - }, - "build": { - "permissions": [ - { "action": "shell", "resource": "git push *", "effect": "ask" } - ] - } - } -} -``` - -Agent definitions merge in configuration order. Later scalar fields replace -earlier values, request maps merge by key, and permission rules are appended. -Global `permissions` are applied to every agent before its agent-specific rules, -so a later agent rule can refine a global rule. - -## Options - -### `description` - -Explains the agent's purpose. It is optional, but strongly recommended for -subagents because OpenCode includes it in the subagent catalog shown to the -model. - -### `mode` - -Accepts `primary`, `subagent`, or `all`. The default is `all`. - -### `model` - -Selects a model using `provider/model` with an optional `#variant`: - -```jsonc -{ - "agents": { - "reviewer": { - "model": "anthropic/claude-sonnet-4-5#high" - } - } -} -``` - -The equivalent expanded form is: - -```jsonc -{ - "agents": { - "reviewer": { - "model": { - "providerID": "anthropic", - "model": "claude-sonnet-4-5", - "variant": "high" - } - } - } -} -``` - -The TUI uses this as the preferred model when the agent is selected. A child -session uses its subagent's configured model, or inherits the parent session's -model when none is configured. In the API, the session's selected model is -stored separately; creating or switching a primary session with only an agent -ID does not itself change that session model. - -### `system` - -Sets the agent's system prompt. A non-empty value replaces OpenCode's -provider-specific base prompt for that agent. Project instructions, skills, -references, and other instruction sources are still added separately. - -For a Markdown agent, use the document body instead of a `system` frontmatter -field. - -### `permissions` - -Permissions are an ordered array of rules: - -```jsonc -{ - "agents": { - "orchestrator": { - "permissions": [ - { "action": "subagent", "resource": "*", "effect": "deny" }, - { "action": "subagent", "resource": "explore", "effect": "allow" }, - { "action": "shell", "resource": "git *", "effect": "ask" } - ] - } - } -} -``` - -Each rule has: - -| Field | Meaning | -| --- | --- | -| `action` | Tool or permission action, with `*` wildcards supported. | -| `resource` | The path, command, agent ID, or other resource matched by the action. Wildcards are supported. | -| `effect` | `allow`, `ask`, or `deny`. | - -The last matching rule wins. Important V2 action names include `shell` for -shell commands, `edit` for all edit/write/patch tools, and `subagent` for child -agents. Other tools generally use their tool name, such as `read`, `glob`, -`grep`, `webfetch`, `websearch`, and `skill`. - - - Put broad wildcard rules first and exceptions afterward. For example, deny - all subagents first, then allow `explore`. - - -`~` and `$HOME` are expanded in filesystem resources for `read`, `edit`, and -`external_directory`. Shell resources are raw command text and are not -expanded. - -### `steps` - -Sets a positive maximum number of model steps. On the final allowed step, -OpenCode removes tools and asks the model to summarize its work in text. New -user input resets the allowance. - -### `hidden` - -When `true`, removes the agent from normal selectors, `@` autocomplete, and the -subagent catalog advertised to models. It is a visibility setting, not a -security boundary. - -### `color` - -Sets the agent's UI color. Use a six-digit hex color such as `#ff6b6b`. - -### `disabled` - -When `true`, removes the agent definition at that point in configuration -loading. This works for built-in and custom agents. - -### `request` - -The V2 schema accepts per-agent request `headers` and JSON `body` overlays: - -```jsonc -{ - "agents": { - "reviewer": { - "request": { - "headers": { "x-agent": "reviewer" }, - "body": { "temperature": 0.1 } - } - } - } -} -``` - - - The current V2 session runner preserves these overlays on the agent - definition but does not yet apply them to model requests. Configure effective - request settings on the provider, model, or model variant instead. Do not use - legacy top-level agent fields such as `temperature`, `top_p`, `prompt`, - `permission`, `tools`, `disable`, or `maxSteps` in new V2 configuration. - diff --git a/packages/www/content/docs/(docs)/attachments.mdx b/packages/www/content/docs/(docs)/attachments.mdx deleted file mode 100644 index 7f97859bf420..000000000000 --- a/packages/www/content/docs/(docs)/attachments.mdx +++ /dev/null @@ -1,168 +0,0 @@ ---- -title: "Attachments" -description: "" ---- - -OpenCode can add local context to a prompt as text or image media. Current V2 -sessions make these attachment types visible to the model: - -| Input | Model receives | -| --- | --- | -| UTF-8 text file | The filename and decoded text | -| Directory | A non-recursive listing of its immediate files and directories | -| PNG, JPEG, GIF, or WebP | Image media | - -SVG files are treated as text, not image media. PDF, AVIF, BMP, audio, video, -and other binary prompt attachments are not currently included in the model -request. Some clients may let you select a PDF, but V2 does not yet make that -PDF visible to the model. - - - Use a model that supports image input before attaching an image. OpenCode - passes supported image media to the selected provider, but the provider and - model still enforce their own formats, dimensions, file counts, and size - limits. A text-only model may reject the request. - - -## Add attachments - -### TUI - -Type `@` followed by a filename and select the result to attach a project file. -This is the preferred way to add source code and other text files: - -```text -Explain the error handling in @src/server.ts -``` - -Paste an image from the clipboard with the configured paste key, `Ctrl+V` by -default. You can also drag a supported image into a terminal that exposes the -dropped file path to the TUI. The TUI reads PNG, JPEG, GIF, and WebP as image -attachments; a dropped SVG is inserted as text. - -### Desktop and web - -Use **Attach file**, paste, or drag and drop. Attach UTF-8 text or a PNG, JPEG, -GIF, or WebP image. The desktop file picker limits one selection to 20 MiB in -total; the server also applies the per-attachment limit described below. - -### CLI - -Pass `--file` or `-f` to `opencode2 run`. Repeat the flag for multiple files: - -```bash -opencode2 run -f src/server.ts -f screenshot.png "Explain the failure" -``` - -The run command accepts at most 100 file flags and reads at most 10 MiB per -file. Use it for text files and the four supported image formats; other binary -files do not become model context. - -### API - -The V2 prompt and command payloads accept a `files` array. Each item requires a -`uri` and can include `name` and `description`: - -```bash -opencode2 api post /api/session/ses_example/prompt --data '{ - "text": "Review this file", - "files": [ - { - "uri": "file:///home/me/project/src/server.ts", - "name": "server.ts", - "description": "Request handler" - } - ] -}' -``` - -Use an absolute `file:` URL for a file available to the server, or an inline -data URL: - -```json -{ - "text": "What is wrong with this layout?", - "files": [ - { - "uri": "data:image/png;base64,", - "name": "layout.png" - } - ] -} -``` - -HTTP and HTTPS attachment URLs are not supported. OpenCode materializes each -attachment before admitting the prompt and rejects invalid URLs, unreadable -paths, non-files other than directories, and attachments over 20 MiB decoded. -For a text `file:` URL, optional positive `start` and `end` query parameters -select one-based lines: - -```text -file:///home/me/project/src/server.ts?start=20&end=60 -``` - -The server infers the media type from the bytes. A supplied filename or data -URL media type does not make an unsupported binary format model-visible. - -## Configure image processing - -Configure image normalization in `opencode.json` or `opencode.jsonc`: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "attachments": { - "image": { - "auto_resize": true, - "max_width": 2000, - "max_height": 2000, - "max_base64_bytes": 5242880 - } - } -} -``` - -All fields are optional: - -| Field | Default | Behavior | -| --- | ---: | --- | -| `auto_resize` | `true` | Resize an image that exceeds any configured limit. If `false`, reject it. | -| `max_width` | `2000` | Maximum width in pixels. Must be a positive integer. | -| `max_height` | `2000` | Maximum height in pixels. Must be a positive integer. | -| `max_base64_bytes` | `5242880` | Maximum byte length of the Base64-encoded image string. Must be a positive integer. | - - - In the current V2 runtime, these settings apply to image media produced by - the built-in `read` tool. Images attached directly through the TUI, desktop, - web, CLI, or API bypass this normalization. Resize direct attachments before - adding them if the provider requires smaller media. - - -The `read` tool recognizes PNG, JPEG, GIF, and WebP by their contents and will -ingest at most 20 MiB of source image bytes. It decodes the image and compares -its width, height, and encoded Base64 length with all three configured limits. - -When `auto_resize` is `true`, OpenCode preserves the aspect ratio, scales the -image down to the dimension limits, and tries progressively smaller PNG and -JPEG encodings until the Base64 limit is met. The resulting media type can -therefore change to PNG or JPEG. If no encoding fits, the tool call fails. - -When `auto_resize` is `false`, exceeding any limit fails the tool call without -modifying the image. An image that cannot be decoded also fails. If the image -resizer cannot be loaded, the `read` tool returns the -original image instead, so these settings are processing limits rather than an -upload or security boundary. - -## Limits and provider behavior - -- Direct prompt attachments are limited to 20 MiB decoded per item by the V2 - server. Client-specific limits can be lower. -- `max_base64_bytes` counts the encoded Base64 characters in bytes, not the - decoded file size and not the complete `data:` URL. -- Text attachments are inserted into the prompt as text and do not require a - multimodal model. Large text read through the `read` tool has separate - paging and truncation limits. -- Image attachments use provider-native image input. Provider errors can still - occur when OpenCode's limits pass but the selected model's limits do not. -- PDFs and other unsupported binary prompt attachments should be converted to - text or supported images before attaching them. diff --git a/packages/www/content/docs/(docs)/commands.mdx b/packages/www/content/docs/(docs)/commands.mdx deleted file mode 100644 index 398f7e740571..000000000000 --- a/packages/www/content/docs/(docs)/commands.mdx +++ /dev/null @@ -1,163 +0,0 @@ ---- -title: "Commands" -description: "" ---- - -Custom commands turn a named prompt template into a slash command. Type the -command in the TUI, followed by any arguments: - -```text -/review src/auth -``` - -## Configure with Markdown - -OpenCode discovers `.md` command files in `commands/` directories: - -```text -~/.config/opencode/commands/ # Global -.opencode/commands/ # Project -``` - -Files may be nested; for example, `.opencode/commands/team/review.md` defines -`/team/review`. Files with other extensions, including `.mdx`, are not -discovered. - -```md title=".opencode/commands/review.md" ---- -description: Review code for correctness and missing tests -agent: plan -model: anthropic/claude-sonnet-4-5#high ---- - -Review $ARGUMENTS. Report bugs first, then missing tests. -``` - -The file body, with surrounding whitespace removed, is the command template. -JSON and Markdown commands share one registry. Project definitions take -precedence over global definitions, and a later definition can override a -built-in or earlier command with the same name. Changes are reloaded -automatically. - -Run it with: - -```text -/review src/auth -``` - -## Configure with JSON - -Add commands under the `commands` key in any OpenCode JSON or JSONC -[configuration file](/docs/config). Each entry's key is the command name and -`template` is required. - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "commands": { - "review": { - "description": "Review code for correctness and missing tests", - "template": "Review $ARGUMENTS. Report bugs first, then missing tests.", - "agent": "plan", - "model": "anthropic/claude-sonnet-4-5#high" - } - } -} -``` - -## Fields - -| Field | Required | Behavior | -| --- | --- | --- | -| `template` | JSON only | Prompt template. In a Markdown command, the file body supplies it. | -| `description` | No | Text shown with the command in autocomplete. | -| `agent` | No | Agent selected before the prompt runs. | -| `model` | No | Model override in `provider/model` or `provider/model#variant` format. | -| `subtask` | No | Accepted as a boolean, but currently has no execution effect in V2. | - -The four optional fields can be used in JSON or YAML frontmatter. Do not put -`template` in frontmatter because the Markdown body always supplies it. - -## Arguments - -Use `$ARGUMENTS` for the complete argument string: - -```md title=".opencode/commands/component.md" ---- -description: Create a component ---- - -Create a typed React component named $ARGUMENTS. -``` - -```text -/component Button -``` - -Use `$1`, `$2`, and higher numbers for parsed positional arguments. Single and -double quotes group text containing spaces and are removed during parsing. - -```md title=".opencode/commands/check.md" ---- -description: Check one area with a specific focus ---- - -Check $1. Focus on $2. -``` - -```text -/check src/auth "error handling and missing tests" -``` - -The highest-numbered positional placeholder present in the template consumes -that argument and all remaining arguments. For example, if a template contains -only `$1`, then `$1` receives the full parsed argument list. Missing positions -become empty strings. - -If a template contains neither positional placeholders nor `$ARGUMENTS`, -OpenCode appends non-empty arguments to the template after a blank line. - -## Shell interpolation - -Wrap a shell command in `!` followed by backticks to insert its output before -the prompt is submitted: - -```md title=".opencode/commands/review-diff.md" ---- -description: Review the current diff ---- - -Review this diff: - -!`git diff --stat && git diff` -``` - -OpenCode runs each interpolation with the configured shell in the active -project location and inserts its combined output into the template. Argument -interpolation happens first, so avoid placing untrusted arguments inside shell -interpolations. - - - Shell interpolations run when the command is evaluated, outside the agent's - tool permission flow. Only use commands from sources you trust. - - -No other template interpolation is performed. In particular, an `@path` -written into a stored template remains ordinary prompt text; V2 does not -automatically attach that file. - -## Agent, model, and execution - -Running a command evaluates its arguments and shell blocks, submits the result -as a durable user prompt in the current session, and schedules normal model -execution. - -If `agent` is set, it overrides the agent selected when the command was -invoked and becomes the session's active agent. If `model` is set, it overrides -the model. Otherwise, a model configured on the command's agent takes -precedence over the model selected at invocation. - -Although `subtask` is accepted in JSON and frontmatter, V2 currently ignores -it: commands run in the current session and do not create a child session. -Selecting an agent whose mode is `subagent` also does not turn the command into -a subtask. diff --git a/packages/www/content/docs/(docs)/compaction.mdx b/packages/www/content/docs/(docs)/compaction.mdx deleted file mode 100644 index 693dff8c75f7..000000000000 --- a/packages/www/content/docs/(docs)/compaction.mdx +++ /dev/null @@ -1,153 +0,0 @@ ---- -title: "Compaction" -description: "" ---- - -Compaction replaces the active model context from an older part of a session -with a generated checkpoint. The checkpoint contains a structured summary and -a serialized tail of recent context, so the agent can continue with more room -in the model's context window. - -Compaction is lossy, but it does not delete the earlier durable session -messages. After a successful compaction, V2 builds model requests from the -latest completed checkpoint and the messages that follow it. - -## Automatic compaction - -Automatic compaction is enabled by default. Before a model call, V2 estimates -the size of the final system prompt, messages, and advertised tools. It starts -compaction when: - -```text -estimated tokens > context limit - max(requested output tokens, buffer) -``` - -The estimate is approximate: V2 JSON-serializes the request and assumes four -characters per token. When compaction succeeds, V2 rebuilds the request from -the new checkpoint and retries the step without promoting the input again. - -V2 also recognizes provider errors classified as context overflow. If an -overflow occurs before the provider produces assistant output or other retry -evidence, V2 can compact and retry that step once. This recovery is attempted -even when `auto` is `false`; `auto` controls only the preflight size check. A -second overflow after recovery is returned as an error. - -## Manual compaction - -In the TUI, run: - -```text -/compact -``` - -`/summarize` is an alias. The default keybind is `c`, configured as -`session_compact`. - -A manual request is durably admitted and wakes the session runner. It can -compact short histories that would not trigger automatic compaction. If the -session is busy, compaction runs at the next safe drain boundary before later -steered or queued prompts are promoted. Repeated requests while one is pending -coalesce into that pending request. Whether compaction completes or fails, the -barrier is then settled so later prompts can proceed. - -The CLI has no separate `compact` subcommand. Use the TUI command or the server -API. For example: - -```bash -opencode2 api v2.session.compact \ - --param sessionID=ses_example \ - --data '{}' -``` - -The equivalent raw request is: - -```bash -opencode2 api post /api/session/ses_example/compact --data '{}' -``` - -`POST /api/session/:sessionID/compact` returns the admitted compaction input; -it does not wait for summary generation. Clients can call -`client.session.compact({ sessionID })` and then wait for the session or follow -the `session.compaction.*` events. Supplying an optional message `id` makes an -exact retry idempotent, but reusing an ID owned by another record returns a -conflict. - -## Configuration - -Add `compaction` to any [OpenCode configuration file](/docs/config): - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "compaction": { - "auto": true, - "prune": false, - "keep": { - "tokens": 8000 - }, - "buffer": 20000 - } -} -``` - -| Field | Default | V2 behavior | -| --- | ---: | --- | -| `auto` | `true` | Runs the preflight context-size check. It does not disable manual compaction or one-shot provider-overflow recovery. | -| `prune` | None | Accepted by the V2 schema, but currently has no runtime effect. V2 does not prune old tool outputs in place. | -| `keep.tokens` | `8000` | Approximate number of tokens from the newest serialized conversation context to retain beside the summary. | -| `buffer` | `20000` | Token reserve used by the automatic threshold. The requested model output allowance wins when it is larger. | - -`keep.tokens` and `buffer` accept non-negative integers. Larger `keep.tokens` -preserves more recent detail but leaves less room for future work. Larger -`buffer` triggers preflight compaction earlier. - -## Checkpoint contents - -V2 uses the session's selected or default model to generate the summary, with -tools disabled and at most 4096 output tokens. The summary records the -objective, important details, completed and active work, blockers, next moves, -and relevant files. - -The newest serialized context up to `keep.tokens` is retained separately. This -is not a byte-for-byte transcript: tool output is limited to 2000 characters, -and file or media attachments become textual descriptors rather than embedded -data. On later compactions, V2 updates the previous summary and carries forward -its retained recent context before selecting a new tail. - -The completed compaction is presented to the model as historical conversation -context, explicitly not as new instructions. Running and failed compactions are -not included in model context. - -## Compaction advances the instruction epoch - -Conversation compaction and instruction synchronization are separate. Before -promoting pending input, V2 compares live instruction sources with the latest -admitted values. Ordinary changes become durable value deltas; their -model-facing System messages are derived during request assembly rather than -persisted. - -Completed compaction advances the instruction epoch at the exact ended-event -sequence and makes the currently admitted values initial. It does not reread -sources or publish an instruction event. Session movement and committed revert -clear the instruction fold so the next safe boundary requires one complete -source read. See [Instructions](/docs/instructions) for source ordering and update -behavior. - -## Current limitations - -- `prune` is reserved configuration; V1-style in-place tool-output pruning is - not implemented in V2. -- Compaction requires a resolvable model with a positive catalog context limit. - There is no separate compaction-model setting or fallback model. -- Summary generation can fail if the summary prompt itself cannot fit beside - its output allowance, the model returns no summary, or the provider fails. -- Automatic and overflow compaction need older conversation context that can be - replaced. A provider overflow can still surface when there is no compressible - head or fixed instructions and tool schemas dominate the request. -- Overflow recovery retries only once per step. Token estimation is heuristic, - so it cannot prevent every provider-specific overflow. -- Earlier durable messages remain stored even though they are no longer in the - active model context. - -V1 used additional tail-turn and pruning behavior. Those V1 details are only -migration context; the settings and behavior on this page describe V2. diff --git a/packages/www/content/docs/(docs)/config.mdx b/packages/www/content/docs/(docs)/config.mdx deleted file mode 100644 index fc0db5bbed40..000000000000 --- a/packages/www/content/docs/(docs)/config.mdx +++ /dev/null @@ -1,450 +0,0 @@ ---- -title: "Config" -description: "" ---- - - - You shouldn't have to configure OpenCode manually. Ask OpenCode to update its configuration for you. - - -## Format - -OpenCode supports both **JSON** and **JSONC** (JSON with Comments) configuration files. - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "model": "openai/gpt-5.2-custom", - "providers": { - "openai": { - "models": { - "gpt-5.2-custom": { - "modelID": "gpt-5.2", - "name": "GPT-5.2 Custom" - } - } - } - } -} -``` - -## Locations - -OpenCode loads global configuration from: - -```text -~/.config/opencode/opencode.json(c) -``` - -Project-specific configuration can use either form: - -```text -/home/user/projects/my-app/opencode.json(c) -/home/user/projects/my-app/.opencode/opencode.json(c) -``` - -When OpenCode starts, it searches for configuration files from the current -directory upward to the project root. It merges direct `opencode.json(c)` files -from the project root toward the current directory, then does the same for -files inside `.opencode` directories. A `.opencode` config therefore overrides -every direct config, even when the direct config is closer to the current -directory. Avoid mixing the two forms across one project hierarchy unless this -precedence is intentional. - -For example, consider a monorepo with OpenCode started from -`/home/user/projects/acme/packages/web`: - -```text -~/.config/opencode/opencode.json - -/home/user/projects/acme/ -├── opencode.json -└── packages/ - └── web/ - ├── opencode.json - └── src/ -``` - -OpenCode applies these files from lowest to highest precedence: - -1. `~/.config/opencode/opencode.json` -2. `/home/user/projects/acme/opencode.json` -3. `/home/user/projects/acme/packages/web/opencode.json` - -In this direct-config example, the package config overrides matching settings -from the repository config, which overrides matching settings from the global -config. Settings that do not conflict are preserved from every file. - -## Schema - -The complete OpenCode configuration schema is available at -[opencode.ai/config.json](https://opencode.ai/config.json). - -Add the `$schema` field to your configuration file to enable validation and -autocomplete in editors that support JSON Schema: - -```json title="opencode.json" -{ - "$schema": "https://opencode.ai/config.json" -} -``` - -Use the schema as the source of truth for available fields, accepted values, -and nested configuration shapes. - -### Shell - -Set the shell used by the terminal and shell tools. - -```jsonc -{ - "shell": "/bin/zsh" -} -``` - -### Model - -Set the default model in `provider/model` format. The root default currently -does not retain a `#variant`; select variants in the TUI or on an agent or command. - -```jsonc -{ - "model": "anthropic/claude-sonnet-4-5" -} -``` - -See the [models guide](/docs/models) for model selection -and local models. - -### Default agent - -Choose the primary agent used when a session does not select one explicitly. - -```jsonc -{ - "default_agent": "build" -} -``` - -See the [agents guide](/docs/agents) for built-in and custom -agents. - -### Autoupdate - -Control automatic updates from the global config. Set this to `false` to -disable updates. The current beta treats `true` and `"notify"` identically and -automatically installs compatible non-major updates; project-level values are -ignored. - -```jsonc -{ - "autoupdate": false -} -``` - -### Sharing - -Set the intended session sharing policy. V2 accepts this field, but session -sharing is not implemented yet. - -```jsonc -{ - "share": "manual" -} -``` - -See the [sharing guide](/docs/sharing) for more details. - -### Username - -Set a username for future display behavior. V2 accepts this field but does not -currently display it in conversations. - -```jsonc -{ - "username": "alice" -} -``` - -### Permissions - -Define ordered rules that allow, deny, or ask before an agent uses a tool on a -matching resource. - -```jsonc -{ - "permissions": [ - { - "action": "shell", - "resource": "git push *", - "effect": "ask" - } - ] -} -``` - -See the [permissions guide](/docs/permissions) for rule matching and available actions. - -### Agents - -Override built-in agents or define specialized agents with their own model, -instructions, mode, and permissions. - -```jsonc -{ - "agents": { - "reviewer": { - "description": "Review changes without editing files", - "mode": "subagent", - "system": "Focus on correctness, security, and missing tests.", - "permissions": [ - { "action": "edit", "resource": "*", "effect": "deny" } - ] - } - } -} -``` - -See the [agents guide](/docs/agents) for all agent options and file-based agents. - -### Snapshots - -Enable or disable filesystem snapshots used by undo and revert behavior. - -```jsonc -{ - "snapshots": false -} -``` - -See the [snapshots guide](/docs/snapshots) for undo and redo behavior. - -### Watcher - -Ignore files and directories that should not trigger filesystem updates. - -```jsonc -{ - "watcher": { - "ignore": ["dist/**", "coverage/**"] - } -} -``` - -### Formatter - -Define formatter settings for compatibility and future use. V2 accepts this -field, but it does not run formatters yet. - -```jsonc -{ - "formatter": { - "prettier": { - "command": ["bunx", "prettier", "--write", "$FILE"], - "extensions": [".js", ".ts", ".tsx"] - } - } -} -``` - -See the [formatters guide](/docs/formatters) for accepted fields and current limitations. - -### LSP - -Define language server settings for compatibility and future use. V2 accepts -this field, but it does not start language servers yet. - -```jsonc -{ - "lsp": { - "typescript": { - "command": ["typescript-language-server", "--stdio"], - "extensions": [".ts", ".tsx"] - } - } -} -``` - -See the [LSP guide](/docs/lsp) for accepted fields and current limitations. - -### Attachments - -Control how oversized images loaded by the `read` tool are resized or rejected -before they are sent to a model. - -```jsonc -{ - "attachments": { - "image": { - "auto_resize": true, - "max_width": 2000, - "max_height": 2000, - "max_base64_bytes": 5242880 - } - } -} -``` - -See the [attachments guide](/docs/attachments) for image processing and limits. - -### Tool output - -Set the maximum number of lines and bytes retained from a tool result. - -```jsonc -{ - "tool_output": { - "max_lines": 2000, - "max_bytes": 51200 - } -} -``` - -### MCP - -Configure local and remote Model Context Protocol servers. Global timeouts can -be overridden by an individual server. - -```jsonc -{ - "mcp": { - "servers": { - "playwright": { - "type": "local", - "command": ["bunx", "@playwright/mcp"] - } - } - } -} -``` - -See the [MCP guide](/docs/mcp-servers) for remote servers, OAuth, environment variables, and timeouts. - -### Compaction - -Control automatic context compaction and how much recent context it preserves. - -```jsonc -{ - "compaction": { - "auto": true, - "keep": { - "tokens": 8000 - }, - "buffer": 20000 - } -} -``` - -See the [compaction guide](/docs/compaction) for automatic context management. - -### Skills - -Add directories or URLs that OpenCode should search for agent skills. - -```jsonc -{ - "skills": ["./team-skills", "https://example.com/.well-known/skills/"] -} -``` - -See the [skills guide](/docs/skills) for skill structure and automatic discovery under `.opencode/skills/`. - -### Commands - -Define reusable slash commands as named prompt templates. - -```jsonc -{ - "commands": { - "review": { - "description": "Review the current changes", - "template": "Review the current diff for correctness and missing tests." - } - } -} -``` - -See the [commands guide](/docs/commands) for arguments, models, agents, and file-based commands. - -### Instructions - -Declare additional instruction files, globs, or URLs. V2 accepts this field, -but does not load these entries yet; use `AGENTS.md` for active instructions. - -```jsonc -{ - "instructions": ["CONTRIBUTING.md", "docs/guidelines/*.md"] -} -``` - -See the [instructions guide](/docs/instructions) for project instructions and `AGENTS.md`. - -### References - -Make local directories or Git repositories available as named supporting -context. - -```jsonc -{ - "references": { - "docs": { - "path": "../product-docs", - "description": "Product behavior and terminology" - }, - "effect": { - "repository": "Effect-TS/effect", - "branch": "main" - } - } -} -``` - -See the [references guide](/docs/references) for shorthand, visibility, and path resolution. - -### Plugins - -Load plugins from packages or local files. Use the object form when a plugin -accepts options. - -```jsonc -{ - "plugins": [ - "opencode-example-plugin", - { - "package": "./plugins/local.ts", - "options": { - "enabled": true - } - } - ] -} -``` - -See the [plugins guide](/docs/build/plugins) for plugin development and configuration. - -### Providers - -Configure providers and add or override their models, request settings, -headers, and model variants. - -```jsonc -{ - "providers": { - "openai": { - "models": { - "gpt-5.2-custom": { - "modelID": "gpt-5.2", - "name": "GPT-5.2 Custom", - "limit": { - "context": 200000, - "output": 32000 - } - } - } - } - } -} -``` - -See the [providers guide](/docs/providers) for credentials, custom endpoints, provider packages, and model configuration. diff --git a/packages/www/content/docs/(docs)/formatters.mdx b/packages/www/content/docs/(docs)/formatters.mdx deleted file mode 100644 index 984c5a92971a..000000000000 --- a/packages/www/content/docs/(docs)/formatters.mdx +++ /dev/null @@ -1,93 +0,0 @@ ---- -title: "Formatters" -description: "" ---- - -OpenCode V2 accepts formatter configuration, but it does not yet include a -formatter runtime. File writes and edits are not automatically formatted. - - - V2 currently has no built-in formatters. The built-in formatter list and - automatic post-edit formatting documented for V1 do not apply to V2. - - -## Configuration - -The `formatter` field accepts a boolean or an object keyed by formatter name: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "formatter": { - "prettier": { - "disabled": false, - "command": ["prettier", "--write", "$FILE"], - "environment": { - "NODE_ENV": "development" - }, - "extensions": [".js", ".jsx", ".ts", ".tsx"] - } - } -} -``` - -This example is valid V2 configuration, but V2 does not currently execute the -command. - -Each named formatter entry supports these optional fields: - -| Field | Type | Current V2 behavior | -| --- | --- | --- | -| `disabled` | `boolean` | Accepted, but there is no runtime formatter to enable or disable. | -| `command` | `string[]` | Accepted as an argument array, but not executed. | -| `environment` | `Record` | Accepts string environment variable names and values, but they are not applied. | -| `extensions` | `string[]` | Accepted without extension-specific validation, but files are not matched against it. | - -All entry fields are optional. The schema therefore also accepts an empty entry -such as `"prettier": {}`. - -## Enable and disable - -The schema accepts all of the following forms: - -```jsonc -// Omit `formatter`, or use false, when formatting is not requested. -{ - "formatter": false -} -``` - -```jsonc -// Reserved for enabling all built-ins once a V2 runtime provides them. -{ - "formatter": true -} -``` - -```jsonc -// Configure named entries or mark one as disabled. -{ - "formatter": { - "prettier": { "disabled": true }, - "custom": { - "command": ["custom-fmt", "$FILE"], - "extensions": [".foo"] - } - } -} -``` - -At present, omitted, `false`, `true`, and object forms have the same runtime -result: V2 runs no formatter. `disabled` is retained as configuration data but -does not control an executable formatter. - -## Commands and placeholders - -`command` is an array of strings, not a shell command string. `$FILE` is the V1 -file-path placeholder and is often retained in migrated configuration. V2 does -not currently substitute `$FILE` or define another formatter placeholder. - -Likewise, V2 does not currently use `extensions` to select commands, merge -`environment` into a child process, discover formatter executables or project -configuration, or run multiple matching formatters. These behaviors will only -be available after a V2 formatter runtime is implemented. diff --git a/packages/www/content/docs/(docs)/index.mdx b/packages/www/content/docs/(docs)/index.mdx deleted file mode 100644 index 9101c3d2693f..000000000000 --- a/packages/www/content/docs/(docs)/index.mdx +++ /dev/null @@ -1,160 +0,0 @@ ---- -title: "Intro" -description: "Get started with OpenCode." ---- - - - These docs are for the beta version of OpenCode, which will become OpenCode 2.0. The beta is still changing: we may - wipe your data, things may break, and APIs, configuration, and plugin APIs may change. - - -## Install - -The curl install script is not available in beta. - -You can also install it with the following package managers. - - - - ```bash - npm install -g @opencode-ai/cli@next - ``` - - - ```bash - bun install -g @opencode-ai/cli@next - ``` - - - ```bash - pnpm install -g @opencode-ai/cli@next - ``` - - - ```bash - yarn global add @opencode-ai/cli@next - ``` - - - -During beta, the binary is called `opencode2`. - -### Homebrew - -Homebrew installation is not available in beta. - -### Arch Linux - -Arch Linux installation is not available in beta. - -### Windows - - - For the best experience on Windows, install [Windows Subsystem for Linux - (WSL)](https://learn.microsoft.com/windows/wsl/install), open your Linux distribution, and use one of the beta package - manager commands above. - - - - - Chocolatey installation is not available in beta. - - - Scoop installation is not available in beta. - - - Mise installation is not available in beta. - - - Docker installation is not available in beta. - - - -Standalone binaries are not available in beta. - ---- - -#### Prerequisites - -To use OpenCode in your terminal, you'll need: - -1. A modern terminal emulator like: - - [Ghostty](https://ghostty.org), Linux and macOS - - [WezTerm](https://wezterm.org), cross-platform - - [Alacritty](https://alacritty.org), cross-platform - - [Kitty](https://sw.kovidgoyal.net/kitty/), Linux and macOS -2. API keys for the LLM providers you want to use. - ---- - -## Connect - -With OpenCode you can use any LLM provider by configuring its API key. - -Run `/connect` in the TUI and select your provider. - -```text -/connect -``` - -If you'd like easy access to all the best coding models you can try out -[OpenCode Console](https://console.opencode.ai). - -You can also try [OpenCode Go](https://opencode.ai/go) a $10/month subscription -plan that grants you access to the best open source models. - -Use `/models` to browse the providers and models available to your project. See [Providers](/docs/providers) for connection and -configuration details. - ---- - -## Usage - -You are now ready to use OpenCode in your project. Here are a few common workflows. - -### Ask questions - -Ask OpenCode to explain your codebase. - -Use `@` to fuzzy search for files in the project. - -```text -How is authentication handled in @packages/functions/src/api/index.ts -``` - -### Add features - -Ask OpenCode to add a feature by describing the desired behavior and providing relevant context. - -```text -When a user deletes a note, flag it as deleted in the database. -Create a screen that shows recently deleted notes. -From this screen, the user can restore a note or permanently delete it. -``` - -Give OpenCode plenty of context and examples. - -### Undo changes - -Use `/undo` when a change isn't what you wanted. - -```text -/undo -``` - -OpenCode stages a conversation revert and restores your original message so you can revise it. In a Git repository, it -also restores file changes when snapshots were captured successfully. Run `/undo` multiple times to move the conversation -boundary back, or use `/redo` to restore the staged conversation and files. See [Undo](/docs/snapshots) for -limitations and safety details. - -```text -/redo -``` - ---- - -## Customize - -Make OpenCode your own by [picking a theme](https://opencode.ai/docs/themes), [customizing -keybinds](https://opencode.ai/docs/keybinds), [configuring formatters](/docs/formatters), [creating commands](/docs/commands), or -editing the [OpenCode config](/docs/config). diff --git a/packages/www/content/docs/(docs)/instructions.mdx b/packages/www/content/docs/(docs)/instructions.mdx deleted file mode 100644 index d61d6cf75390..000000000000 --- a/packages/www/content/docs/(docs)/instructions.mdx +++ /dev/null @@ -1,128 +0,0 @@ ---- -title: "Instructions" -description: "" ---- - -Instructions are privileged context that guide an agent throughout a session. -V2 combines built-in context, discovered `AGENTS.md` files, and dynamic sources -such as skill, reference, MCP, and session context. It stores source values as -durable deltas, then renders initial instructions and chronological updates when -assembling each model request. - -## AGENTS.md - -Use `AGENTS.md` for persistent guidance such as build commands, architecture, -code conventions, and verification requirements. Commit project files so the -whole team receives the same instructions. - -V2 loads: - -1. The global file at `$XDG_CONFIG_HOME/opencode/AGENTS.md`, normally - `~/.config/opencode/AGENTS.md`. -2. Every `AGENTS.md` from the current Location up to and including the project - root. - -For example, when the Location is `packages/web`, OpenCode can load all three -project files below: - -```text -my-project/ -├── AGENTS.md -└── packages/ - ├── AGENTS.md - └── web/ - └── AGENTS.md -``` - -The files are combined rather than selecting a single winner. They are rendered -in this order: global, then project files from the Location toward the project -root. OpenCode does not resolve conflicts between their contents, so keep broad -guidance global and put scoped guidance in the relevant project directory. - -If the Location is outside the project root, only the global file is loaded. -Setting `OPENCODE_DISABLE_PROJECT_CONFIG=1` also skips project `AGENTS.md` -discovery but does not disable the global file. - - - Current V2 discovery only recognizes `AGENTS.md`. The `CLAUDE.md` fallback - and related precedence described by older OpenCode documentation do not apply. - - -### Nested instructions - -An `AGENTS.md` below the Location is not part of the initial upward scan. When -the read tool successfully reads a file or lists a directory, OpenCode discovers -`AGENTS.md` files from that target upward to, but not including, the Location. -It adds newly discovered files to the session in nearest-first order. - -Each nested file is injected once per session and recorded in durable session -history. Reading the same area again does not inject it again. Consequently, -editing an already injected nested `AGENTS.md` does not replace its earlier -session entry automatically; start a new session if the updated text must apply -immediately. - -## Config entries - -The V2 config schema accepts an `instructions` array of strings: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "instructions": [ - "CONTRIBUTING.md", - "docs/guidelines/*.md", - "https://example.com/shared-instructions.md" - ] -} -``` - -Configuration is loaded from global through project-local files. If more than -one config defines `instructions`, the highest-precedence, closest config's -entire array is selected; arrays are not merged. - - - V2 currently parses and retains this field but does not resolve its entries - into instruction sources. Local files, glob patterns, and HTTP or HTTPS URLs - in `instructions` therefore do not reach the model yet. Use `AGENTS.md` for - active V2 instructions. URL fetching and timeout behavior documented for V1 - are not supported by the current V2 implementation. - - -See [Config](/docs/config) for config locations and general precedence. - -## Ordering - -The selected agent or provider system prompt is sent first. OpenCode then sends -the session's initial instructions, composed in this order: - -1. Built-in environment and date context. -2. Ambient `AGENTS.md` discovery. -3. Available skill, reference, and MCP guidance. -4. Session-specific instruction entries supplied through the API. - -These sources are combined; ordering is not an override mechanism. Nested -`AGENTS.md` files discovered by reads are chronological session entries rather -than part of the initial instructions. - -## Changes - -Before promoting pending input, V2 compares live instruction sources with the -latest admitted source values: - -- A new or changed ambient `AGENTS.md` aggregate is announced as a system update - that replaces the previous ambient aggregate. -- Removing all ambient files announces that the previous ambient instructions - no longer apply. -- A temporary read or discovery failure preserves the session's last known - instructions instead of treating them as deleted. If no instruction epoch - exists yet, pending input waits until every source is available. -- Completed conversation compaction advances the instruction epoch, making the - currently admitted values initial without rereading sources or authoring an - instruction event. -- Moving a session or committing a revert clears the instruction fold. The next - safe boundary requires one complete source read before promoting input. - -The durable event stores changed source keys and value hashes, not rendered -prose. During request assembly, OpenCode renders the epoch's initial values and -interleaves later changes as chronological System messages. Clients see changed -keys but never the privileged value bodies. diff --git a/packages/www/content/docs/(docs)/mcp-servers.mdx b/packages/www/content/docs/(docs)/mcp-servers.mdx deleted file mode 100644 index 93fac75bea40..000000000000 --- a/packages/www/content/docs/(docs)/mcp-servers.mdx +++ /dev/null @@ -1,262 +0,0 @@ ---- -title: "MCP servers" -description: "" ---- - -OpenCode can connect to [Model Context Protocol](https://modelcontextprotocol.io/) servers and make their tools, prompts, and instructions available to agents. MCP tools consume model context, so enable only the servers you need. - -## Configure servers - -Define each server by a unique name under `mcp.servers` in your [OpenCode configuration](/docs/config). V2 does not place server names directly under `mcp`. - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "servers": { - "my-server": { - "type": "local", - "command": ["npx", "-y", "example-mcp-server"] - } - } - } -} -``` - -Servers connect automatically unless `disabled` is `true`. There is no V2 `enabled` field. - -```jsonc -{ - "mcp": { - "servers": { - "my-server": { - "type": "local", - "command": ["npx", "-y", "example-mcp-server"], - "disabled": true - } - } - } -} -``` - -As with other configuration, a server in a higher-precedence project config replaces a server with the same name from a lower-precedence config. Use different names when you need separate connections or accounts. - -## Local servers - -A local server is a command that OpenCode starts using the MCP stdio transport. - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "servers": { - "everything": { - "type": "local", - "command": [ - "npx", - "-y", - "@modelcontextprotocol/server-everything" - ], - "cwd": ".", - "environment": { - "LOG_LEVEL": "info", - "MCP_API_KEY": "{env:MCP_API_KEY}" - } - } - } - } -} -``` - -| Field | Required | Description | -| --- | --- | --- | -| `type` | Yes | Must be `"local"`. | -| `command` | Yes | Executable followed by its arguments. | -| `cwd` | No | Process working directory. Relative paths resolve from the workspace directory; the workspace is the default. | -| `environment` | No | String environment variables added to the inherited OpenCode process environment. | -| `disabled` | No | Set to `true` to prevent the server from connecting. Defaults to `false`. | -| `timeout` | No | Per-server timeout overrides. | - -Use `{env:NAME}` to substitute an environment variable while loading config. Shell expressions such as `$NAME` are not expanded in JSON strings. - -## Remote servers - -A remote server uses the MCP Streamable HTTP transport. Its `url` must be a valid absolute URL. - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "servers": { - "context7": { - "type": "remote", - "url": "https://mcp.context7.com/mcp", - "oauth": false, - "headers": { - "CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}" - } - } - } - } -} -``` - -| Field | Required | Description | -| --- | --- | --- | -| `type` | Yes | Must be `"remote"`. | -| `url` | Yes | Streamable HTTP endpoint. | -| `headers` | No | String HTTP headers sent to the MCP endpoint. | -| `oauth` | No | OAuth client settings, or `false` to disable OAuth support. | -| `disabled` | No | Set to `true` to prevent the server from connecting. Defaults to `false`. | -| `timeout` | No | Per-server timeout overrides. | - -Use `oauth: false` for a server that exclusively uses an API key or another header-based credential. - -## OAuth - -OAuth support is enabled for remote servers unless `oauth` is `false`. OpenCode discovers the authorization server, uses PKCE, refreshes tokens, and attempts dynamic client registration when the server supports it. OAuth credentials are stored outside project configuration. - -For a server that supports dynamic client registration, only the remote server is required: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "servers": { - "sentry": { - "type": "remote", - "url": "https://mcp.sentry.dev/mcp" - } - } - } -} -``` - -When the server reports that it needs authentication, run `/connect` in the TUI: - -```text -/connect -``` - -Select the MCP server under **Services**, then complete the browser authorization flow. - -You can also authenticate from the command line: - -```bash -opencode2 mcp auth sentry -``` - -The CLI command prints the authorization URL and waits for the redirect to OpenCode's loopback callback server. - -If the provider issued client credentials, configure them using V2's snake_case field names: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "servers": { - "company-tools": { - "type": "remote", - "url": "https://mcp.example.com/mcp", - "oauth": { - "client_id": "{env:MCP_CLIENT_ID}", - "client_secret": "{env:MCP_CLIENT_SECRET}", - "scope": "tools:read tools:execute", - "callback_port": 19876, - "redirect_uri": "http://127.0.0.1:19876/callback" - } - } - } - } -} -``` - -| OAuth field | Description | -| --- | --- | -| `client_id` | Pre-registered OAuth client ID. If omitted, OpenCode attempts dynamic client registration. | -| `client_secret` | Client secret for a pre-registered client. | -| `scope` | Space-delimited scopes to request. | -| `callback_port` | Local callback port, from `1` through `65535`. An available ephemeral port is used by default. | -| `redirect_uri` | Pre-registered loopback redirect URI. Its path and port must reach the local callback listener. | - -Remove stored credentials with: - -```bash -opencode2 mcp logout sentry -``` - -## Timeouts - -Timeouts are positive integer milliseconds. Configure defaults under `mcp.timeout`; a server's `timeout` fields override matching defaults. - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "timeout": { - "startup": 45000, - "catalog": 30000, - "execution": 600000 - }, - "servers": { - "slow-tools": { - "type": "remote", - "url": "https://mcp.example.com/mcp", - "timeout": { - "catalog": 60000 - } - } - } - } -} -``` - -| Timeout | Default | Applies to | -| --- | --- | --- | -| `startup` | 30 seconds | Establishing the transport and initializing the server. | -| `catalog` | 30 seconds | Listing tools, prompts, resources, and resource templates. | -| `execution` | 12 hours | Calling tools, getting prompts, and reading resources. | - -## Names and permissions - -OpenCode combines the server name and MCP tool name as `_`. Characters other than letters, numbers, `_`, and `-` are replaced with `_`; for example, server `context 7` and tool `resolve.library/id` become `context_7_resolve_library_id`. MCP prompts appear as slash commands named `:` using the same normalization. - -Choose short server names that remain unique after normalization. Under the default Code Mode, MCP tools are grouped by the normalized server name. - -Use permission actions to hide or deny a server's tools without stopping its connection: - -```jsonc -{ - "permissions": [ - { - "action": "context7_*", - "resource": "*", - "effect": "deny" - } - ] -} -``` - -## CLI commands - -V2 provides these MCP management commands: - -```bash -# Add a local server to the project config -opencode2 mcp add everything --env LOG_LEVEL=info -- npx -y @modelcontextprotocol/server-everything - -# Add a remote server to the project config -opencode2 mcp add context7 --url https://mcp.context7.com/mcp --header 'CONTEXT7_API_KEY={env:CONTEXT7_API_KEY}' - -# Add to the global config instead -opencode2 mcp add context7 --global --url https://mcp.context7.com/mcp - -# List configured servers and connection status -opencode2 mcp list - -# Authenticate or remove OAuth credentials -opencode2 mcp auth context7 -opencode2 mcp logout context7 -``` - -`mcp add` accepts either `--url` for a remote server or a command after `--` for a local server, not both. Use `--header NAME=VALUE` only with remote servers and `--env NAME=VALUE` only with local servers. Edit the config directly for OAuth, timeout, working-directory, or enablement settings. diff --git a/packages/www/content/docs/(docs)/meta.json b/packages/www/content/docs/(docs)/meta.json deleted file mode 100644 index 722630744135..000000000000 --- a/packages/www/content/docs/(docs)/meta.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "title": "Docs", - "description": "Use and configure OpenCode.", - "root": true, - "pages": [ - "---Get started---", - "index", - "migrate-v1", - "config", - "troubleshooting", - "---Configure---", - "providers", - "models", - "agents", - "permissions", - "sharing", - "snapshots", - "commands", - "skills", - "instructions", - "mcp-servers", - "attachments", - "compaction", - "formatters", - "lsp", - "references" - ] -} diff --git a/packages/www/content/docs/(docs)/migrate-v1.mdx b/packages/www/content/docs/(docs)/migrate-v1.mdx deleted file mode 100644 index 22c1973b73ce..000000000000 --- a/packages/www/content/docs/(docs)/migrate-v1.mdx +++ /dev/null @@ -1,576 +0,0 @@ ---- -title: "Migrate from V1" -description: "Move from OpenCode V1 to the OpenCode 2.0 beta." ---- - -## Breaking changes - -V2 has three intentional breaking changes: - -- [Plugins](#plugins) use a new plugin API. -- The [server API and clients](#server-api-and-clients) have new contracts. -- [TUI configuration](#tui-configuration) moves from layered `tui.json(c)` files to one global `cli.json` file (auto migrated). - -All other functionality is intended to remain compatible with V1. - -Existing server config files, agent definitions, command definitions, skills, and other files in `.opencode/` should -continue to work without changes. If one of these stops working in V2, treat it as a beta compatibility bug rather than -an expected migration requirement. - - - Run `/report` if existing V1 functionality does not work in V2. The report skill collects diagnostics and helps you file - a compatibility issue. - - - - OpenCode 2.0 is in beta. Beta data may be wiped, features may break unintentionally, and the server and plugin APIs may - continue to change. - - -During the beta, OpenCode V1 and V2 use different executable names. You can keep using `opencode` for V1 while trying V2 -with `opencode2`. - -## Install the beta - -Install the beta from the `next` distribution tag: - -```bash -npm install -g @opencode-ai/cli@next -``` - -Start it in your project with: - -```bash -opencode2 -``` - -## Configuration - -This section covers both JSON/JSONC configuration and file-based definitions under `.opencode/`. - -### Use your existing configuration - -V2 reads existing global and project configuration from the same locations as V1: - -```text -~/.config/opencode/opencode.json(c) -/opencode.json(c) -/.opencode/opencode.json(c) -``` - -V2 reads these same locations. It detects V1-shaped configuration and translates it in memory without rewriting the -source file. Existing V1 configuration is intended to keep working, so you do not need to convert it to try or adopt V2. - -### Ask OpenCode to migrate - -The V1 config format remains supported. The native V2 format is optional and makes several settings more explicit and -ergonomic. - -The recommended migration path is to ask OpenCode to update the configuration for you: - -```text -Migrate my OpenCode configuration, including file-based definitions, from the V1 format to the native V2 format. -Preserve its behavior and all unrelated settings. -``` - -OpenCode can inspect the complete file, apply the relevant changes below, and avoid rewriting settings that do not need to -change. Do not mix V1 and V2 field names manually in one file. - -### Sharing - -The deprecated V1 `autoshare` boolean becomes the explicit `share` policy: - -```jsonc -// V1 -{ "autoshare": true } - -// V2 -{ "share": "auto" } -``` - -Use `"manual"`, `"auto"`, or `"disabled"`. If the V1 file already uses `share`, no change is needed. - -### Permissions and tools - -V1 groups permission effects by tool. V2 uses one ordered `permissions` array, making precedence and exceptions explicit: - -```jsonc -// V1 -{ - "permission": { - "bash": { - "git push *": "ask" - }, - "edit": "allow" - }, - "tools": { - "websearch": false - } -} - -// V2 -{ - "permissions": [ - { "action": "shell", "resource": "git push *", "effect": "ask" }, - { "action": "edit", "resource": "*", "effect": "allow" }, - { "action": "websearch", "resource": "*", "effect": "deny" } - ] -} -``` - -Permission actions also changed: `bash` is now `shell`, `task` is now `subagent`, and `write` and `patch` are now `edit`. -See [Permissions](/docs/permissions) for the ordered V2 rule format. - -### Agents and modes - -The singular `agent` and deprecated `mode` maps become `agents`. Agent fields become more consistent with the rest of the -V2 config: - -```jsonc -// V1 -{ - "agent": { - "reviewer": { - "prompt": "Review for correctness and missing tests.", - "model": "anthropic/claude-sonnet-4-5", - "variant": "high", - "disable": false, - "permission": { - "edit": "deny" - } - } - } -} - -// V2 -{ - "agents": { - "reviewer": { - "system": "Review for correctness and missing tests.", - "model": "anthropic/claude-sonnet-4-5#high", - "disabled": false, - "permissions": [ - { "action": "edit", "resource": "*", "effect": "deny" } - ] - } - } -} -``` - -`prompt` becomes `system`, `disable` becomes `disabled`, and a separate `variant` joins the model reference after `#`. -`temperature`, `top_p`, and provider-specific `options` move under `request.body`. `maxSteps` becomes `steps`. Entries from -the old `mode` map become primary agents. - -### Snapshots - -Rename the singular `snapshot` field to `snapshots`. Its boolean value does not change: - -```jsonc -// V1 -{ "snapshot": false } - -// V2 -{ "snapshots": false } -``` - -### Attachments - -Rename the singular `attachment` object to `attachments`. Nested image settings keep the same names: - -```jsonc -// V1 -{ "attachment": { "image": { "auto_resize": true } } } - -// V2 -{ "attachments": { "image": { "auto_resize": true } } } -``` - -### MCP servers - -V2 groups servers under `mcp.servers`, replaces `enabled` with the inverse `disabled`, and separates timeout purposes: - -```jsonc -// V1 -{ - "mcp": { - "playwright": { - "type": "local", - "command": ["npx", "@playwright/mcp"], - "enabled": true, - "timeout": 30000 - } - } -} - -// V2 -{ - "mcp": { - "servers": { - "playwright": { - "type": "local", - "command": ["npx", "@playwright/mcp"], - "disabled": false, - "timeout": { - "catalog": 30000, - "execution": 30000 - } - } - } - } -} -``` - -Remote OAuth fields use snake case: `clientId` becomes `client_id`, `clientSecret` becomes `client_secret`, -`callbackPort` becomes `callback_port`, and `redirectUri` becomes `redirect_uri`. The V1 `experimental.mcp_timeout` value -also becomes the default `mcp.timeout.catalog` and `mcp.timeout.execution` values. See [MCP servers](/docs/mcp-servers). - -### Compaction - -V2 groups the retained-context token budget under `keep` and gives the reserve a clearer name: - -```jsonc -// V1 -{ - "compaction": { - "preserve_recent_tokens": 8000, - "reserved": 20000 - } -} - -// V2 -{ - "compaction": { - "keep": { - "tokens": 8000 - }, - "buffer": 20000 - } -} -``` - -`auto` and `prune` keep their names. V2 has no native `tail_turns` field; recent context is retained by token budget instead. -See [Compaction](/docs/compaction). - -### Skills - -V1 separates extra skill paths and URLs. V2 combines both into one ordered array: - -```jsonc -// V1 -{ - "skills": { - "paths": ["./team-skills"], - "urls": ["https://example.com/skills/"] - } -} - -// V2 -{ - "skills": ["./team-skills", "https://example.com/skills/"] -} -``` - -Existing skill files and automatic `.opencode/skills/` discovery do not change. See [Skills](/docs/skills). - -### Commands - -Rename the singular `command` map to `commands`. Join a separate model `variant` to the model reference: - -```jsonc -// V1 -{ - "command": { - "review": { - "template": "Review the current changes.", - "model": "anthropic/claude-sonnet-4-5", - "variant": "high" - } - } -} - -// V2 -{ - "commands": { - "review": { - "template": "Review the current changes.", - "model": "anthropic/claude-sonnet-4-5#high" - } - } -} -``` - -`template`, `description`, `agent`, and `subtask` keep their names. Existing Markdown command definitions remain supported. -See [Commands](/docs/commands). - -### References - -Rename the deprecated singular `reference` map to `references`: - -```jsonc -// V1 -{ "reference": { "docs": "../docs" } } - -// V2 -{ "references": { "docs": "../docs" } } -``` - -V1 already accepts `references`, so no change is needed when the file uses it. Reference entries keep the -same shapes. See [References](/docs/references). - -### Providers - -Rename the singular `provider` map to `providers`. V2 separates the runtime package, endpoint, and request settings: - -```jsonc -// V1 -{ - "provider": { - "acme": { - "npm": "@ai-sdk/openai-compatible", - "api": "https://llm.example.com/v1", - "options": { - "apiKey": "{env:ACME_API_KEY}" - } - } - } -} - -// V2 -{ - "providers": { - "acme": { - "package": "aisdk:@ai-sdk/openai-compatible", - "settings": { - "baseURL": "https://llm.example.com/v1", - "apiKey": "{env:ACME_API_KEY}" - } - } - } -} -``` - -V1 `npm` becomes `package`, and AI SDK packages receive the `aisdk:` prefix. `api` becomes `settings.baseURL`. Provider -`options` are separated into `settings`, `headers`, and `body` according to their request role. See [Providers](/docs/providers). - -### Models and variants - -Models remain nested under their provider, but several model fields become more explicit: - -- `id` becomes `modelID`. -- `tool_call` and `modalities` become `capabilities.tools`, `capabilities.input`, and `capabilities.output`. -- A `status` of `"deprecated"` becomes `disabled: true`. -- Cache costs move from `cache_read` and `cache_write` to `cache.read` and `cache.write`. -- Provider-specific `options` become `settings`. -- A V1 variants object becomes a V2 array with an `id` on each entry. - -```jsonc -// V1 -{ - "variants": { - "high": { - "reasoningEffort": "high" - } - } -} - -// V2 -{ - "variants": [ - { - "id": "high", - "settings": { - "reasoningEffort": "high" - } - } - ] -} -``` - -See [Models](/docs/models) for the complete native model shape. - -### Fields without native equivalents - -Most fields that keep the same shape, including `shell`, `model`, `default_agent`, `autoupdate`, `watcher`, `formatter`, -`lsp`, `instructions`, `enterprise`, and `tool_output`, require no migration. - -These V1 fields do not have one-to-one native V2 config fields: - -- `logLevel`: use `OPENCODE_LOG_LEVEL` when starting OpenCode. -- `server`: use the V2 service and explicit server options; the server API is an intentional breaking change. -- `layout`: remove it; V1 already treated it as deprecated and always used stretch layout. -- `enabled_providers` and `disabled_providers`: there is no native provider allowlist or denylist field yet. -- `small_model`: V2 selects models for internal maintenance agents without a separate top-level field. -- `compaction.tail_turns`: V2 uses `compaction.keep.tokens` instead. - -If your V1 configuration relies on a field without a native equivalent, keep using the supported V1 format rather than -forcing a manual conversion. Run `/report` if V2 does not preserve the behavior you rely on. - -### Agent files - -V1 agent files may use `agent/`, `agents/`, `mode/`, or `modes/`. V2 still discovers all four directories. The preferred -V2 location is: - -```text -.opencode/agents/.md -``` - -Files under a V1 `mode/` or `modes/` directory represent primary agents. When moving one into `agents/`, add -`mode: primary` to its frontmatter. Files under `agent/` can move to `agents/` without changing their path-derived ID. - -When converting the frontmatter to native V2 fields: - -- Keep the Markdown body as the agent's system instructions. -- Rename `prompt` to `system` when it appears in JSON configuration; file bodies do not need a `system` field. -- Rename `disable` to `disabled` and `permission` to `permissions`. -- Join `model` and `variant` as `provider/model#variant`. -- Move `temperature`, `top_p`, and provider-specific options under `request.body`. - -V2 translates legacy agent frontmatter automatically, so these edits are optional. See [Agents](/docs/agents). - -### Command files - -V1 command files may use `command/` or `commands/`. V2 discovers both. The preferred location is: - -```text -.opencode/commands/.md -``` - -Move files from `command/` to the same relative path under `commands/` to preserve command names. The Markdown body remains -the command template, and `description`, `agent`, and `subtask` frontmatter keep the same names. If frontmatter has separate -`model` and `variant` fields, append the variant to the model and remove `variant`: - -```yaml -# V1 -model: anthropic/claude-sonnet-4-5 -variant: high - -# V2 -model: anthropic/claude-sonnet-4-5#high -``` - -See [Commands](/docs/commands). - -### Skill files - -V2 discovers skills from both `.opencode/skill/` and `.opencode/skills/`. The preferred layout is: - -```text -.opencode/skills//SKILL.md -``` - -Move the complete skill directory, not only `SKILL.md`, so relative scripts, references, and other supporting files remain -available. Keep the directory name stable to preserve the skill ID. Existing skill frontmatter and Markdown bodies do not -require a V2 rewrite. See [Skills](/docs/skills). - -### Instruction files - -Existing `AGENTS.md` files stay in place. V2 discovers the global `~/.config/opencode/AGENTS.md` and project `AGENTS.md` -files from the current directory up to the project root. - -If a V1 setup relied on a `CLAUDE.md` fallback, move that guidance into the applicable `AGENTS.md`. V2 currently only -discovers `AGENTS.md`; because non-API V1 behavior is intended to remain compatible, also run `/report` with the affected -project details. See [Instructions](/docs/instructions). - -## TUI configuration - -V1 loaded `tui.json(c)` from the global config directory and from project directories discovered while walking up from -the current directory. V2 instead stores CLI and TUI settings in one global file: - -```text -~/.config/opencode/cli.json -``` - -The CLI owns this file. The background service does not load it, and V2 does not discover or merge project-local -`tui.json(c)` or `cli.json` files. - -The native V2 format groups related settings. For example: - -```jsonc -// V1: ~/.config/opencode/tui.json -{ - "theme": "tokyonight", - "scroll_speed": 2, - "scroll_acceleration": { - "enabled": true - } -} - -// V2: ~/.config/opencode/cli.json -{ - "theme": { - "name": "tokyonight" - }, - "scroll": { - "speed": 2, - "acceleration": true - } -} -``` - -V2 migrates the global TUI configuration automatically. On the first CLI or TUI startup, when `cli.json` does not already -exist, it: - -- Reads `~/.config/opencode/tui.json`. -- Reads persisted TUI preferences from the legacy `kv.json` state file. -- Converts supported settings to the native grouped format and writes `~/.config/opencode/cli.json`. -- Leaves the V1 files unchanged so V1 can continue using them. - -Migration runs only while `cli.json` is absent. Once that file exists, V2 treats it as the source of truth and does not -continually synchronize later changes from `tui.json` or `kv.json`. If you created `cli.json` before starting V2, merge any -V1 settings you still need into it manually. - -Project-local V1 TUI configuration is not migrated because V2 has no project-local CLI configuration. Move settings you -still want into the global `cli.json`; when multiple projects used different values for the same setting, choose the -global behavior you want V2 to use. - -## Plugins - -Rename `plugin` to `plugins`. Replace a package-and-options tuple with an object: - -```jsonc -// V1 -{ - "plugin": [ - "opencode-example-plugin", - ["./plugin/local.ts", { "enabled": true }] - ] -} - -// V2 -{ - "plugins": [ - "opencode-example-plugin", - { - "package": "./plugin/local.ts", - "options": { "enabled": true } - } - ] -} -``` - -V2 discovers local plugins from both `.opencode/plugin/` and `.opencode/plugins/`; use `.opencode/plugins/` for V2 files. -Moving a file between these directories does not migrate its implementation. - -V1 plugins will not work in V2. - -The config entry can be translated automatically, but plugin implementation code must be ported to the new API. The V2 -plugin API is still being finalized during beta, and detailed plugin migration guidance will be published when it is -ready. - -Once the V2 plugin API is finalized, OpenCode should be able to migrate the majority of V1 plugins while keeping related -local modules and dependencies together. See the current beta [Plugins guide](/docs/build/plugins). - -## Server API and clients - -OpenCode 2 has a revised, more ergonomic server API and a new set of clients. Integrations that call the V1 server API -must migrate to the V2 API. - -Use the `@opencode-ai/client` package to access the new clients. The server API and clients are still being finalized -during beta, so their contracts may continue to change. See the generated [API reference](/docs/api) for the current endpoints, -request types, and responses. - -## Verify your setup - -Start `opencode2` in a project and verify your model, provider credentials, agents, permissions, MCP servers, and plugins -before relying on the beta for regular work. Keep your V1 setup until you have confirmed the V2 behavior you need, and do -not point V1 at configuration that you have converted to the native V2 shape. diff --git a/packages/www/content/docs/(docs)/models.mdx b/packages/www/content/docs/(docs)/models.mdx deleted file mode 100644 index 26737745bede..000000000000 --- a/packages/www/content/docs/(docs)/models.mdx +++ /dev/null @@ -1,213 +0,0 @@ ---- -title: "Models" -description: "" ---- - -OpenCode builds its model catalog from [Models.dev](https://models.dev), provider integrations, and your configuration. -Only enabled models whose provider is available for the current project appear in the model picker. - -Connect a provider with `/connect` in the TUI, or configure it in [Providers](/docs/providers). - -## Choose a model - -Open the model picker with `/models` or the default `m` keybind. The picker shows the models available from -providers connected to the current project. - -Select a model to use it in the current session. Switching models updates that session without changing your config. Use -the catalog entries shown in the picker rather than guessing a provider or model name. - -## Per-run model - -Select a model for one non-interactive run with `--model` or `-m`: - -```bash -opencode2 run --model openai/gpt-5.2 "Explain this repository" -opencode2 run -m openai/gpt-5.2#high "Review the current changes" -``` - -Agents and commands can also select their own model. See [Agents](/docs/agents) and [Commands](/docs/commands). - -## Variants - -Variants are named request overlays for one model, commonly used for reasoning effort or token budgets. Available names -are model-specific and are derived from current catalog metadata. Do not assume that names such as `low`, `high`, or -`max` exist for every model; `/variants` shows the valid choices. - -Use `/variants` to choose one for the current model, or press `ctrl+t` to cycle through available variants. - -## Configure - -### Default model - -Set `model` in `opencode.json` or `opencode.jsonc`: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "model": "anthropic/claude-sonnet-4-5" -} -``` - -The configured model becomes the catalog default when its provider is available and the model is enabled. Otherwise, -session execution falls back to the newest available supported model. An explicit model already selected on a session -takes precedence over the default; switching models changes that session and does not rewrite your config. - -See [Config](/docs/config) for configuration locations and precedence. - -### Model settings - -Provider and model entries can supply three kinds of request configuration: - -- `settings` contains provider-package options such as `baseURL`, `reasoningEffort`, or `thinkingConfig`. -- `headers` adds HTTP request headers. -- `body` adds provider-specific fields to the request body. - -These values are provider-specific JSON. OpenCode applies provider values first, then model values, then the selected -variant. Nested `settings` and `body` objects are merged; later array and scalar values replace earlier values. Header -names are matched case-insensitively. - -You can also map a friendly catalog ID to a different API model ID with `modelID`: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "model": "openai/coding-default", - "providers": { - "openai": { - "models": { - "coding-default": { - "modelID": "gpt-5.2", - "name": "Coding default", - "capabilities": { - "tools": true, - "input": ["text", "image"], - "output": ["text"] - }, - "limit": { - "context": 200000, - "output": 32000 - } - } - } - } - } -} -``` - -Here `openai/coding-default` is the selectable catalog reference, while `gpt-5.2` is sent to the provider. When adding a -model that is not already in the catalog, set accurate `capabilities` and `limit` values so OpenCode can expose tools and -enforce the correct context limits. Set `disabled: true` on a model entry to hide it from the available catalog. - -### Custom variants - -Add a variant, or override a catalog variant with the same ID, under the model's `variants` array: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "providers": { - "openai": { - "models": { - "gpt-5.2": { - "settings": { - "reasoningEffort": "medium" - }, - "variants": [ - { - "id": "fast", - "settings": { - "reasoningEffort": "low" - } - }, - { - "id": "deep", - "settings": { - "reasoningEffort": "high", - "reasoningSummary": "auto" - } - } - ] - } - } - } - } -} -``` - -Variant entries support `settings`, `headers`, and `body`. Selecting one deeply overlays its values on the effective -provider and model configuration. An unknown variant fails model resolution instead of silently using the base model. - -### Local models - -For an OpenAI-compatible server, define a provider package, endpoint, and at least one model: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "model": "local/coder", - "providers": { - "local": { - "name": "Local server", - "package": "aisdk:@ai-sdk/openai-compatible", - "settings": { - "baseURL": "http://127.0.0.1:1234/v1" - }, - "models": { - "coder": { - "modelID": "model-name-on-server", - "capabilities": { - "tools": true, - "input": ["text"], - "output": ["text"] - }, - "limit": { - "context": 32768, - "output": 8192 - } - } - } - } - } -} -``` - -Use the server's real model name, limits, modalities, and tool support. OpenCode cannot infer these for a model you add -manually. If the endpoint requires a key, add `apiKey` to provider `settings` using an environment substitution such as -`"apiKey": "{env:LOCAL_API_KEY}"`; do not commit secrets. - -### Model references - -Configuration and CLI options identify a model as `provider/model`, with an optional `#variant`: - -```text -openai/gpt-5.2 -openai/gpt-5.2#high -openrouter/anthropic/claude-sonnet-4.5#high -``` - -OpenCode splits the reference at the first `/`, so model IDs may contain additional slashes. Provider and model IDs are -case-sensitive. Provider IDs cannot contain `/` or `#`, and model IDs cannot contain `#`. - -The expanded config form is equivalent when generated or programmatic configuration is more convenient: - -```jsonc -{ - "model": { - "providerID": "openrouter", - "model": "anthropic/claude-sonnet-4.5" - } -} -``` - -Root, agent, and command `model` fields accept both forms. Use the IDs shown by `/models`, not provider display names. - -### Caveats - -- The selector object uses `model`, while a provider catalog entry uses `modelID` for the upstream API identifier. -- The root `model` currently sets the default provider and model only. Although its selection shape accepts a variant, - the V2 catalog default does not retain it; select a variant in the TUI, with `opencode2 run`, or on an agent or command. -- Model options are provider-specific. A setting accepted by one provider package may be ignored or rejected by another. -- Catalog data, credentials, and config are location-scoped. A model available in one project may be unavailable in - another. -- Configuration files are watched and normally reload automatically, but an in-flight model request keeps the settings - with which it started. diff --git a/packages/www/content/docs/(docs)/providers.mdx b/packages/www/content/docs/(docs)/providers.mdx deleted file mode 100644 index 7e711f9dfd20..000000000000 --- a/packages/www/content/docs/(docs)/providers.mdx +++ /dev/null @@ -1,204 +0,0 @@ ---- -title: "Providers" -description: "" ---- - -OpenCode builds its provider and model catalog from [Models.dev](https://models.dev), then applies the `providers` -overlays from your [configuration](/docs/config). A provider needs both a usable runtime package and, when required, an -active connection. - -## Connect a provider - -Run `/connect` in the TUI, choose an integration, and complete one of the methods it offers: - -```text -/connect -``` - -An integration may support an API key, OAuth, environment variables, or a combination of them. API keys and OAuth -tokens entered through `/connect` are stored by the OpenCode service in its database. Run `/connect` again to replace -or remove a stored credential. - -Providers from Models.dev also declare their standard environment variables. A non-empty declared variable is exposed -as an environment connection automatically, so common providers usually need no config: - -```bash -export ANTHROPIC_API_KEY="your-key" -``` - -For a custom provider, `env` declares the variables that can supply its key: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "providers": { - "acme": { - "env": ["ACME_API_KEY"] - } - } -} -``` - -When several credential sources exist, OpenCode uses the stored credential first, then the first non-empty variable in -`env`, then `settings.apiKey`. Use config substitution instead of committing a literal key: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "providers": { - "acme": { - "settings": { - "apiKey": "{env:ACME_API_KEY}" - } - } - } -} -``` - -Do not commit API keys or authorization headers to your repository. - -## Configure - -The `providers` object is keyed by provider ID. Each provider accepts these fields: - -| Field | Purpose | -| --- | --- | -| `name` | Display name. | -| `env` | Ordered environment variable names that provide a connection. | -| `package` | Runtime provider package. | -| `settings` | JSON settings passed to the runtime package, such as `baseURL`. | -| `headers` | String-valued HTTP headers added to requests. | -| `body` | JSON fields merged into request bodies. | -| `models` | Models to add or override, keyed by catalog model ID. | - -Configuration files are applied from lowest to highest precedence. `settings` and `body` are deep-merged. Headers are -merged case-insensitively. At request time, provider values are inherited by the model, model values override them, and -the selected variant is applied last. - -### Endpoint - -Override `settings.baseURL` to send an existing provider through a proxy or compatible endpoint. Its existing package, -models, and connection continue to apply: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "providers": { - "anthropic": { - "settings": { - "baseURL": "https://llm-proxy.example.com/anthropic" - } - } - } -} -``` - -`settings` is package-specific. A field only has an effect when the selected package supports it. - -### Headers and body - -Headers and body fields can be set at provider, model, or variant scope: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "providers": { - "openai": { - "headers": { - "X-Gateway-Tenant": "engineering" - }, - "body": { - "metadata": { - "application": "opencode" - } - }, - "models": { - "gpt-5.2": { - "headers": { - "X-Model-Policy": "coding" - } - } - } - } - } -} -``` - -These are request overlays, not a generic authentication scheme. Prefer `/connect`, `env`, or `settings.apiKey` for -provider credentials unless the endpoint explicitly requires a custom header. - -### Provider packages - -For an OpenAI-compatible service, use the V2 native compatible package. The model map is explicit because a custom -provider has no Models.dev catalog entries: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "model": "acme/qwen3-coder", - "providers": { - "acme": { - "name": "Acme Gateway", - "env": ["ACME_API_KEY"], - "package": "@opencode-ai/ai/providers/openai-compatible", - "settings": { - "baseURL": "https://llm.acme.example/v1" - }, - "models": { - "qwen3-coder": { - "name": "Qwen 3 Coder", - "capabilities": { - "tools": true, - "input": ["text"], - "output": ["text"] - }, - "limit": { - "context": 131072, - "output": 32768 - } - } - } - } - } -} -``` - -Omit `env` for an endpoint that does not require authentication. The native compatible package requires -`settings.baseURL` and uses bearer authentication when a key is available. - -The `package` field supports two runtime contracts: - -| Form | Contract | -| --- | --- | -| `"@opencode-ai/ai/providers/openai-compatible"` | A V2 native package exporting `model(modelID, settings)`. An npm specifier or absolute `file://` URL may use the same contract. | -| `"aisdk:@ai-sdk/openai-compatible"` | An AI SDK provider package. The `aisdk:` prefix is required. | - -Native packages receive the merged `settings` plus the resolved `apiKey`, `headers`, `body`, and `limits`. AI SDK -packages receive their merged provider options. Use a package's own documentation for accepted settings; OpenCode does -not validate package-specific keys. - -`package` may also be set on one model to override the provider package for that model. - -### Models - -Add a model under a provider's `models` map. The object key is the model ID used in OpenCode; `modelID` is the ID sent to -the provider: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "model": "openai/coding", - "providers": { - "openai": { - "models": { - "coding": { - "modelID": "gpt-5.2", - "name": "GPT-5.2 Coding" - } - } - } - } -} -``` - -See [Models](/docs/models) for model selection, defaults, capabilities, limits, costs, and variants. diff --git a/packages/www/content/docs/(docs)/sharing.mdx b/packages/www/content/docs/(docs)/sharing.mdx deleted file mode 100644 index 666d527bc04a..000000000000 --- a/packages/www/content/docs/(docs)/sharing.mdx +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: "Session sharing" -description: "" ---- - -Session sharing is not yet available in OpenCode V2. V2 does not currently -publish sessions, upload conversation history to a sharing service, or create -public links. - - - The V2 TUI registers `/share`, but it currently only reports that sharing is unavailable. There is no functional - share/unshare command or server API endpoint. - - -## Configuration - -The V2 configuration schema accepts a `share` field with three values: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "share": "manual" -} -``` - -- `"manual"` represents sharing only when explicitly requested. -- `"auto"` represents automatically sharing new sessions. -- `"disabled"` represents preventing session sharing. - -These values are parsed but are not acted on by the current V2 runtime. In -particular, setting `"auto"` does not publish sessions. If `share` is omitted, -V2 leaves the sharing policy unspecified. - -## Beta limitations - -V2 currently provides no public session viewer, share URL, history sync, -retention controls, or unshare/delete operation. Until those surfaces are -implemented in the V2 server and protocol, keep using sessions locally and do -not treat the `share` configuration field as a privacy or publishing control. diff --git a/packages/www/content/docs/(docs)/skills.mdx b/packages/www/content/docs/(docs)/skills.mdx deleted file mode 100644 index b2860a16c4c8..000000000000 --- a/packages/www/content/docs/(docs)/skills.mdx +++ /dev/null @@ -1,227 +0,0 @@ ---- -title: "Skills" -description: "" ---- - -Skills are Markdown instructions that OpenCode can advertise to an agent and -load when they are relevant. A skill can include supporting scripts, -references, and other files in the same directory. - -## Create a skill - -Create one directory per skill with a `SKILL.md` file: - -```text -.opencode/skills/ -└── git-release/ - ├── SKILL.md - ├── scripts/ - │ └── changelog.ts - └── references/ - └── release-policy.md -``` - -```markdown title=".opencode/skills/git-release/SKILL.md" ---- -name: Git Release -description: Prepare release notes, version bumps, and GitHub releases -metadata: - opencode/slash: "true" ---- - -## Workflow - -1. Read `references/release-policy.md`. -2. Summarize merged changes since the previous tag. -3. Propose the version bump before changing files. -4. Run `scripts/changelog.ts` only after the user approves the version. -``` - -Paths in a skill are relative to the directory containing `SKILL.md`. - -## Discovery - -OpenCode automatically adds the following source directories: - -| Scope | Sources | -| --- | --- | -| Global | `~/.config/opencode/skills` | -| Global compatibility | `~/.claude/skills`, `~/.agents/skills` | -| Project | `.opencode/skills` | -| Project compatibility | `.claude/skills`, `.agents/skills` | - -For project sources, OpenCode searches from the current directory upward to -the project root and includes matching directories at every level. - -Within each source directory, OpenCode discovers: - -- Markdown files at the source root, such as `skills/git-release.md` -- `SKILL.md` files at any depth, such as `skills/git-release/SKILL.md` - -The directory form is recommended because it gives the skill a private base -directory for supporting files. - -## Configure sources - -Use the `skills` array in any `opencode.json` or `opencode.jsonc` to add local -directories or HTTP catalogs: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "skills": [ - "./team-skills", - "~/shared/opencode-skills", - "/opt/company-skills", - "https://example.com/opencode/skills/" - ] -} -``` - -Relative paths are resolved from the active OpenCode working directory, not -from the directory containing the config file. Paths beginning with `~/` use -the current user's home directory. Only `http://` and `https://` values are -treated as URL sources. - -Every discovered config document contributes its `skills` entries; the arrays -are additive rather than replacing one another. - -### HTTP catalogs - -An HTTP source is a base URL containing an `index.json`: - -```json title="index.json" -{ - "skills": [ - { - "name": "git-release", - "version": "3", - "files": [ - "git-release.md", - "references/release-policy.md" - ] - } - ] -} -``` - -OpenCode downloads those files from -`/git-release/`. File paths must be safe, relative, -same-origin paths. Each entry must include either `SKILL.md` or a Markdown file -named after the index entry, such as `git-release.md`. - -Use the named Markdown form for HTTP catalogs. Each downloaded skill directory -is itself a source root, so `git-release.md` produces the ID `git-release`; a -root-level `SKILL.md` produces the literal ID `SKILL` in the current V2 -implementation. Increment `version` when files change so OpenCode refreshes -the cached copy. - -## Frontmatter - -V2 reads these fields: - -| Field | Purpose | -| --- | --- | -| `name` | Display name; defaults to the path-derived ID | -| `description` | Summary shown to the model and command catalog | -| `slash` | Set to `false` to hide the skill from the V2 slash-command catalog | -| `metadata.opencode/slash` | Boolean or `"true"`/`"false"`; overrides `slash` | -| `metadata.opencode/autoinvoke` | Set to `false` to omit the skill from model-facing discovery | - -Frontmatter, `name`, and `description` are optional at runtime. However, a -clear `description` is strongly recommended: skills without one are not -advertised to the model. `license`, `compatibility`, and other metadata may be -included for portability, but V2 does not interpret them. - -`opencode/autoinvoke: false` only removes the skill from the model's available -skills list. The skill remains registered and can still be activated explicitly -by its ID. - -## IDs and validation - -The skill ID comes from its path, not its frontmatter: - -| File | ID | -| --- | --- | -| `/git-release.md` | `git-release` | -| `/git-release/SKILL.md` | `git-release` | -| `/teams/release/SKILL.md` | `release` | - -IDs are exact and case-sensitive. V2 currently does not enforce the Agent -Skills name regex, length limits, a match between `name` and the directory, or -a maximum description length. The frontmatter `name` is only a display label. - -For portable, predictable skills, use a unique lowercase kebab-case ID of 1-64 -characters and keep it aligned with the directory name: - -```text -^[a-z0-9]+(-[a-z0-9]+)*$ -``` - -## Precedence - -Skills are keyed by ID. If several sources define the same ID, the later source -wins. Sources are registered in this order, from lower to higher precedence: - -1. Built-in skills -2. `.claude/skills` sources, global first and then from the current directory upward -3. `.agents/skills` sources, global first and then from the current directory upward -4. `~/.config/opencode/skills` -5. Project `.opencode/skills`, from the project root toward the current directory -6. Explicit `skills` config entries, in config priority and array order - -Avoid duplicate IDs unless an override is intentional. - -## Runtime loading - -At each model step, OpenCode advertises permitted skills that have a -description and do not set `opencode/autoinvoke` to `false`. The advertisement -contains only each skill's ID, name, and description; it does not add every -skill body to the prompt. - -When the model calls the `skill` tool with an exact ID, OpenCode: - -1. Resolves the current winning definition for that ID -2. Checks the `skill` permission for the selected agent -3. Adds the Markdown body, without frontmatter, to the conversation -4. Provides the skill's base directory and a sample of up to ten supporting file paths - -Supporting file contents are not loaded automatically. The agent can read a -referenced file when the skill instructs it to do so. The supporting-file -sample is available for directory-based `SKILL.md` skills; flat Markdown skills -receive no neighboring file list. - -In the V2 CLI, skills appear as `/id` commands unless `slash` resolves to -`false`. Selecting one appends the skill body as a skill message and resumes -the session. - -## Permissions - -Permission rules use the `skill` action and the skill ID as the resource. Rules -are evaluated in order, with the last matching rule winning: - -```jsonc title="opencode.jsonc" -{ - "permissions": [ - { "action": "skill", "resource": "*", "effect": "allow" }, - { "action": "skill", "resource": "internal-*", "effect": "deny" }, - { "action": "skill", "resource": "experimental-*", "effect": "ask" } - ] -} -``` - -`deny` removes matching skills from model-facing discovery and rejects skill -tool loading. `ask` advertises the skill but requests approval when the model -loads it. The same rules can be placed under an individual -`agents..permissions` array. - -## Troubleshooting - -If a skill is missing or loads the wrong content: - -1. Confirm the file is either a root-level `*.md` or a nested file named exactly `SKILL.md`. -2. Check the path-derived ID rather than the frontmatter `name`. -3. Add a `description` if the skill should be advertised to the model. -4. Check `opencode/autoinvoke` and the selected agent's `skill` permissions. -5. Look for a later source defining the same ID. -6. For HTTP catalogs, verify `index.json`, same-origin file paths, and a changed `version`. diff --git a/packages/www/content/docs/(docs)/snapshots.mdx b/packages/www/content/docs/(docs)/snapshots.mdx deleted file mode 100644 index 6f178eb9cc34..000000000000 --- a/packages/www/content/docs/(docs)/snapshots.mdx +++ /dev/null @@ -1,108 +0,0 @@ ---- -title: "Undo" -description: "" ---- - -OpenCode snapshots let the default interactive TUI roll back conversation history and related file changes. They are a -convenience for revising recent work, not a replacement for Git commits or backups. - -## Configuration - -Snapshots are enabled by default. Set `snapshots` to `false` in your [configuration](/docs/config#snapshots) to stop capturing -filesystem state: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "snapshots": false -} -``` - -Filesystem snapshots require a Git repository. With snapshots disabled, unavailable, or missing, undo can still stage a -conversation rollback, but it has no captured file state to restore. Disabling snapshots does not delete snapshots that -were already stored. - -## What is captured - -For each model step, OpenCode attempts to capture the worktree immediately before the model call and after a cleanly -completed step. It records the paths changed between those two points on the assistant message. - -Snapshots use a separate internal Git object database in the OpenCode data directory. They do not create commits, move -branches, or intentionally modify your repository's Git index. Capture is limited to the session's active directory, which -may be a subdirectory of the repository. - -Within that directory, snapshots include tracked files and untracked files that are not ignored by Git. An individual -untracked file larger than 2 MiB is excluded. Ignored files, files outside the active directory, and changes to Git -metadata are not captured. - -During undo, OpenCode does not check out an entire tree. It restores only paths attributed to cleanly completed assistant -steps after the selected conversation boundary. Each path is restored to its state before the first affected step. - -## Undo - -Wait for the session to become idle, then run: - -```text -/undo -``` - -The TUI finds the latest non-empty user message and stages a revert at that message: - -- The selected user message and every later message are hidden, but not deleted yet. -- The selected message's text, attachments, and agent mentions are placed in the composer for revision. -- Captured files changed by the affected assistant steps are restored to their earlier contents. Files created by those - steps are removed when they did not exist in the earlier snapshot. -- A summary shows the staged message count and restored paths. - -Running `/undo` again moves the staged boundary to an earlier user message. OpenCode keeps the filesystem state from -immediately before the first undo as the redo baseline, so repeated undos form one wider staged revert rather than a redo -stack. - - - Sending a new prompt while an undo is staged commits the revert. The hidden message range is removed from the active - session history, the currently reverted files are kept, and redo is no longer available. - - -## Redo - -While a revert is staged, run: - -```text -/redo -``` - -Redo clears the staged boundary, makes the hidden messages visible again, and restores affected files to their exact state -immediately before the first undo. It does not rerun the model. After multiple undos, one redo restores the whole staged -range; there is no step-by-step redo stack. - -## Revert a message - -The TUI's **Message Actions** menu also provides **Revert**. It stages the selected message as the conversation boundary -and uses the same file restoration and redo behavior, but it does not copy that message into the composer. - -For a conversation-and-files rollback, select a user message. If an assistant message is selected, that message is hidden, -but only file changes attributed to later assistant steps are restored; the selected assistant message's own file changes -are not included. - -## Limitations and safety - -- Capture is best effort. A failed capture is logged and the model step continues, so conversation rollback may have no - matching file rollback. -- Interrupted or failed steps do not receive a completed end snapshot. File changes made before the failure may remain. -- Shell commands can change databases, services, processes, network resources, Git state, ignored build output, or files - outside the active directory. Undo and redo do not reverse those side effects. -- Undo overwrites the current contents of affected paths with older contents. Redo likewise overwrites those paths with - the pre-undo state, including edits made after running undo. -- Other processes can edit the worktree between capture and restore. The server rejects revert operations while the - session is actively running, but it cannot protect against external editors or commands. -- Snapshot objects can contain complete contents of tracked and non-ignored untracked files. They are stored locally in - the OpenCode data directory; do not treat snapshots as secret-free metadata. -- Undo is not secure erasure. Committing a revert removes messages from the active projection, not from durable session - history or existing snapshot storage. - -Review the staged file summary and your Git diff before continuing. Commit or back up important work independently before -using undo on a dirty worktree. - - - `/undo` and `/redo` are interactive TUI commands. The non-interactive `run` command does not provide them. - diff --git a/packages/www/content/docs/(docs)/troubleshooting.mdx b/packages/www/content/docs/(docs)/troubleshooting.mdx deleted file mode 100644 index 9696c1fdf061..000000000000 --- a/packages/www/content/docs/(docs)/troubleshooting.mdx +++ /dev/null @@ -1,177 +0,0 @@ ---- -title: "Troubleshooting" -description: "Diagnose OpenCode startup, server, and session issues." ---- - - - You can ask OpenCode to debug itself. Describe the problem and ask it to use this troubleshooting page; it can read - the steps below, inspect its service and logs, and help identify the issue. - - -OpenCode runs as two processes: the TUI is a client, while a background server owns sessions, plugins, permissions, and -other application state. Start by determining whether an issue is in the client, the shared server, or a specific project. - -## Check the background service - -Show the current server status: - -```bash -opencode2 service status -``` - -Verify that its API is healthy: - -```bash -opencode2 api get /api/health -``` - -If the service is stuck or unhealthy, restart it: - -```bash -opencode2 service restart -``` - -From inside the TUI, run `/restart` to restart the managed service and reconnect: - -```text -/restart -``` - -You can also stop and start it explicitly: - -```bash -opencode2 service stop -opencode2 service start -``` - - - OpenCode normally discovers or starts the shared background service automatically. The service commands are only - needed when diagnosing its lifecycle. - - -## Run an isolated session - -Use standalone mode to run the TUI with a private server that exits with it: - -```bash -opencode2 --standalone -``` - -If an issue disappears in standalone mode, it is likely related to the shared background service rather than the TUI or -project itself. - -## Inspect the API - -The `api` command uses the same discovery and authentication flow as the TUI. It accepts either an HTTP method and path or -an OpenAPI operation ID. - -See the [API reference](/docs/api) for all endpoints and operation IDs. - -Pass a JSON request body with `--data` or `-d`, and add headers with `--header` or `-H`. - - - Running `opencode2 api` may start the background service when no compatible healthy service is available. - - -## Read logs - -Installed builds write logs to: - -```text -~/.local/share/opencode/log/opencode.log -``` - -Follow the log while reproducing the problem: - -```bash -tail -f ~/.local/share/opencode/log/opencode.log -``` - -Each line includes a process `run` ID and a `role` field. Use `role=cli` for TUI and command startup, and `role=server` for -session, provider, plugin, permission, and tool activity. - -```bash -grep 'role=cli' ~/.local/share/opencode/log/opencode.log -grep 'role=server' ~/.local/share/opencode/log/opencode.log -grep 'run=8fc3b1d5' ~/.local/share/opencode/log/opencode.log -``` - -Increase verbosity for one reproduction: - -```bash -OPENCODE_LOG_LEVEL=DEBUG opencode2 -``` - -## Service files - -The shared server registers itself at: - -```text -~/.local/state/opencode/service.json -``` - -Its private service configuration is stored separately at: - -```text -~/.config/opencode/service.json -``` - -The database normally lives at: - -```text -~/.local/share/opencode/opencode-next.db -``` - -`OPENCODE_DB` can override the database location. - - - Do not delete or edit service files or the database while troubleshooting. Use the service commands to manage the - daemon, and make a backup before inspecting persistent data with external tools. - - -## Explicit servers - -When connecting with `--server`, set `OPENCODE_PASSWORD` if the server requires authentication: - -```bash -OPENCODE_PASSWORD=secret opencode2 --server http://127.0.0.1:4096 -``` - -The CLI checks the server before opening the TUI and reports whether it is unreachable, requires a password, or rejected -the supplied password. - -## Report an issue - -Include the following when reporting a reproducible problem: - -- Output from `opencode2 --version` -- Output from `opencode2 service status` -- The smallest sequence of steps that reproduces the issue -- Whether the issue also occurs with `opencode2 --standalone` -- Relevant log lines, including their `run` and `role` fields - -Remove API keys, authorization headers, prompts, file contents, and other sensitive data before sharing logs. - -## Local development - -When working from the OpenCode repository, run V2 commands from the repository root: - -```bash -bun dev -``` - -The local development channel keeps its logs, SQLite database, and service registration separate from installed builds: - -```text -~/.local/share/opencode/log/opencode-local.log -~/.local/share/opencode/opencode-local.db -~/.local/state/opencode/service-local.json -``` - -Use the same diagnostics through the package development command: - -```bash -bun dev service status -bun dev service restart -bun dev api get /api/health -``` diff --git a/packages/www/content/docs/api/index.mdx b/packages/www/content/docs/api/index.mdx deleted file mode 100644 index 102ff30324e2..000000000000 --- a/packages/www/content/docs/api/index.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: "API Reference" -description: "OpenCode HTTP API." ---- - -The endpoint reference is generated from the current OpenCode V2 [OpenAPI specification](/openapi.json). diff --git a/packages/www/content/docs/api/meta.json b/packages/www/content/docs/api/meta.json deleted file mode 100644 index bd04fc44ffca..000000000000 --- a/packages/www/content/docs/api/meta.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "title": "API", - "description": "Use the OpenCode HTTP API.", - "root": true, - "pages": ["index"] -} diff --git a/packages/www/content/docs/build/client.mdx b/packages/www/content/docs/build/client.mdx index ace8cc573b54..51805cfb1830 100644 --- a/packages/www/content/docs/build/client.mdx +++ b/packages/www/content/docs/build/client.mdx @@ -6,12 +6,12 @@ description: "Connect an application to the OpenCode HTTP API." `@opencode-ai/client` is the generated TypeScript client for the OpenCode HTTP API. Use it when your application connects to an OpenCode server over the network. Its types and methods are generated from the same contract as the -[API reference](/docs/api). +[API reference](/api). - + The V2 API and client are beta. Method names, inputs, and outputs may change before the stable release. - + ## Install @@ -70,6 +70,48 @@ for await (const event of client.event.subscribe()) { } ``` +## Local background service + +The main client entrypoints are browser-compatible and do not include local +process management. In a Node application, import the native Promise service +API from `@opencode-ai/client/service`. + +- `Service.discover()` returns a healthy registered endpoint without starting + a process. +- `Service.ensure()` returns a compatible service, starting one when needed. +- `Service.stop()` stops the exact registered service instance. +- `Service.headers(endpoint)` creates the authentication headers for a client. + +```ts +import { OpenCode } from "@opencode-ai/client" +import { Service } from "@opencode-ai/client/service" + +const endpoint = await Service.ensure() +const client = OpenCode.make({ + baseUrl: endpoint.url, + headers: Service.headers(endpoint), +}) + +const health = await client.health.get() +``` + +`Service.ensure()` accepts an optional registration file, required version, +service command, and `onStart` callback: + +```ts +const endpoint = await Service.ensure({ + file: "/var/run/opencode/service.json", + version: "2.0.0", + command: ["opencode", "serve", "--service"], + onStart(reason, previousVersion) { + console.log(reason, previousVersion) + }, +}) +``` + +Omit these options to use the standard registration path and +`opencode serve --service` command. + ## Effect OpenCode provides a first-class Effect client through the @@ -106,16 +148,11 @@ const session = await Effect.runPromise( Streaming operations, including `client.event.subscribe()` and `client.session.log(...)`, return Effect `Stream` values. -### Service - -`Service` discovers and manages the local OpenCode background service from a -Node application: +### Local background service -- `Service.discover()` returns a healthy registered endpoint without starting - a process. -- `Service.ensure()` returns a compatible service, starting one when needed. -- `Service.stop()` stops the registered service. -- `Service.headers(endpoint)` creates the authentication headers for a client. +The Node-only `@opencode-ai/client/effect/service` entrypoint exposes the same +operations as Effect values. Add `@effect/platform-node` and provide its +filesystem layer when running them. ```sh bun add @effect/platform-node diff --git a/packages/www/content/docs/build/index.mdx b/packages/www/content/docs/build/index.mdx index ee92d3acdc72..02614255ea96 100644 --- a/packages/www/content/docs/build/index.mdx +++ b/packages/www/content/docs/build/index.mdx @@ -1,24 +1,23 @@ --- title: "Build" description: "Build on the engine used by millions daily." -mode: "wide" --- - + Build plugins that add tools, integrations, commands, agents, and custom behavior while keeping the rest of OpenCode intact. - + Connect to OpenCode with the same client used by the TUI and desktop app, then build any interface, workflow, or agent experience around it. - + Embed OpenCode directly into your application and build a completely custom agent, interface, or developer product around it. - + The plugin API, client, and SDK are still being finalized during beta and may change before OpenCode 2.0 is stable. - + diff --git a/packages/www/content/docs/build/meta.json b/packages/www/content/docs/build/meta.json deleted file mode 100644 index 44436a37eac1..000000000000 --- a/packages/www/content/docs/build/meta.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "title": "Build", - "description": "Extend and embed OpenCode.", - "root": true, - "defaultOpen": true, - "pages": ["index", "plugins", "client", "sdk"] -} diff --git a/packages/www/content/docs/build/meta.ts b/packages/www/content/docs/build/meta.ts new file mode 100644 index 000000000000..cdc062676d4f --- /dev/null +++ b/packages/www/content/docs/build/meta.ts @@ -0,0 +1,6 @@ +import { defineMeta } from "blume" + +export default defineMeta({ + title: "Build", + pages: ["index", "plugins", "client", "sdk"], +}) diff --git a/packages/www/content/docs/build/plugins.mdx b/packages/www/content/docs/build/plugins.mdx index 74c7a6e5e9b1..6522c55906dc 100644 --- a/packages/www/content/docs/build/plugins.mdx +++ b/packages/www/content/docs/build/plugins.mdx @@ -7,10 +7,10 @@ Plugins extend OpenCode in-process. They can transform agents, models, commands, integrations, references, skills, and tools; intercept model requests and tool execution; and call a subset of the V2 client. - + The V2 plugin API is beta. Entrypoints, hooks, draft shapes, and configuration may change before the stable release. Use the `/v2` exports described on this page. - + ## Load plugins @@ -49,7 +49,7 @@ Use the object form to pass JSON configuration to the plugin. OpenCode passes `options` unchanged as `ctx.options`; omitted options become an empty object. The plugin owns validation and defaults for its options. -See [Config](/docs/config#locations) for configuration locations and precedence. +See [Config](/config#locations) for configuration locations and precedence. Entries from all applicable files are processed from lowest to highest precedence rather than replacing the entire array. @@ -162,22 +162,22 @@ connections, and background tasks. ### Context -The plugin context is essentially an [OpenCode server client](/docs/build/client). +The plugin context is essentially an [OpenCode server client](/build/client). Its read and action methods use the same inputs and responses as the client. It adds plugin-only methods for transforms, runtime hooks, reloads, registrations, and plugin options. | Capability | Available operations | | ---------------------- | -------------------------------------------------------------------------------------------- | -| `ctx.agent` | `list`, `transform`, `reload` | +| `ctx.agent` | `list`, `get`, `transform`, `reload` | | `ctx.catalog.provider` | `list`, `get` | -| `ctx.catalog.model` | `list`, `default` | +| `ctx.catalog.model` | `list`, `get`, `default` | | `ctx.catalog` | `transform`, `reload` | | `ctx.command` | `list`, `transform`, `reload` | | `ctx.integration` | `list`, `get`, `connect`, `attempt`, `transform`, `reload`, and connection lookup/resolution | | `ctx.plugin` | `list` currently active plugin IDs | | `ctx.reference` | `list`, `transform`, `reload` | -| `ctx.session` | `create`, `get`, `prompt`, `command`, `interrupt`, and `hook` | +| `ctx.session` | `create`, `get`, `prompt`, `command`, `synthetic`, `interrupt`, and `hook` | | `ctx.skill` | `list`, `transform`, `reload` | | `ctx.tool` | `transform` and `hook` | | `ctx.aisdk` | `hook` | @@ -403,7 +403,7 @@ opencode2 api get /api/plugin ``` If a plugin is absent, check the server log described in -[Troubleshooting](/docs/troubleshooting#read-logs). Invalid modules and setup failures are +[Troubleshooting](/troubleshooting#read-logs). Invalid modules and setup failures are logged; one failing package does not prevent unrelated valid packages from being resolved. @@ -440,6 +440,7 @@ fibers, and registrations are released when the plugin reloads or unloads. OpenCode does not expose its private Core services to the plugin; use the capabilities on `ctx`. -Typed tools can use `Schema` from `effect` and the contracts exported from -`@opencode-ai/plugin/v2/effect/tool`. Their executors return an Effect and may -fail with the typed tool failure channel. +Typed tools can use `Schema` from `effect` and `Tool.make` from +`@opencode-ai/plugin/v2/effect/tool`. Effect and Promise plugins use the same +`tools.add(name, tool, options?)` registration shape. Effect executors +return an Effect and may fail with the typed tool failure channel. diff --git a/packages/www/content/docs/build/sdk.mdx b/packages/www/content/docs/build/sdk.mdx index 0262cc3c786d..8b7cfcaf1ef3 100644 --- a/packages/www/content/docs/build/sdk.mdx +++ b/packages/www/content/docs/build/sdk.mdx @@ -1,18 +1,27 @@ --- title: "SDK" -description: "Embed an OpenCode host in an Effect application." +description: "Embed OpenCode directly in your application." --- -`@opencode-ai/sdk-next` is the Effect-native SDK for applications that need to -host OpenCode in-process. Unlike the [network client](/docs/build/client), it assembles the -OpenCode server and routes API calls through its HTTP router in memory. It opens -no HTTP listener and adds no network hop between the client and server. +We're working on a general-purpose SDK for embedding OpenCode directly inside +your application. The regular SDK is coming soon. - +An Effect-native version is available now for applications built with Effect. +Its current documentation is below. For other applications, run OpenCode as a +server and use the [TypeScript client](/build/client) in the meantime. + +## Effect + +`@opencode-ai/sdk-next` hosts OpenCode in-process. Unlike the +[network client](/build/client), it assembles the OpenCode server and routes API +calls through its HTTP router in memory. It opens no HTTP listener and adds no +network hop between the client and server. + + The V2 SDK is beta and currently private to the OpenCode workspace. It is not published for external installation yet, and its package name and API may change before release. - + ## Create a host @@ -72,4 +81,4 @@ const active = await Effect.runPromise( Call `opencode.plugin(...)` to register an embedded V2 plugin. Embedded plugins use the same discovery and location-scoped activation path as configured plugins. The SDK also exports `Tool` for plugin-defined tools. See the -[Plugins guide](/docs/build/plugins) for the plugin shape and available hooks. +[Plugins guide](/build/plugins) for the plugin shape and available hooks. diff --git a/packages/docs/config.mdx b/packages/www/content/docs/config.mdx similarity index 99% rename from packages/docs/config.mdx rename to packages/www/content/docs/config.mdx index bf53d35b4520..413352e90792 100644 --- a/packages/docs/config.mdx +++ b/packages/www/content/docs/config.mdx @@ -3,9 +3,9 @@ title: "Config" description: "" --- - + You shouldn't have to configure OpenCode manually. Ask OpenCode to update its configuration for you. - + ## Format diff --git a/packages/docs/index.mdx b/packages/www/content/docs/index.mdx similarity index 90% rename from packages/docs/index.mdx rename to packages/www/content/docs/index.mdx index bbb3ccfdfe5e..330b1df41b52 100644 --- a/packages/docs/index.mdx +++ b/packages/www/content/docs/index.mdx @@ -1,16 +1,16 @@ --- -title: "Intro" +title: "Get started" description: "Get started with OpenCode." --- - + These docs are for the beta version of OpenCode, which will become OpenCode 2.0. The beta is still changing: we may wipe your data, things may break, and APIs, configuration, and plugin APIs may change. - + ## Install -The curl install script is not available in beta. +The curl install script is not available in beta. You can also install it with the following package managers. @@ -40,7 +40,7 @@ You can also install it with the following package managers. The package uses a trusted postinstall script to select the native binary for your platform. The Bun and pnpm commands above explicitly allow that script to run. -During beta, the binary is called `opencode2`. +During beta, the binary is called `opencode2`. ### Homebrew @@ -52,11 +52,11 @@ Arch Linux installation is not available in beta. ### Windows - + For the best experience on Windows, install [Windows Subsystem for Linux (WSL)](https://learn.microsoft.com/windows/wsl/install), open your Linux distribution, and use one of the beta package manager commands above. - + @@ -119,7 +119,7 @@ You are now ready to use OpenCode in your project. Here are a few common workflo Ask OpenCode to explain your codebase. -Use `@` to fuzzy search for files in the project. +Use `@` to fuzzy search for files in the project. ```text How is authentication handled in @packages/functions/src/api/index.ts @@ -135,7 +135,7 @@ Create a screen that shows recently deleted notes. From this screen, the user can restore a note or permanently delete it. ``` -Give OpenCode plenty of context and examples. +Give OpenCode plenty of context and examples. ### Undo changes diff --git a/packages/www/content/docs/meta.json b/packages/www/content/docs/meta.json deleted file mode 100644 index 0971e69803db..000000000000 --- a/packages/www/content/docs/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "Documentation", - "pages": ["(docs)", "build", "api"] -} diff --git a/packages/www/content/docs/meta.ts b/packages/www/content/docs/meta.ts new file mode 100644 index 000000000000..73b351f829e0 --- /dev/null +++ b/packages/www/content/docs/meta.ts @@ -0,0 +1,5 @@ +import { defineMeta } from "blume" + +export default defineMeta({ + pages: ["index", "migrate-v1", "config", "troubleshooting", "configure", "build"], +}) diff --git a/packages/docs/migrate-v1.mdx b/packages/www/content/docs/migrate-v1.mdx similarity index 99% rename from packages/docs/migrate-v1.mdx rename to packages/www/content/docs/migrate-v1.mdx index aa90e7d7f892..63eff7b17a46 100644 --- a/packages/docs/migrate-v1.mdx +++ b/packages/www/content/docs/migrate-v1.mdx @@ -17,15 +17,15 @@ Existing server config files, agent definitions, command definitions, skills, an continue to work without changes. If one of these stops working in V2, treat it as a beta compatibility bug rather than an expected migration requirement. - + Run `/report` if existing V1 functionality does not work in V2. The report skill collects diagnostics and helps you file a compatibility issue. - + - + OpenCode 2.0 is in beta. Beta data may be wiped, features may break unintentionally, and the server and plugin APIs may continue to change. - + During the beta, OpenCode V1 and V2 use different executable names. You can keep using `opencode` for V1 while trying V2 with `opencode2`. @@ -551,7 +551,7 @@ Rename `plugin` to `plugins`. Replace a package-and-options tuple with an object V2 discovers local plugins from both `.opencode/plugin/` and `.opencode/plugins/`; use `.opencode/plugins/` for V2 files. Moving a file between these directories does not migrate its implementation. -V1 plugins will not work in V2. +V1 plugins will not work in V2. The config entry can be translated automatically, but plugin implementation code must be ported to the new API. The V2 plugin API is still being finalized during beta, and detailed plugin migration guidance will be published when it is diff --git a/packages/docs/troubleshooting.mdx b/packages/www/content/docs/troubleshooting.mdx similarity index 97% rename from packages/docs/troubleshooting.mdx rename to packages/www/content/docs/troubleshooting.mdx index f0fe58cee922..e5fae3981875 100644 --- a/packages/docs/troubleshooting.mdx +++ b/packages/www/content/docs/troubleshooting.mdx @@ -3,10 +3,10 @@ title: "Troubleshooting" description: "Diagnose OpenCode startup, server, and session issues." --- - + You can ask OpenCode to debug itself. Describe the problem and ask it to use this troubleshooting page; it can read the steps below, inspect its service and logs, and help identify the issue. - + OpenCode runs as two processes: the TUI is a client, while a background server owns sessions, plugins, permissions, and other application state. Start by determining whether an issue is in the client, the shared server, or a specific project. @@ -44,10 +44,10 @@ opencode2 service stop opencode2 service start ``` - + OpenCode normally discovers or starts the shared background service automatically. The service commands are only needed when diagnosing its lifecycle. - + ## Run an isolated session @@ -69,9 +69,9 @@ See the [API reference](/api) for all endpoints and operation IDs. Pass a JSON request body with `--data` or `-d`, and add headers with `--header` or `-H`. - + Running `opencode2 api` may start the background service when no compatible healthy service is available. - + ## Read logs @@ -124,10 +124,10 @@ The database normally lives at: `OPENCODE_DB` can override the database location. - + Do not delete or edit service files or the database while troubleshooting. Use the service commands to manage the daemon, and make a backup before inspecting persistent data with external tools. - + ## Explicit servers diff --git a/packages/docs/openapi.json b/packages/www/openapi.json similarity index 100% rename from packages/docs/openapi.json rename to packages/www/openapi.json diff --git a/packages/www/package.json b/packages/www/package.json index de5219829b32..e55370a9eb8c 100644 --- a/packages/www/package.json +++ b/packages/www/package.json @@ -1,41 +1,30 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/www", - "version": "1.17.18", "private": true, "type": "module", "scripts": { - "dev": "vite dev --host 0.0.0.0 --port 3000", - "build": "vite build", - "preview": "vite preview --host 0.0.0.0", - "pretypecheck": "fumadocs-mdx", - "typecheck": "tsgo --noEmit" + "dev": "bun run generate && blume dev --host --port 3000", + "build": "bun run generate && blume build && bun script/prepare-cloudflare.ts", + "deploy": "wrangler deploy --config dist/server/wrangler.json", + "generate": "bun script/generate-theme-tokens.ts", + "check:generated": "bun script/generate-theme-tokens.ts --check", + "typecheck": "blume check", + "validate": "blume validate", + "doctor": "blume doctor" }, "dependencies": { - "@cloudflare/vite-plugin": "1.44.0", - "@tailwindcss/vite": "4.3.2", - "@tanstack/react-router": "1.170.17", - "@tanstack/react-start": "1.168.27", - "@tanstack/router-plugin": "1.168.19", - "fumadocs-core": "16.11.1", - "fumadocs-mdx": "15.1.0", - "fumadocs-ui": "16.11.1", - "react": "19.2.7", - "react-dom": "19.2.7", - "tailwindcss": "4.3.2", - "vite": "8.1.4" + "blume": "1.1.4" }, "devDependencies": { - "@types/mdx": "2.0.14", - "@types/node": "catalog:", - "@types/react": "19.2.17", - "@types/react-dom": "19.2.3", - "@typescript/native-preview": "catalog:", - "@vitejs/plugin-react": "6.0.3", - "typescript": "catalog:", + "@astrojs/cloudflare": "14.1.4", + "@types/bun": "catalog:", + "astro": "7.1.3", + "effect": "catalog:", + "prettier": "3.6.2", "wrangler": "4.110.0" }, "engines": { - "node": ">=22" + "node": ">=22.12" } } diff --git a/packages/www/pages/index.astro b/packages/www/pages/index.astro new file mode 100644 index 000000000000..40064206284b --- /dev/null +++ b/packages/www/pages/index.astro @@ -0,0 +1,5 @@ +--- +export const prerender = false + +return Astro.redirect(`${import.meta.env.BASE_URL}docs`, 308) +--- diff --git a/packages/docs/script/generate-theme-tokens.ts b/packages/www/script/generate-theme-tokens.ts similarity index 97% rename from packages/docs/script/generate-theme-tokens.ts rename to packages/www/script/generate-theme-tokens.ts index 25f614d119b3..0c849377142e 100644 --- a/packages/docs/script/generate-theme-tokens.ts +++ b/packages/www/script/generate-theme-tokens.ts @@ -55,7 +55,7 @@ const example = { } satisfies ThemeDocument Schema.decodeUnknownSync(ThemeDocument)(example) const output = await format( - `{/* Generated by packages/docs/script/generate-theme-tokens.ts. Do not edit. */} + `{/* Generated by packages/www/script/generate-theme-tokens.ts. Do not edit. */} \`\`\`json title="my-theme.json" ${JSON.stringify(example, null, 2)} @@ -102,7 +102,7 @@ surfaces that need different contrast without changing the base theme. if (process.argv.includes("--check")) { const current = await Bun.file(target).text() if (current === output) process.exit(0) - console.error("Generated theme token documentation is stale. Run `bun run generate` from packages/docs.") + console.error("Generated theme token documentation is stale. Run `bun run generate` from packages/www.") process.exit(1) } diff --git a/packages/www/script/prepare-cloudflare.ts b/packages/www/script/prepare-cloudflare.ts new file mode 100644 index 000000000000..3e8099a73b05 --- /dev/null +++ b/packages/www/script/prepare-cloudflare.ts @@ -0,0 +1,8 @@ +const path = "dist/server/wrangler.json" +const config = await Bun.file(path).json() + +delete config.kv_namespaces +delete config.images +delete config.previews + +await Bun.write(path, JSON.stringify(config)) diff --git a/packages/docs/snippets/generated/theme-tokens.mdx b/packages/www/snippets/generated/theme-tokens.mdx similarity index 99% rename from packages/docs/snippets/generated/theme-tokens.mdx rename to packages/www/snippets/generated/theme-tokens.mdx index 932072f6cb4c..39873c357142 100644 --- a/packages/docs/snippets/generated/theme-tokens.mdx +++ b/packages/www/snippets/generated/theme-tokens.mdx @@ -1,4 +1,4 @@ -{/* Generated by packages/docs/script/generate-theme-tokens.ts. Do not edit. */} +{/* Generated by packages/www/script/generate-theme-tokens.ts. Do not edit. */} ```json title="my-theme.json" { diff --git a/packages/www/source.config.ts b/packages/www/source.config.ts deleted file mode 100644 index b163c92dbf70..000000000000 --- a/packages/www/source.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { defineDocs } from "fumadocs-mdx/config" - -export const docs = defineDocs({ - dir: "content/docs", -}) diff --git a/packages/www/src/components/mdx.tsx b/packages/www/src/components/mdx.tsx deleted file mode 100644 index 443947fa9f34..000000000000 --- a/packages/www/src/components/mdx.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Children, isValidElement, type ComponentProps, type ReactNode } from "react" -import { Callout } from "fumadocs-ui/components/callout" -import { Card, Cards } from "fumadocs-ui/components/card" -import { Tab as FumadocsTab, Tabs as FumadocsTabs } from "fumadocs-ui/components/tabs" -import defaultMdxComponents from "fumadocs-ui/mdx" -import type { MDXComponents } from "mdx/types" - -function Tip(props: { children: ReactNode }) { - return {props.children} -} - -function Note(props: { children: ReactNode }) { - return {props.children} -} - -function Warning(props: { children: ReactNode }) { - return {props.children} -} - -function Tab(props: { title?: string; value?: string; children: ReactNode }) { - return {props.children} -} - -function Tabs(props: { children: ReactNode }) { - const items = Children.toArray(props.children).flatMap((child) => { - if (!isValidElement>(child)) return [] - const value = child.props.value ?? child.props.title - return value ? [value] : [] - }) - return {props.children} -} - -function CardGroup(props: { children: ReactNode; cols?: number }) { - return {props.children} -} - -export function getMdxComponents(components?: MDXComponents) { - return { - ...defaultMdxComponents, - Tip, - Note, - Warning, - Tabs, - Tab, - CardGroup, - Card, - ...components, - } satisfies MDXComponents -} - -export const useMDXComponents = getMdxComponents - -declare global { - type MDXProvidedComponents = ReturnType -} diff --git a/packages/www/src/lib/layout.tsx b/packages/www/src/lib/layout.tsx deleted file mode 100644 index c226f0f708b5..000000000000 --- a/packages/www/src/lib/layout.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared" - -export function baseOptions(): BaseLayoutProps { - return { - nav: { - title: "OpenCode", - url: "/", - }, - links: [ - { - text: "GitHub", - url: "https://github.com/anomalyco/opencode", - external: true, - }, - ], - } -} diff --git a/packages/www/src/lib/source.ts b/packages/www/src/lib/source.ts deleted file mode 100644 index 8a72b43aaf72..000000000000 --- a/packages/www/src/lib/source.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { docs } from "collections/server" -import { loader } from "fumadocs-core/source" - -export const source = loader({ - baseUrl: "/docs", - source: docs.toFumadocsSource(), -}) diff --git a/packages/www/src/routeTree.gen.ts b/packages/www/src/routeTree.gen.ts deleted file mode 100644 index 6a5e345a969d..000000000000 --- a/packages/www/src/routeTree.gen.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* eslint-disable */ - -// @ts-nocheck - -// noinspection JSUnusedGlobalSymbols - -// This file was automatically generated by TanStack Router. -// You should NOT make any changes in this file as it will be overwritten. -// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. - -import { Route as rootRouteImport } from './routes/__root' -import { Route as IndexRouteImport } from './routes/index' -import { Route as DocsSplatRouteImport } from './routes/docs/$' -import { Route as ApiSearchRouteImport } from './routes/api/search' - -const IndexRoute = IndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => rootRouteImport, -} as any) -const DocsSplatRoute = DocsSplatRouteImport.update({ - id: '/docs/$', - path: '/docs/$', - getParentRoute: () => rootRouteImport, -} as any) -const ApiSearchRoute = ApiSearchRouteImport.update({ - id: '/api/search', - path: '/api/search', - getParentRoute: () => rootRouteImport, -} as any) - -export interface FileRoutesByFullPath { - '/': typeof IndexRoute - '/api/search': typeof ApiSearchRoute - '/docs/$': typeof DocsSplatRoute -} -export interface FileRoutesByTo { - '/': typeof IndexRoute - '/api/search': typeof ApiSearchRoute - '/docs/$': typeof DocsSplatRoute -} -export interface FileRoutesById { - __root__: typeof rootRouteImport - '/': typeof IndexRoute - '/api/search': typeof ApiSearchRoute - '/docs/$': typeof DocsSplatRoute -} -export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/api/search' | '/docs/$' - fileRoutesByTo: FileRoutesByTo - to: '/' | '/api/search' | '/docs/$' - id: '__root__' | '/' | '/api/search' | '/docs/$' - fileRoutesById: FileRoutesById -} -export interface RootRouteChildren { - IndexRoute: typeof IndexRoute - ApiSearchRoute: typeof ApiSearchRoute - DocsSplatRoute: typeof DocsSplatRoute -} - -declare module '@tanstack/react-router' { - interface FileRoutesByPath { - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexRouteImport - parentRoute: typeof rootRouteImport - } - '/docs/$': { - id: '/docs/$' - path: '/docs/$' - fullPath: '/docs/$' - preLoaderRoute: typeof DocsSplatRouteImport - parentRoute: typeof rootRouteImport - } - '/api/search': { - id: '/api/search' - path: '/api/search' - fullPath: '/api/search' - preLoaderRoute: typeof ApiSearchRouteImport - parentRoute: typeof rootRouteImport - } - } -} - -const rootRouteChildren: RootRouteChildren = { - IndexRoute: IndexRoute, - ApiSearchRoute: ApiSearchRoute, - DocsSplatRoute: DocsSplatRoute, -} -export const routeTree = rootRouteImport - ._addFileChildren(rootRouteChildren) - ._addFileTypes() - -import type { getRouter } from './router.tsx' -import type { createStart } from '@tanstack/react-start' -declare module '@tanstack/react-start' { - interface Register { - ssr: true - router: Awaited> - } -} diff --git a/packages/www/src/router.tsx b/packages/www/src/router.tsx deleted file mode 100644 index 0e0657259f37..000000000000 --- a/packages/www/src/router.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { createRouter } from "@tanstack/react-router" -import { routeTree } from "./routeTree.gen" - -export function getRouter() { - return createRouter({ - routeTree, - scrollRestoration: true, - defaultPreload: "intent", - }) -} - -declare module "@tanstack/react-router" { - interface Register { - router: ReturnType - } -} diff --git a/packages/www/src/routes/__root.tsx b/packages/www/src/routes/__root.tsx deleted file mode 100644 index 3af1ba498fab..000000000000 --- a/packages/www/src/routes/__root.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router" -import { RootProvider } from "fumadocs-ui/provider/tanstack" -import type { ReactNode } from "react" -import styles from "../styles.css?url" - -export const Route = createRootRoute({ - head: () => ({ - meta: [ - { charSet: "utf-8" }, - { name: "viewport", content: "width=device-width, initial-scale=1" }, - { title: "OpenCode" }, - { - name: "description", - content: "The open source AI coding agent.", - }, - ], - links: [{ rel: "stylesheet", href: styles }], - }), - shellComponent: RootDocument, -}) - -function RootDocument(props: { children: ReactNode }) { - return ( - - - - - - {props.children} - - - - ) -} diff --git a/packages/www/src/routes/api/search.ts b/packages/www/src/routes/api/search.ts deleted file mode 100644 index 2e29939209ce..000000000000 --- a/packages/www/src/routes/api/search.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router" -import { createFromSource } from "fumadocs-core/search/server" -import { source } from "@/lib/source" - -const search = createFromSource(source, { - language: "english", -}) - -export const Route = createFileRoute("/api/search")({ - server: { - handlers: { - GET: ({ request }) => search.GET(request), - }, - }, -}) diff --git a/packages/www/src/routes/docs/$.tsx b/packages/www/src/routes/docs/$.tsx deleted file mode 100644 index 9d0060a03474..000000000000 --- a/packages/www/src/routes/docs/$.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { createFileRoute, notFound } from "@tanstack/react-router" -import { createServerFn } from "@tanstack/react-start" -import browserCollections from "collections/browser" -import { useFumadocsLoader } from "fumadocs-core/source/client" -import { DocsLayout } from "fumadocs-ui/layouts/docs" -import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/layouts/docs/page" -import { Suspense } from "react" -import { getMdxComponents } from "@/components/mdx" -import { baseOptions } from "@/lib/layout" -import { source } from "@/lib/source" - -export const Route = createFileRoute("/docs/$")({ - loader: async ({ params }) => { - const slugs = params._splat?.split("/").filter(Boolean) ?? [] - const data = await loadPage({ data: slugs }) - await clientLoader.preload(data.path) - return data - }, - head: ({ loaderData }) => ({ - meta: [ - { title: `${loaderData?.title ?? "Docs"} | OpenCode` }, - { name: "description", content: loaderData?.description ?? "OpenCode documentation." }, - ], - }), - component: Documentation, -}) - -const loadPage = createServerFn({ method: "GET" }) - .validator((slugs: string[]) => slugs) - .handler(async ({ data: slugs }) => { - const page = source.getPage(slugs) - if (!page) throw notFound() - return { - path: page.path, - title: page.data.title, - description: page.data.description, - tree: await source.serializePageTree(source.getPageTree()), - } - }) - -const clientLoader = browserCollections.docs.createClientLoader({ - component({ toc, frontmatter, default: Content }) { - return ( - - {frontmatter.title} - {frontmatter.description} - - - - - ) - }, -}) - -function Documentation() { - const data = useFumadocsLoader(Route.useLoaderData()) - return ( - - {clientLoader.useContent(data.path)} - - ) -} diff --git a/packages/www/src/routes/index.tsx b/packages/www/src/routes/index.tsx deleted file mode 100644 index d674a30d723c..000000000000 --- a/packages/www/src/routes/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router" - -export const Route = createFileRoute("/")({ - component: Home, -}) - -function Home() { - return ( -
    -

    OpenCode

    -

    The open source AI coding agent

    -

    The new opencode.ai SSR site is running. The V2 documentation is available now.

    - - Read the docs - -
    - ) -} diff --git a/packages/www/src/styles.css b/packages/www/src/styles.css deleted file mode 100644 index 9fab42c420d7..000000000000 --- a/packages/www/src/styles.css +++ /dev/null @@ -1,85 +0,0 @@ -@import "tailwindcss"; -@import "fumadocs-ui/css/neutral.css"; -@import "fumadocs-ui/css/preset.css"; - -:root { - --site-background: #f7f7f5; - --site-foreground: #171717; - --site-border: #d8d8d2; - --site-accent: #3b7dd8; - --fd-layout-width: 90rem; -} - -* { - box-sizing: border-box; -} - -html, -body { - min-height: 100%; -} - -body { - margin: 0; -} - -/* Fumadocs 16.11 stretches top layout tabs across the main grid row. */ -#nd-docs-layout > div:has(> a[href="/docs/build"], > a[href="/docs/api"]) { - align-self: start; - height: 3rem; -} - -.home { - min-height: 100vh; - display: grid; - align-content: center; - justify-items: start; - gap: 1.5rem; - padding: clamp(1.5rem, 7vw, 7rem); - color: var(--site-foreground); - background: var(--site-background); -} - -.home-kicker, -.home-copy, -.home h1 { - margin: 0; -} - -.home-kicker { - font: - 600 0.75rem/1.2 ui-monospace, - monospace; - letter-spacing: 0.12em; - text-transform: uppercase; -} - -.home h1 { - max-width: 13ch; - font: - 500 clamp(3rem, 9vw, 8rem) / 0.92 ui-monospace, - monospace; - letter-spacing: -0.07em; -} - -.home-copy { - max-width: 42rem; - font: - 400 clamp(1rem, 2vw, 1.25rem) / 1.5 ui-monospace, - monospace; -} - -.home-link { - display: inline-block; - padding: 0.8rem 1rem; - color: var(--site-background); - background: var(--site-foreground); - font: - 500 0.875rem/1 ui-monospace, - monospace; - text-decoration: none; -} - -.home-link:hover { - background: var(--site-accent); -} diff --git a/packages/www/tsconfig.json b/packages/www/tsconfig.json index fa688da72b11..0a92f5be4567 100644 --- a/packages/www/tsconfig.json +++ b/packages/www/tsconfig.json @@ -1,24 +1,7 @@ { - "include": ["src/**/*.ts", "src/**/*.tsx", "source.config.ts", "vite.config.ts", ".source/**/*.ts"], + "extends": "astro/tsconfigs/strict", + "include": [".blume/.astro/types.d.ts", ".blume/src/env.d.ts", "**/*"], "compilerOptions": { - "target": "ES2022", - "jsx": "react-jsx", - "module": "ESNext", - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - "skipLibCheck": true, - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true, - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "types": ["vite/client"], - "paths": { - "@/*": ["./src/*"], - "collections/*": ["./.source/*"] - } + "types": ["bun"] } } diff --git a/packages/www/vite.config.ts b/packages/www/vite.config.ts deleted file mode 100644 index 1fdc270427e5..000000000000 --- a/packages/www/vite.config.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { cloudflare } from "@cloudflare/vite-plugin" -import tailwindcss from "@tailwindcss/vite" -import { tanstackStart } from "@tanstack/react-start/plugin/vite" -import viteReact from "@vitejs/plugin-react" -import mdx from "fumadocs-mdx/vite" -import { defineConfig } from "vite" - -export default defineConfig({ - resolve: { - tsconfigPaths: true, - }, - plugins: [ - cloudflare({ - viteEnvironment: { name: "ssr" }, - configPath: process.env.SST_WRANGLER_PATH, - }), - tailwindcss(), - mdx(), - tanstackStart(), - viteReact(), - ], -}) diff --git a/packages/www/wrangler.jsonc b/packages/www/wrangler.jsonc index b1b96c993419..ff8e3992e673 100644 --- a/packages/www/wrangler.jsonc +++ b/packages/www/wrangler.jsonc @@ -1,7 +1,41 @@ { "$schema": "node_modules/wrangler/config-schema.json", "name": "opencode-www", - "compatibility_date": "2026-07-12", - "compatibility_flags": ["nodejs_compat"], - "main": "@tanstack/react-start/server-entry", + "compatibility_date": "2026-07-25", + "workers_dev": false, + "preview_urls": false, + "assets": { + "binding": "ASSETS", + "directory": "./dist/client", + "html_handling": "auto-trailing-slash", + "not_found_handling": "404-page" + }, + "env": { + "dev": { + "name": "opencode-www-dev", + "routes": [ + { + "pattern": "dev.opencode.ai/v2", + "zone_name": "opencode.ai" + }, + { + "pattern": "dev.opencode.ai/v2/*", + "zone_name": "opencode.ai" + } + ] + }, + "production": { + "name": "opencode-www", + "routes": [ + { + "pattern": "opencode.ai/v2", + "zone_name": "opencode.ai" + }, + { + "pattern": "opencode.ai/v2/*", + "zone_name": "opencode.ai" + } + ] + } + } } diff --git a/script/generate.ts b/script/generate.ts index dbf38f8a3c21..e51808342f82 100755 --- a/script/generate.ts +++ b/script/generate.ts @@ -6,6 +6,6 @@ await $`bun ./packages/sdk/js/script/build.ts` await $`bun dev generate > ../sdk/openapi.json`.cwd("packages/opencode") -await $`bun run generate`.cwd("packages/docs") +await $`bun run generate`.cwd("packages/www") await $`./script/format.ts` From 56a9c0150adcb39d2057bbabd31f052db0cee1f2 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 25 Jul 2026 21:15:37 -0400 Subject: [PATCH 117/150] fix(www): mark deploy script as module --- packages/www/script/prepare-cloudflare.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/www/script/prepare-cloudflare.ts b/packages/www/script/prepare-cloudflare.ts index 3e8099a73b05..164e96afd303 100644 --- a/packages/www/script/prepare-cloudflare.ts +++ b/packages/www/script/prepare-cloudflare.ts @@ -1,3 +1,5 @@ +export {} + const path = "dist/server/wrangler.json" const config = await Bun.file(path).json() From 9840f63b12fc098fa167611bbe995c74578106ea Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 25 Jul 2026 21:16:47 -0400 Subject: [PATCH 118/150] chore(www): simplify worker routes --- packages/www/wrangler.jsonc | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/www/wrangler.jsonc b/packages/www/wrangler.jsonc index ff8e3992e673..24624114e9ea 100644 --- a/packages/www/wrangler.jsonc +++ b/packages/www/wrangler.jsonc @@ -15,11 +15,7 @@ "name": "opencode-www-dev", "routes": [ { - "pattern": "dev.opencode.ai/v2", - "zone_name": "opencode.ai" - }, - { - "pattern": "dev.opencode.ai/v2/*", + "pattern": "dev.opencode.ai/v2*", "zone_name": "opencode.ai" } ] @@ -28,11 +24,7 @@ "name": "opencode-www", "routes": [ { - "pattern": "opencode.ai/v2", - "zone_name": "opencode.ai" - }, - { - "pattern": "opencode.ai/v2/*", + "pattern": "opencode.ai/v2*", "zone_name": "opencode.ai" } ] From c7871e14d4267f3c747e0dfc868d90fb4f652b9c Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 25 Jul 2026 21:21:57 -0400 Subject: [PATCH 119/150] fix(www): remove deployment environment gate --- .github/workflows/deploy-www.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/deploy-www.yml b/.github/workflows/deploy-www.yml index e636cbf970b0..c224f8112928 100644 --- a/.github/workflows/deploy-www.yml +++ b/.github/workflows/deploy-www.yml @@ -18,7 +18,6 @@ jobs: deploy: if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'v2') runs-on: ubuntu-latest - environment: ${{ github.ref_name == 'v2' && 'production' || 'dev' }} steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 From 2ddc91a0e8d3550bb3ab134c724391b44ee54943 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 25 Jul 2026 23:42:53 -0400 Subject: [PATCH 120/150] fix(tui): show shell working directory in prompt --- packages/tui/src/mini/tool.ts | 8 ++-- packages/tui/src/routes/session/index.tsx | 8 +++- packages/tui/test/mini/entry.body.test.ts | 50 +++++++++++++++++++++++ 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/packages/tui/src/mini/tool.ts b/packages/tui/src/mini/tool.ts index 010ab3b540d6..f4fc0a3975d6 100644 --- a/packages/tui/src/mini/tool.ts +++ b/packages/tui/src/mini/tool.ts @@ -644,17 +644,17 @@ function snapQuestion(p: ToolProps): ToolSnapshot { function scrollBashStart(p: ToolProps): string { const cmd = p.input.command ?? "" const wd = p.input.workdir ?? "" - const formatted = wd && wd !== "." ? displayPath(p, wd) : "" + const formatted = wd && wd !== "." ? displayPath(p, wd, { home: true }) : "" const dir = formatted === "." ? "" : formatted if (cmd && !dir) { return `$ ${cmd}` } if (!cmd) { - return dir ? `# Running in ${dir}` : "" + return dir ? `${dir}$` : "" } - return `# Running in ${dir}\n$ ${cmd}` + return `${dir}$ ${cmd}` } function scrollBashProgress(p: ToolProps): string { @@ -670,7 +670,7 @@ function scrollBashProgress(p: ToolProps): string { } const wdRaw = (p.input.workdir ?? "").trim() - const wd = wdRaw ? displayPath(p, wdRaw) : "" + const wd = wdRaw ? displayPath(p, wdRaw, { home: true }) : "" const lines = out.split("\n") const first = (lines[0] || "").trim() const second = (lines[1] || "").trim() diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 46428a1a9349..4b1717b4886b 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -2562,6 +2562,7 @@ function Shell(props: ToolProps) { const ctx = use() const client = useClient() const data = useData() + const pathFormatter = usePathFormatter() const permission = createMemo(() => { const request = data.session.permission.list(ctx.sessionID)?.[0] return request?.source?.type === "tool" && request.source.callID === props.part.id @@ -2575,6 +2576,7 @@ function Shell(props: ToolProps) { }) const isRunning = createMemo(() => props.part.state.status === "running" || backgroundRunning()) const command = createMemo(() => stringValue(props.input.command)) + const workdir = createMemo(() => pathFormatter.format(stringValue(props.input.workdir))) const [expanded, setExpanded] = createSignal(false) const [backgroundOutput, setBackgroundOutput] = createSignal("") const [outputTruncated, setOutputTruncated] = createSignal(false) @@ -2648,7 +2650,11 @@ function Shell(props: ToolProps) { }) const maxLines = 10 const maxChars = createMemo(() => maxLines * Math.max(20, ctx.width - 6)) - const input = createMemo(() => (command() ? `${isRunning() ? "" : "$ "}${command()}` : "")) + const input = createMemo(() => { + if (!command()) return "" + const prompt = workdir() && workdir() !== "." ? `${workdir()}$ ` : isRunning() ? "" : "$ " + return `${prompt}${command()}` + }) const content = createMemo(() => [input(), output()].filter(Boolean).join("\n\n")) const collapsed = createMemo(() => collapseToolOutput(content(), maxLines, maxChars())) const limited = createMemo(() => { diff --git a/packages/tui/test/mini/entry.body.test.ts b/packages/tui/test/mini/entry.body.test.ts index 717466de3c6e..6151b719fb6f 100644 --- a/packages/tui/test/mini/entry.body.test.ts +++ b/packages/tui/test/mini/entry.body.test.ts @@ -1,4 +1,6 @@ import { describe, expect, test } from "bun:test" +import os from "os" +import path from "path" import type { SessionMessageAssistantTool } from "@opencode-ai/client/promise" import { entryBody, entryCanStream, entryDone } from "../../src/mini/entry.body" import type { StreamCommit, ToolSnapshot } from "../../src/mini/types" @@ -11,6 +13,7 @@ function commit(input: Partial & Pick { }) }) + test("renders a shell workdir before the prompt", () => { + expect( + entryBody( + toolCommit({ + tool: "shell", + directory: "/work/project", + phase: "start", + toolState: "running", + state: { + status: "running", + input: { + command: "ls", + workdir: "packages/foo", + }, + metadata: {}, + }, + }), + ), + ).toEqual({ + type: "text", + content: "packages/foo$ ls", + }) + + expect( + entryBody( + toolCommit({ + tool: "shell", + directory: path.join(os.homedir(), "project"), + phase: "start", + toolState: "running", + state: { + status: "running", + input: { + command: "pwd", + workdir: path.join(os.homedir(), "outside", "folder"), + }, + metadata: {}, + }, + }), + ), + ).toEqual({ + type: "text", + content: "~/outside/folder$ pwd", + }) + }) + test("renders direct shell commits without a synthetic shell header", () => { expect( entryBody( From efb629a33ac495d6ca9b71ae45be283e10cde9ba Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Sun, 26 Jul 2026 09:25:05 +0530 Subject: [PATCH 121/150] feat(core): add pluggable web search (#35558) Co-authored-by: Dax Raad --- AGENTS.md | 1 - packages/client/src/effect/api.ts | 4 +- packages/client/src/effect/api/api.ts | 20 ++ .../client/src/effect/generated/client.ts | 23 ++ packages/client/src/effect/index.ts | 2 + packages/client/src/promise/api.ts | 1 + .../client/src/promise/generated/client.ts | 31 ++ .../client/src/promise/generated/types.ts | 38 ++ packages/client/src/promise/index.ts | 1 + packages/client/test/promise.test.ts | 33 ++ packages/core/src/config.ts | 4 + packages/core/src/config/plugin/websearch.ts | 27 ++ packages/core/src/config/websearch.ts | 8 + packages/core/src/location-services.ts | 2 + packages/core/src/plugin.ts | 2 + packages/core/src/plugin/host.ts | 153 ++++---- packages/core/src/plugin/internal.ts | 12 +- packages/core/src/plugin/promise.ts | 30 ++ packages/core/src/plugin/supervisor.ts | 6 +- packages/core/src/plugin/websearch/exa.ts | 82 +++++ packages/core/src/plugin/websearch/index.ts | 4 + packages/core/src/plugin/websearch/mcp.ts | 68 ++++ .../core/src/plugin/websearch/parallel.ts | 103 ++++++ packages/core/src/tool/websearch.ts | 284 ++++----------- packages/core/src/websearch.ts | 144 ++++++++ packages/core/test/config/config.test.ts | 57 ++- packages/core/test/lib/tool.ts | 12 +- packages/core/test/plugin/fixture.ts | 6 + packages/core/test/plugin/host.ts | 59 ++- packages/core/test/plugin/promise.test.ts | 33 ++ .../core/test/plugin/websearch-fixture.ts | 50 +++ packages/core/test/plugin/websearch.test.ts | 170 +++++++++ packages/core/test/tool-websearch.test.ts | 335 ++++++------------ packages/core/test/websearch.test.ts | 129 +++++++ packages/plugin/src/v2/effect/index.ts | 1 + packages/plugin/src/v2/effect/plugin.ts | 2 + packages/plugin/src/v2/effect/websearch.ts | 23 ++ packages/plugin/src/v2/promise/index.ts | 1 + packages/plugin/src/v2/promise/integration.ts | 24 +- packages/plugin/src/v2/promise/plugin.ts | 2 + packages/plugin/src/v2/promise/websearch.ts | 25 ++ .../plugin/test/contract-identity.test.ts | 5 +- packages/protocol/src/api.ts | 3 + packages/protocol/src/client.ts | 1 + packages/protocol/src/groups/websearch.ts | 46 +++ packages/schema/src/event-manifest.ts | 2 + packages/schema/src/index.ts | 1 + packages/schema/src/websearch.ts | 42 +++ packages/sdk-next/src/index.ts | 1 + .../sdk-next/test/contract-identity.test.ts | 3 + packages/sdk-next/test/embedded.test.ts | 32 ++ packages/server/src/handlers.ts | 2 + packages/server/src/handlers/websearch.ts | 68 ++++ .../tui/src/component/dialog-integration.tsx | 42 ++- packages/tui/src/context/data.tsx | 20 ++ packages/tui/test/cli/tui/data.test.tsx | 4 + packages/tui/test/fixture/tui-client.ts | 3 + 57 files changed, 1700 insertions(+), 587 deletions(-) create mode 100644 packages/core/src/config/plugin/websearch.ts create mode 100644 packages/core/src/config/websearch.ts create mode 100644 packages/core/src/plugin/websearch/exa.ts create mode 100644 packages/core/src/plugin/websearch/index.ts create mode 100644 packages/core/src/plugin/websearch/mcp.ts create mode 100644 packages/core/src/plugin/websearch/parallel.ts create mode 100644 packages/core/src/websearch.ts create mode 100644 packages/core/test/plugin/websearch-fixture.ts create mode 100644 packages/core/test/plugin/websearch.test.ts create mode 100644 packages/core/test/websearch.test.ts create mode 100644 packages/plugin/src/v2/effect/websearch.ts create mode 100644 packages/plugin/src/v2/promise/websearch.ts create mode 100644 packages/protocol/src/groups/websearch.ts create mode 100644 packages/schema/src/websearch.ts create mode 100644 packages/server/src/handlers/websearch.ts diff --git a/AGENTS.md b/AGENTS.md index 14f9f27f1452..027d22647eee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,3 @@ -- To regenerate the legacy JavaScript SDK, run `./packages/sdk/js/script/build.ts`. - After changing the public Protocol or Server `HttpApi`, run `bun run generate` from `packages/client`. Do not edit `src/generated` or `src/generated-effect` directly. - Keep runtime dependencies directed from Schema to Core and Protocol, then from Core and Protocol to Server. Client runtime code may depend on Schema and Protocol but never Core or Server; `sdk-next` composes Client, Core, and Server. - Do not modify `packages/opencode` unless the user explicitly asks for V1 work. `packages/opencode` is the V1 implementation and is present for reference only. New implementation changes should land in the V2 package set: `packages/core`, `packages/cli`, `packages/server`, `packages/protocol`, `packages/schema`, and related generated client surfaces when required. diff --git a/packages/client/src/effect/api.ts b/packages/client/src/effect/api.ts index e7cb2012f588..d0b217034545 100644 --- a/packages/client/src/effect/api.ts +++ b/packages/client/src/effect/api.ts @@ -1,7 +1,9 @@ -import type { ModelApi, ProviderApi } from "./api/api.js" +import type { ModelApi, ProviderApi, WebsearchApi } from "./api/api.js" export type * from "./api/api.js" +export type WebSearchApi = WebsearchApi + export interface CatalogApi { readonly provider: ProviderApi readonly model: ModelApi diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index 5a001c2630e5..3c229bc61519 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -1042,6 +1042,25 @@ export interface DebugApi { readonly location: { readonly list: DebugLocationListOperation; readonly evict: DebugLocationEvictOperation } } +type Endpoint27_0Request = Parameters[0] +export type Endpoint27_0Input = { readonly location?: Endpoint27_0Request["query"]["location"] } +export type Endpoint27_0Output = EffectValue> +export type WebsearchProvidersOperation = (input?: Endpoint27_0Input) => Effect.Effect + +type Endpoint27_1Request = Parameters[0] +export type Endpoint27_1Input = { + readonly location?: Endpoint27_1Request["query"]["location"] + readonly query: Endpoint27_1Request["payload"]["query"] + readonly providerID?: Endpoint27_1Request["payload"]["providerID"] +} +export type Endpoint27_1Output = EffectValue> +export type WebsearchQueryOperation = (input: Endpoint27_1Input) => Effect.Effect + +export interface WebsearchApi { + readonly providers: WebsearchProvidersOperation + readonly query: WebsearchQueryOperation +} + export interface AppApi { readonly health: HealthApi readonly server: ServerApi @@ -1070,4 +1089,5 @@ export interface AppApi { readonly projectCopy: ProjectCopyApi readonly vcs: VcsApi readonly debug: DebugApi + readonly websearch: WebsearchApi } diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts index de18f5c05e2b..ce791ce4f178 100644 --- a/packages/client/src/effect/generated/client.ts +++ b/packages/client/src/effect/generated/client.ts @@ -1244,6 +1244,28 @@ const adaptGroup26 = (raw: RawClient["server.debug"]) => ({ location: { list: Endpoint26_0(raw), evict: Endpoint26_1(raw) }, }) +type Endpoint27_0Request = Parameters[0] +type Endpoint27_0Input = { readonly location?: Endpoint27_0Request["query"]["location"] } +const Endpoint27_0 = (raw: RawClient["server.websearch"]) => (input?: Endpoint27_0Input) => + raw["websearch.providers"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint27_1Request = Parameters[0] +type Endpoint27_1Input = { + readonly location?: Endpoint27_1Request["query"]["location"] + readonly query: Endpoint27_1Request["payload"]["query"] + readonly providerID?: Endpoint27_1Request["payload"]["providerID"] +} +const Endpoint27_1 = (raw: RawClient["server.websearch"]) => (input: Endpoint27_1Input) => + raw["websearch.query"]({ + query: { location: input["location"] }, + payload: { query: input["query"], providerID: input["providerID"] }, + }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup27 = (raw: RawClient["server.websearch"]) => ({ + providers: Endpoint27_0(raw), + query: Endpoint27_1(raw), +}) + const adaptClient = (raw: RawClient) => ({ health: adaptGroup0(raw["server.health"]), server: adaptGroup1(raw["server.server"]), @@ -1272,6 +1294,7 @@ const adaptClient = (raw: RawClient) => ({ projectCopy: adaptGroup24(raw["server.projectCopy"]), vcs: adaptGroup25(raw["server.vcs"]), debug: adaptGroup26(raw["server.debug"]), + websearch: adaptGroup27(raw["server.websearch"]), }) export const make = (options?: { readonly baseUrl?: URL | string }) => diff --git a/packages/client/src/effect/index.ts b/packages/client/src/effect/index.ts index 27424aad823c..500352062f2e 100644 --- a/packages/client/src/effect/index.ts +++ b/packages/client/src/effect/index.ts @@ -14,6 +14,7 @@ export type { PluginApi, ProviderApi, ReferenceApi, + WebSearchApi, SessionApi, SkillApi, } from "./api.js" @@ -35,6 +36,7 @@ export { Provider } from "@opencode-ai/schema/provider" export { Pty } from "@opencode-ai/schema/pty" export { Question } from "@opencode-ai/schema/question" export { Reference } from "@opencode-ai/schema/reference" +export { WebSearch } from "@opencode-ai/schema/websearch" export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema" export { Session } from "@opencode-ai/schema/session" export { SessionPending } from "@opencode-ai/schema/session-pending" diff --git a/packages/client/src/promise/api.ts b/packages/client/src/promise/api.ts index b4bc8635b2c5..9614fef04c28 100644 --- a/packages/client/src/promise/api.ts +++ b/packages/client/src/promise/api.ts @@ -8,6 +8,7 @@ export type ModelApi = Client["model"] export type PluginApi = Client["plugin"] export type ProviderApi = Client["provider"] export type ReferenceApi = Client["reference"] +export type WebSearchApi = Client["websearch"] export type SessionApi = Client["session"] export type SkillApi = Client["skill"] diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts index 97c84a5d7aa4..b5fd3fd6c3f8 100644 --- a/packages/client/src/promise/generated/client.ts +++ b/packages/client/src/promise/generated/client.ts @@ -207,6 +207,10 @@ import type { DebugLocationListOutput, DebugLocationEvictInput, DebugLocationEvictOutput, + WebsearchProvidersInput, + WebsearchProvidersOutput, + WebsearchQueryInput, + WebsearchQueryOutput, } from "./types" import { ClientError } from "./client-error" @@ -1735,6 +1739,33 @@ export function make(options: ClientOptions) { ), }, }, + websearch: { + providers: (input?: WebsearchProvidersInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/websearch/provider`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [503, 401, 400], + empty: false, + }, + requestOptions, + ), + query: (input: WebsearchQueryInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/websearch`, + query: { location: input["location"] }, + body: { query: input["query"], providerID: input["providerID"] }, + successStatus: 200, + declaredStatuses: [400, 503, 401], + empty: false, + }, + requestOptions, + ), + }, } } diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index ef614aa1bbde..19ae6c3f5dd8 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -539,6 +539,10 @@ export type VcsFileStatus = { status: "added" | "deleted" | "modified" } +export type WebSearchProvider = { id: string; name: string } + +export type WebSearchResult = { url: string; title?: string; content?: string; time: { published?: number } } + export type SessionMessageModelSelected = { id: string metadata?: { [x: string]: JsonValue } @@ -1058,6 +1062,15 @@ export type FormCancelled = { data: { id: string; sessionID: string } } +export type WebsearchUpdated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "websearch.updated" + location?: LocationRef + data: {} +} + export type SessionIdle = { id: string created: number @@ -2355,6 +2368,7 @@ export type V2Event = | FormCreated | FormReplied | FormCancelled + | WebsearchUpdated | SessionStatus2 | SessionIdle | TuiPromptAppend @@ -4958,3 +4972,27 @@ export type DebugLocationEvictInput = { } export type DebugLocationEvictOutput = void + +export type WebsearchProvidersInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type WebsearchProvidersOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Array +} + +export type WebsearchQueryInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly query: { readonly query: string; readonly providerID?: string }["query"] + readonly providerID?: { readonly query: string; readonly providerID?: string }["providerID"] +} + +export type WebsearchQueryOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: { providerID: string; results: Array } +} diff --git a/packages/client/src/promise/index.ts b/packages/client/src/promise/index.ts index fd889c64e55e..dd64831e013d 100644 --- a/packages/client/src/promise/index.ts +++ b/packages/client/src/promise/index.ts @@ -9,6 +9,7 @@ export type { PluginApi, ProviderApi, ReferenceApi, + WebSearchApi, SessionApi, SkillApi, } from "./api.js" diff --git a/packages/client/test/promise.test.ts b/packages/client/test/promise.test.ts index d3c966ef9be9..c3c0512f1746 100644 --- a/packages/client/test/promise.test.ts +++ b/packages/client/test/promise.test.ts @@ -32,6 +32,7 @@ test("exposes every standard HTTP API group", () => { "projectCopy", "vcs", "debug", + "websearch", ]) expect(Object.keys(client.debug)).toEqual(["location"]) expect(Object.keys(client.debug.location)).toEqual(["list", "evict"]) @@ -41,6 +42,7 @@ test("exposes every standard HTTP API group", () => { expect(Object.keys(client.integration.connect)).toEqual(["key"]) expect(Object.keys(client.integration.oauth)).toEqual(["connect", "status", "complete", "cancel"]) expect(Object.keys(client.integration.command)).toEqual(["connect", "status", "cancel"]) + expect(Object.keys(client.websearch)).toEqual(["providers", "query"]) expect(Object.keys(client.file)).toEqual(["read", "list", "find"]) expect(Object.keys(client.vcs)).toEqual(["status", "diff"]) expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"]) @@ -48,6 +50,37 @@ test("exposes every standard HTTP API group", () => { expect(Object.keys(client.project)).toEqual(["list", "current", "directories"]) }) +test("websearch.query uses the public HTTP contract", async () => { + let request: Request | undefined + const client = OpenCode.make({ + baseUrl: "http://localhost:3000", + fetch: async (input, init) => { + request = input instanceof Request ? input : new Request(input, init) + return Response.json({ + location: { directory: "/tmp/project", project: { id: "proj_test", directory: "/tmp/project" } }, + data: { + providerID: "exa", + results: [{ url: "https://example.com", title: "Result", content: "result", time: {} }], + }, + }) + }, + }) + + const result = await client.websearch.query({ + query: "opencode", + providerID: "exa", + location: { directory: "/tmp/project" }, + }) + + expect(result.data).toEqual({ + providerID: "exa", + results: [{ url: "https://example.com", title: "Result", content: "result", time: {} }], + }) + expect(request?.method).toBe("POST") + expect(request?.url).toBe("http://localhost:3000/api/websearch?location%5Bdirectory%5D=%2Ftmp%2Fproject") + expect(await request?.json()).toEqual({ query: "opencode", providerID: "exa" }) +}) + test("server.get uses the public HTTP contract", async () => { let request: Request | undefined const client = OpenCode.make({ diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 2f338b6b35ee..99cdff45e13a 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -27,6 +27,7 @@ import { ConfigModel } from "./config/model" import { ConfigPlugin } from "./config/plugin" import { ConfigProvider } from "./config/provider" import { ConfigReference } from "./config/reference" +import { ConfigWebSearch } from "./config/websearch" import { ConfigToolOutput } from "./config/tool-output" import { ConfigVariable } from "./config/variable" import { ConfigWatcher } from "./config/watcher" @@ -108,6 +109,9 @@ export class Info extends Schema.Class("Config.Info")({ references: ConfigReference.Info.pipe(Schema.optional).annotate({ description: "Named local directories or Git repositories available as external context", }), + websearch: ConfigWebSearch.Info.pipe(Schema.optional).annotate({ + description: "Web search provider selection", + }), plugins: ConfigPlugin.Plugins.pipe(Schema.optional).annotate({ description: "Ordered plugin enablement directives and external package declarations", }), diff --git a/packages/core/src/config/plugin/websearch.ts b/packages/core/src/config/plugin/websearch.ts new file mode 100644 index 000000000000..1a063549facd --- /dev/null +++ b/packages/core/src/config/plugin/websearch.ts @@ -0,0 +1,27 @@ +export * as ConfigWebSearchPlugin from "./websearch" + +import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { Effect, Stream } from "effect" +import { Config } from "../../config" + +export const Plugin = define({ + id: "opencode.config.websearch", + effect: Effect.fn(function* (ctx) { + const config = yield* Config.Service + const loaded = { entries: yield* config.entries() } + yield* ctx.websearch.transform((websearch) => { + const providerID = Config.latest(loaded.entries, "websearch")?.provider + if (providerID) websearch.default.set(providerID) + }) + yield* ctx.event.subscribe().pipe( + Stream.filter((event) => event.type === "config.updated"), + Stream.runForEach(() => + config.entries().pipe( + Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))), + Effect.andThen(ctx.websearch.reload()), + ), + ), + Effect.forkScoped({ startImmediately: true }), + ) + }), +}) diff --git a/packages/core/src/config/websearch.ts b/packages/core/src/config/websearch.ts new file mode 100644 index 000000000000..4db2a50fc648 --- /dev/null +++ b/packages/core/src/config/websearch.ts @@ -0,0 +1,8 @@ +export * as ConfigWebSearch from "./websearch" + +import { WebSearch } from "@opencode-ai/schema/websearch" +import { Schema } from "effect" + +export class Info extends Schema.Class("ConfigWebSearch.Info")({ + provider: WebSearch.ID, +}) {} diff --git a/packages/core/src/location-services.ts b/packages/core/src/location-services.ts index 5208f6a7cc51..3cce750142c5 100644 --- a/packages/core/src/location-services.ts +++ b/packages/core/src/location-services.ts @@ -29,6 +29,7 @@ import { Pty } from "./pty" import { QuestionV2 } from "./question" import { Shell } from "./shell" import { Reference } from "./reference" +import { WebSearch } from "./websearch" import { ReferenceInstructions } from "./reference/instructions" import { SessionRunnerLLM } from "./session/runner/llm" import { SessionRunnerModel } from "./session/runner/model" @@ -56,6 +57,7 @@ const locationServiceNodes = [ AgentV2.node, CommandV2.node, Reference.node, + WebSearch.node, Integration.node, Catalog.node, ModelResolver.node, diff --git a/packages/core/src/plugin.ts b/packages/core/src/plugin.ts index 6007b67abb6b..bb093a7b97a9 100644 --- a/packages/core/src/plugin.ts +++ b/packages/core/src/plugin.ts @@ -14,6 +14,7 @@ import { Integration } from "./integration" import { Location } from "./location" import { PluginHost } from "./plugin/host" import { PluginRuntime } from "./plugin/runtime" +import { WebSearch } from "./websearch" import { Reference } from "./reference" import { SkillV2 } from "./skill" import { State } from "./state" @@ -158,5 +159,6 @@ export const node = makeLocationNode({ ToolHooks.node, PluginHooks.node, PluginRuntime.node, + WebSearch.node, ], }) diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index 640fdea14825..730ddafecc55 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -1,6 +1,8 @@ export * as PluginHost from "./host" -import type { Plugin } from "@opencode-ai/plugin/v2/effect" +import { Plugin } from "@opencode-ai/plugin/v2/effect" +import type { IntegrationMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration" +import type { CredentialOAuth } from "@opencode-ai/sdk/v2/types" import { EventManifest } from "@opencode-ai/schema/event-manifest" import { App } from "../app" import { Effect, Schema, Stream } from "effect" @@ -23,6 +25,7 @@ import { Tool } from "../tool/tool" import { Tools } from "../tool/tools" import { ToolHooks } from "../tool/hooks" import { WorkspaceV2 } from "../workspace" +import { WebSearch } from "../websearch" import { PluginHooks } from "./hooks" const mutable = (value: T) => value as DeepMutable @@ -38,6 +41,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int const reference = yield* Reference.Service const skill = yield* SkillV2.Service const tools = yield* Tools.Service + const websearch = yield* WebSearch.Service const toolHooks = yield* ToolHooks.Service const hooks = yield* PluginHooks.Service const runtime = yield* PluginRuntime.Service @@ -247,79 +251,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int remove: (id) => draft.remove(Integration.ID.make(id)), method: { list: (id) => mutable(draft.method.list(Integration.ID.make(id))), - update: (input) => { - if ("authorize" in input) { - const methodID = Integration.MethodID.make(input.method.id) - const refresh = input.refresh - draft.method.update({ - integrationID: Integration.ID.make(input.integrationID), - method: { ...input.method, id: methodID }, - authorize: (inputs) => - input.authorize(inputs).pipe( - Effect.map((authorization) => { - if (authorization.mode === "auto") { - return { - ...authorization, - callback: authorization.callback.pipe( - Effect.map((credential) => - Credential.OAuth.make({ - ...credential, - methodID: Integration.MethodID.make(credential.methodID), - }), - ), - ), - } - } - return { - ...authorization, - callback: (code: string) => - authorization.callback(code).pipe( - Effect.map((credential) => - Credential.OAuth.make({ - ...credential, - methodID: Integration.MethodID.make(credential.methodID), - }), - ), - ), - } - }), - ), - ...(refresh - ? { - refresh: (value: Credential.OAuth) => - refresh(value).pipe( - Effect.map((next) => - Credential.OAuth.make({ - ...next, - methodID: Integration.MethodID.make(next.methodID), - }), - ), - ), - } - : {}), - ...(input.label ? { label: input.label } : {}), - }) - return - } - if (input.method.type === "env") { - draft.method.update({ - integrationID: Integration.ID.make(input.integrationID), - method: { type: "env", names: input.method.names }, - }) - return - } - if (input.method.type === "command") { - draft.method.update({ - integrationID: Integration.ID.make(input.integrationID), - method: Schema.decodeUnknownSync(Integration.CommandMethod)(input.method), - }) - return - } - draft.method.update({ - integrationID: Integration.ID.make(input.integrationID), - method: { type: "key", label: input.method.label }, - }) - }, + update: (input) => draft.method.update(methodImplementation(input)), remove: (id, method) => draft.method.remove(Integration.ID.make(id), Schema.decodeUnknownSync(Integration.Method)(method)), }, @@ -430,6 +362,32 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int }) }, }, + websearch: { + providers: () => response(websearch.providers()), + query: (input) => + response( + websearch.query({ + query: input.query, + providerID: input.providerID === undefined ? undefined : WebSearch.ID.make(input.providerID), + }), + ), + reload: websearch.reload, + transform: (callback) => + websearch.transform((draft) => { + callback({ + add: (definition) => + draft.add({ + id: WebSearch.ID.make(definition.id), + name: definition.name, + execute: definition.execute, + }), + default: { + get: draft.default.get, + set: (providerID) => draft.default.set(WebSearch.ID.make(providerID)), + }, + }) + }), + }, session: { hook: (name, callback) => hooks.register("session", name, callback), create: (input) => @@ -449,3 +407,50 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int }, } satisfies Plugin.Context }) + +function methodImplementation(input: IntegrationMethodRegistration): Integration.Implementation { + if ("authorize" in input) { + const refresh = input.refresh + return { + integrationID: Integration.ID.make(input.integrationID), + method: { ...input.method, id: Integration.MethodID.make(input.method.id) }, + authorize: (inputs) => + input.authorize(inputs).pipe( + Effect.map((authorization) => { + if (authorization.mode === "auto") { + return { + ...authorization, + callback: authorization.callback.pipe(Effect.map(credential)), + } + } + return { + ...authorization, + callback: (code: string) => authorization.callback(code).pipe(Effect.map(credential)), + } + }), + ), + ...(refresh ? { refresh: (value: Credential.OAuth) => refresh(value).pipe(Effect.map(credential)) } : {}), + ...(input.label ? { label: input.label } : {}), + } + } + if (input.method.type === "env") { + return { + integrationID: Integration.ID.make(input.integrationID), + method: { type: "env", names: input.method.names }, + } + } + if (input.method.type === "command") { + return { + integrationID: Integration.ID.make(input.integrationID), + method: Schema.decodeUnknownSync(Integration.CommandMethod)(input.method), + } + } + return { + integrationID: Integration.ID.make(input.integrationID), + method: { type: "key", label: input.method.label }, + } +} + +function credential(value: CredentialOAuth) { + return Credential.OAuth.make({ ...value, methodID: Integration.MethodID.make(value.methodID) }) +} diff --git a/packages/core/src/plugin/internal.ts b/packages/core/src/plugin/internal.ts index 5457a13d3da3..815dd4339d10 100644 --- a/packages/core/src/plugin/internal.ts +++ b/packages/core/src/plugin/internal.ts @@ -13,6 +13,7 @@ import { ConfigProviderPlugin } from "../config/plugin/provider" import { ConfigPolicyPlugin } from "../config/plugin/policy" import { ConfigReferencePlugin } from "../config/plugin/reference" import { ConfigSkillPlugin } from "../config/plugin/skill" +import { ConfigWebSearchPlugin } from "../config/plugin/websearch" import { EventV2 } from "../event" import { FileMutation } from "../file-mutation" import { Form } from "../form" @@ -21,12 +22,14 @@ import { FSUtil } from "@opencode-ai/util/fs-util" import { Global } from "@opencode-ai/util/global" import { Image } from "../image" import { Integration } from "../integration" +import { KV } from "../kv" import { Location } from "../location" import { LocationMutation } from "../location-mutation" import { ModelsDev } from "../models-dev" import { Npm } from "@opencode-ai/util/npm" import { PermissionV2 } from "../permission" import { Reference } from "../reference" +import { WebSearch } from "../websearch" import { Ripgrep } from "../ripgrep" import { SessionInstructions } from "../session/instructions" import { Shell } from "../shell" @@ -50,6 +53,7 @@ import { AgentPlugin } from "./agent" import { CommandPlugin } from "./command" import { ModelsDevPlugin } from "./models-dev" import { ProviderPlugins } from "./provider" +import { WebSearchPlugins } from "./websearch" import { PluginRuntime } from "./runtime" import { SkillPlugin } from "./skill" import { SystemPromptPlugin } from "./system-prompt" @@ -70,6 +74,7 @@ const services = Effect.fn("PluginInternal.services")(function* () { const http = yield* HttpClient.HttpClient const image = yield* Image.Service const integration = yield* Integration.Service + const kv = yield* KV.Service const location = yield* Location.Service const locationMutation = yield* LocationMutation.Service const models = yield* ModelsDev.Service @@ -79,12 +84,12 @@ const services = Effect.fn("PluginInternal.services")(function* () { const form = yield* Form.Service const read = yield* ReadToolFileSystem.Service const reference = yield* Reference.Service + const websearch = yield* WebSearch.Service const ripgrep = yield* Ripgrep.Service const instructions = yield* SessionInstructions.Service const shell = yield* Shell.Service const skill = yield* SkillV2.Service const tools = yield* Tools.Service - const websearch = yield* WebSearchTool.ConfigService const wellknown = yield* WellKnown.Service return Context.mergeAll( Context.make(AgentV2.Service, agent), @@ -99,6 +104,7 @@ const services = Effect.fn("PluginInternal.services")(function* () { Context.make(HttpClient.HttpClient, http), Context.make(Image.Service, image), Context.make(Integration.Service, integration), + Context.make(KV.Service, kv), Context.make(Location.Service, location), Context.make(LocationMutation.Service, locationMutation), Context.make(ModelsDev.Service, models), @@ -108,12 +114,12 @@ const services = Effect.fn("PluginInternal.services")(function* () { Context.make(Form.Service, form), Context.make(ReadToolFileSystem.Service, read), Context.make(Reference.Service, reference), + Context.make(WebSearch.Service, websearch), Context.make(Ripgrep.Service, ripgrep), Context.make(SessionInstructions.Service, instructions), Context.make(Shell.Service, shell), Context.make(SkillV2.Service, skill), Context.make(Tools.Service, tools), - Context.make(WebSearchTool.ConfigService, websearch), Context.make(WellKnown.Service, wellknown), ) }) @@ -132,6 +138,7 @@ const pre = [ ...SystemPromptPlugin.Plugins, ModelsDevPlugin, ...ProviderPlugins, + ...WebSearchPlugins, PatchTool.Plugin, EditTool.Plugin, GlobTool.Plugin, @@ -153,6 +160,7 @@ const post = [ ConfigCommandPlugin.Plugin, ConfigSkillPlugin.Plugin, ConfigProviderPlugin.Plugin, + ConfigWebSearchPlugin.Plugin, VariantPlugin.Plugin, ConfigPolicyPlugin.Plugin, ] as const satisfies readonly InternalPlugin[] diff --git a/packages/core/src/plugin/promise.ts b/packages/core/src/plugin/promise.ts index 950f6a2afcb9..a5172b0c98ac 100644 --- a/packages/core/src/plugin/promise.ts +++ b/packages/core/src/plugin/promise.ts @@ -12,6 +12,7 @@ import { AbsolutePath } from "@opencode-ai/schema/schema" import { Session } from "@opencode-ai/schema/session" import { SessionMessage } from "@opencode-ai/schema/session-message" import { Workspace } from "@opencode-ai/schema/workspace" +import { WebSearch } from "@opencode-ai/schema/websearch" import { DateTime, Effect, Scope, Stream } from "effect" import { Tool } from "../tool/tool" @@ -197,6 +198,31 @@ export function fromPromise(plugin: Plugin) { hook: (name, callback) => register(host.tool.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))), }, + websearch: { + providers: (input) => run(host.websearch.providers(input)), + query: (input) => + run( + host.websearch.query({ + ...input, + providerID: input.providerID === undefined ? undefined : WebSearch.ID.make(input.providerID), + }), + ), + reload: () => run(host.websearch.reload()), + transform: (callback) => + register( + host.websearch.transform((draft) => { + callback({ + add: (definition) => + draft.add({ + id: definition.id, + name: definition.name, + execute: (input) => attempt((signal) => definition.execute(input, { signal })), + }), + default: draft.default, + }) + }), + ), + }, session: { hook: (name, callback) => register(host.session.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))), @@ -270,6 +296,10 @@ export function fromPromise(plugin: Plugin) { }) } +function attempt(evaluate: (signal: AbortSignal) => PromiseLike) { + return Effect.tryPromise({ try: evaluate, catch: (cause) => cause }) +} + function model(input: { readonly id: string; readonly providerID: string; readonly variant?: string }) { return Model.Ref.make({ id: Model.ID.make(input.id), diff --git a/packages/core/src/plugin/supervisor.ts b/packages/core/src/plugin/supervisor.ts index b512ac35c6ca..c6b977d5a760 100644 --- a/packages/core/src/plugin/supervisor.ts +++ b/packages/core/src/plugin/supervisor.ts @@ -20,6 +20,7 @@ import { FSUtil } from "@opencode-ai/util/fs-util" import { Global } from "@opencode-ai/util/global" import { Image } from "../image" import { Integration } from "../integration" +import { KV } from "../kv" import { Location } from "../location" import { LocationMutation } from "../location-mutation" import { ModelsDev } from "../models-dev" @@ -34,7 +35,7 @@ import { Shell } from "../shell" import { SkillV2 } from "../skill" import { ReadToolFileSystem } from "../tool/read-filesystem" import { ToolRegistry } from "../tool/registry" -import { WebSearchTool } from "../tool/websearch" +import { WebSearch } from "../websearch" import { WellKnown } from "../wellknown" import { PluginInternal } from "./internal" import { PluginRuntime } from "./runtime" @@ -289,6 +290,7 @@ export const node = makeLocationNode({ httpClient, Image.node, Integration.node, + KV.node, Location.node, LocationMutation.node, ModelsDev.node, @@ -303,7 +305,7 @@ export const node = makeLocationNode({ Shell.node, SkillV2.node, ToolRegistry.toolsNode, - WebSearchTool.configNode, + WebSearch.node, WellKnown.node, ], }) diff --git a/packages/core/src/plugin/websearch/exa.ts b/packages/core/src/plugin/websearch/exa.ts new file mode 100644 index 000000000000..ba7596040782 --- /dev/null +++ b/packages/core/src/plugin/websearch/exa.ts @@ -0,0 +1,82 @@ +export * as WebSearchExa from "./exa" + +import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { Effect, Schema, Scope } from "effect" +import { HttpClient } from "effect/unstable/http" +import { WebSearchMcp } from "./mcp" + +export const endpoint = "https://mcp.exa.ai/mcp" + +const McpInput = Schema.Struct({ + query: Schema.String, + numResults: Schema.Number.pipe(Schema.optional), +}) + +const McpOutput = Schema.Struct({ + content: Schema.Array( + Schema.Struct({ + type: Schema.Literal("text"), + text: Schema.String, + _meta: Schema.Struct({ searchTime: Schema.Number }).pipe(Schema.optional), + }), + ), +}) + +export const Plugin = define({ + id: "opencode.websearch.exa", + effect: Effect.fn("WebSearchExa.Plugin")(function* (ctx) { + const http = yield* HttpClient.HttpClient + yield* ctx.integration.transform((draft) => { + draft.update("exa", (integration) => (integration.name = "Exa")) + draft.method.update({ + integrationID: "exa", + method: { type: "key", label: "API key (optional)" }, + }) + draft.method.update({ + integrationID: "exa", + method: { type: "env", names: ["EXA_API_KEY"] }, + }) + }) + yield* ctx.websearch.transform((draft) => { + draft.add({ + id: "exa", + name: "Exa", + execute: (input) => + Effect.gen(function* () { + const connection = yield* ctx.integration.connection.active("exa") + const credential = connection ? yield* ctx.integration.connection.resolve(connection) : undefined + const url = new URL(endpoint) + if (credential?.type === "key") url.searchParams.set("exaApiKey", credential.key) + const result = yield* WebSearchMcp.call( + http, + url.toString(), + "web_search_exa", + { input: McpInput, output: McpOutput }, + { query: input.query, numResults: 8 }, + ) + const content = result?.content.find((item) => item.text) + return content ? parseResults(content.text) : [] + }), + }) + }) + }), +}) + +function parseResults(text: string) { + return text.split(/\n\n---\n\n/).flatMap((block) => { + const url = block.match(/^URL:\s*(.+)$/m)?.[1]?.trim() + if (!url) return [] + const title = block.match(/^Title:\s*(.+)$/m)?.[1]?.trim() + const publishedText = block.match(/^Published:\s*(.+)$/m)?.[1]?.trim() + const published = publishedText && publishedText !== "N/A" ? Date.parse(publishedText) : undefined + const content = block.match(/^(?:Highlights|Text):\s*\n?([\s\S]*)$/m)?.[1]?.trim() + return [ + { + url, + ...(title && title !== "N/A" ? { title } : {}), + ...(content ? { content } : {}), + time: { ...(published !== undefined && Number.isFinite(published) ? { published } : {}) }, + }, + ] + }) +} diff --git a/packages/core/src/plugin/websearch/index.ts b/packages/core/src/plugin/websearch/index.ts new file mode 100644 index 000000000000..8f8b5fec6de5 --- /dev/null +++ b/packages/core/src/plugin/websearch/index.ts @@ -0,0 +1,4 @@ +import { WebSearchExa } from "./exa" +import { WebSearchParallel } from "./parallel" + +export const WebSearchPlugins = [WebSearchExa.Plugin, WebSearchParallel.Plugin] as const diff --git a/packages/core/src/plugin/websearch/mcp.ts b/packages/core/src/plugin/websearch/mcp.ts new file mode 100644 index 000000000000..b479acdcaeec --- /dev/null +++ b/packages/core/src/plugin/websearch/mcp.ts @@ -0,0 +1,68 @@ +export * as WebSearchMcp from "./mcp" + +import { Duration, Effect, Schema } from "effect" +import { HttpClient, HttpClientRequest } from "effect/unstable/http" +import { collectBoundedResponseBody } from "../../tool/http-body" + +export const MAX_RESPONSE_BYTES = 256 * 1024 + +export const parseResponse = (body: string, result: Schema.Struct) => { + const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({ result }))) + const parse = (payload: string) => { + const trimmed = payload.trim() + if (!trimmed.startsWith("{")) return Effect.succeed(undefined) + return decode(trimmed).pipe(Effect.map((response) => response.result)) + } + return Effect.gen(function* () { + const trimmed = body.trim() + const direct = trimmed ? yield* parse(trimmed) : undefined + if (direct) return direct + for (const line of body.split("\n")) { + if (!line.startsWith("data: ")) continue + const data = yield* parse(line.substring(6)) + if (data) return data + } + }) +} + +export const call = ( + http: HttpClient.HttpClient, + url: string, + tool: string, + schema: { readonly input: Schema.Struct; readonly output: Schema.Struct }, + value: Schema.Struct.Type, + headers: Record = {}, +) => + Effect.gen(function* () { + const request = yield* HttpClientRequest.post(url).pipe( + HttpClientRequest.accept("application/json, text/event-stream"), + HttpClientRequest.setHeaders(headers), + HttpClientRequest.schemaBodyJson( + Schema.Struct({ + jsonrpc: Schema.Literal("2.0"), + id: Schema.Literal(1), + method: Schema.Literal("tools/call"), + params: Schema.Struct({ name: Schema.String, arguments: schema.input }), + }), + )({ + jsonrpc: "2.0" as const, + id: 1 as const, + method: "tools/call" as const, + params: { name: tool, arguments: value }, + }), + ) + return yield* Effect.gen(function* () { + const response = yield* HttpClient.filterStatusOk(http).execute(request) + const body = yield* collectBoundedResponseBody( + response, + MAX_RESPONSE_BYTES, + () => new Error(`${tool} response exceeded ${MAX_RESPONSE_BYTES} bytes`), + ) + return yield* parseResponse(body.toString("utf8"), schema.output) + }).pipe( + Effect.timeoutOrElse({ + duration: Duration.seconds(25), + orElse: () => Effect.fail(new Error(`${tool} request timed out`)), + }), + ) + }) diff --git a/packages/core/src/plugin/websearch/parallel.ts b/packages/core/src/plugin/websearch/parallel.ts new file mode 100644 index 000000000000..e55302d8af70 --- /dev/null +++ b/packages/core/src/plugin/websearch/parallel.ts @@ -0,0 +1,103 @@ +export * as WebSearchParallel from "./parallel" + +import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { Effect, Schema, Scope } from "effect" +import { HttpClient } from "effect/unstable/http" +import { App } from "../../app" +import { WebSearchMcp } from "./mcp" + +export const endpoint = "https://search.parallel.ai/mcp" + +const McpInput = Schema.Struct({ + objective: Schema.String, + search_queries: Schema.Array(Schema.String), + model_name: Schema.String.check(Schema.isMaxLength(100)).pipe(Schema.optional), +}) + +const SearchResponse = Schema.Struct({ + search_id: Schema.String, + results: Schema.Array( + Schema.Struct({ + url: Schema.String, + title: Schema.NullOr(Schema.String).pipe(Schema.optional), + publish_date: Schema.NullOr(Schema.String).pipe(Schema.optional), + excerpts: Schema.Array(Schema.String), + }), + ), + warnings: Schema.NullOr( + Schema.Array( + Schema.Struct({ + type: Schema.Literals(["spec_validation_warning", "input_validation_warning", "warning"]), + message: Schema.String, + detail: Schema.NullOr(Schema.Record(Schema.String, Schema.Json)).pipe(Schema.optional), + }), + ), + ).pipe(Schema.optional), + usage: Schema.NullOr( + Schema.Array( + Schema.Struct({ + name: Schema.String, + count: Schema.Int, + }), + ), + ).pipe(Schema.optional), + session_id: Schema.String, +}) +const McpOutput = Schema.Struct({ + content: Schema.Array(Schema.Struct({ type: Schema.Literal("text"), text: Schema.String })), + structuredContent: SearchResponse, +}) + +export const Plugin = define({ + id: "opencode.websearch.parallel", + effect: Effect.fn("WebSearchParallel.Plugin")(function* (ctx) { + const http = yield* HttpClient.HttpClient + yield* ctx.integration.transform((draft) => { + draft.update("parallel", (integration) => (integration.name = "Parallel")) + draft.method.update({ + integrationID: "parallel", + method: { type: "key", label: "API key (optional)" }, + }) + draft.method.update({ + integrationID: "parallel", + method: { type: "env", names: ["PARALLEL_API_KEY"] }, + }) + }) + yield* ctx.websearch.transform((draft) => { + draft.add({ + id: "parallel", + name: "Parallel", + execute: (input) => + Effect.gen(function* () { + const connection = yield* ctx.integration.connection.active("parallel") + const credential = connection ? yield* ctx.integration.connection.resolve(connection) : undefined + const result = yield* WebSearchMcp.call( + http, + endpoint, + "web_search", + { input: McpInput, output: McpOutput }, + { + objective: input.query, + search_queries: [input.query], + }, + { + "User-Agent": App.useragent(ctx.app), + ...(credential?.type === "key" ? { Authorization: `Bearer ${credential.key}` } : {}), + }, + ) + return ( + result?.structuredContent.results.map((item) => { + const published = item.publish_date ? Date.parse(item.publish_date) : undefined + return { + url: item.url, + ...(item.title ? { title: item.title } : {}), + ...(item.excerpts.length ? { content: item.excerpts.join("\n\n") } : {}), + time: { ...(published !== undefined && Number.isFinite(published) ? { published } : {}) }, + } + }) ?? [] + ) + }), + }) + }) + }), +}) diff --git a/packages/core/src/tool/websearch.ts b/packages/core/src/tool/websearch.ts index bc6f1bcfa7d9..9e3a6256bd11 100644 --- a/packages/core/src/tool/websearch.ts +++ b/packages/core/src/tool/websearch.ts @@ -2,262 +2,122 @@ export * as WebSearchTool from "./websearch" import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" import { ToolFailure } from "@opencode-ai/ai" -import { Context, Duration, Effect, Layer, Schema } from "effect" -import { HttpClient, HttpClientRequest } from "effect/unstable/http" -import { makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { App } from "../app" -import { PositiveInt } from "../schema" +import { Effect, Schema } from "effect" +import { Form } from "../form" +import { KV } from "../kv" import { PermissionV2 } from "../permission" -import { Tool } from "./tool" -import { collectBoundedResponseBody } from "./http-body" -import { checksum } from "../util/encode" +import { WebSearch } from "../websearch" export const name = "websearch" export const NO_RESULTS = "No search results found. Please try a different query." -export const EXA_URL = "https://mcp.exa.ai/mcp" -export const PARALLEL_URL = "https://search.parallel.ai/mcp" -export const MAX_NUM_RESULTS = 20 -export const MAX_CONTEXT_CHARACTERS = 50_000 -export const MAX_RESPONSE_BYTES = 256 * 1024 -/** - * Provider-independent local web search retained in V2 core for launch parity. - * This invokes the legacy Exa/Parallel product backends itself. It is distinct - * from provider-hosted web search tools, which remain route-owned and execute - * at the model provider. Ownership of this compromise can be revisited later. - */ -export const description = `Search the web using the session's local web search provider. Use this for current information beyond knowledge cutoff. - -This is a provider-independent local tool backed by Exa or Parallel. Provider-hosted web search tools are separate and execute at the model provider. - -Optional controls support result count, live crawling ('fallback' or 'preferred'), search type ('auto', 'fast', or 'deep'), and maximum context characters. +export const description = `Search the web using the user's selected search integration. Use this for current information beyond knowledge cutoff. The current year is ${new Date().getFullYear()}. Use this year when searching for recent information or current events.` export const Input = Schema.Struct({ query: Schema.String.annotate({ description: "Websearch query" }), - numResults: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_NUM_RESULTS))).annotate({ - description: `Number of search results to return (default: 8, maximum: ${MAX_NUM_RESULTS})`, - }), - livecrawl: Schema.optional(Schema.Literals(["fallback", "preferred"])).annotate({ - description: - "Live crawl mode - 'fallback': use live crawling as backup if cached unavailable, 'preferred': prioritize live crawling (default: 'fallback')", - }), - type: Schema.optional(Schema.Literals(["auto", "fast", "deep"])).annotate({ - description: "Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search", - }), - contextMaxCharacters: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_CONTEXT_CHARACTERS))).annotate( - { - description: `Maximum characters for context string optimized for models (default: 10000, maximum: ${MAX_CONTEXT_CHARACTERS})`, - }, - ), -}) - -export const Provider = Schema.Literals(["exa", "parallel"]) -export type Provider = typeof Provider.Type - -export interface Config { - readonly provider?: Provider - readonly enableExa: boolean - readonly enableParallel: boolean - readonly exaApiKey?: string - readonly parallelApiKey?: string -} - -export class ConfigService extends Context.Service()("@opencode/v2/WebSearchConfig") {} - -/** Isolates the retained product environment contract from the generic tool implementation. */ -export const defaultConfigLayer = Layer.sync(ConfigService, () => - ConfigService.of({ - provider: - process.env.OPENCODE_WEBSEARCH_PROVIDER === "exa" || process.env.OPENCODE_WEBSEARCH_PROVIDER === "parallel" - ? process.env.OPENCODE_WEBSEARCH_PROVIDER - : undefined, - enableExa: - ["1", "true"].includes(process.env.OPENCODE_EXPERIMENTAL?.toLowerCase() ?? "") || - ["1", "true"].includes(process.env.OPENCODE_ENABLE_EXA?.toLowerCase() ?? "") || - ["1", "true"].includes(process.env.OPENCODE_EXPERIMENTAL_EXA?.toLowerCase() ?? ""), - enableParallel: - ["1", "true"].includes(process.env.OPENCODE_ENABLE_PARALLEL?.toLowerCase() ?? "") || - ["1", "true"].includes(process.env.OPENCODE_EXPERIMENTAL_PARALLEL?.toLowerCase() ?? ""), - exaApiKey: process.env.EXA_API_KEY, - parallelApiKey: process.env.PARALLEL_API_KEY, - }), -) - -export const configNode = makeLocationNode({ service: ConfigService, layer: defaultConfigLayer, deps: [] }) - -export function selectProvider( - sessionID: string, - flags: Pick = { enableExa: false, enableParallel: false }, - override?: Provider, -): Provider { - if (override) return override - if (flags.enableParallel) return "parallel" - if (flags.enableExa) return "exa" - return Number.parseInt(checksum(sessionID) ?? "0", 36) % 2 === 0 ? "exa" : "parallel" -} - -const McpResult = Schema.Struct({ - result: Schema.Struct({ - content: Schema.Array(Schema.Struct({ type: Schema.String, text: Schema.String })), - }), -}) -const decodeMcpResult = Schema.decodeUnknownEffect(Schema.fromJsonString(McpResult)) - -const parsePayload = (payload: string) => - Effect.gen(function* () { - const trimmed = payload.trim() - if (!trimmed.startsWith("{")) return undefined - return (yield* decodeMcpResult(trimmed)).result.content.find((item) => item.text)?.text - }) - -export const parseResponse = Effect.fn("WebSearchTool.parseResponse")(function* (body: string) { - const trimmed = body.trim() - const direct = trimmed ? yield* parsePayload(trimmed) : undefined - if (direct) return direct - for (const line of body.split("\n")) { - if (!line.startsWith("data: ")) continue - const data = yield* parsePayload(line.substring(6)) - if (data) return data - } - return undefined -}) - -const ExaArgs = Schema.Struct({ - query: Schema.String, - type: Schema.String, - numResults: Schema.Number, - livecrawl: Schema.String, - contextMaxCharacters: Schema.optional(Schema.Number), -}) -const ParallelArgs = Schema.Struct({ - objective: Schema.String, - search_queries: Schema.Array(Schema.String), - session_id: Schema.String, }) -const McpRequest = (args: Schema.Struct) => - Schema.Struct({ - jsonrpc: Schema.Literal("2.0"), - id: Schema.Literal(1), - method: Schema.Literal("tools/call"), - params: Schema.Struct({ name: Schema.String, arguments: args }), - }) - -const exaUrl = (apiKey: string | undefined) => { - if (!apiKey) return EXA_URL - const url = new URL(EXA_URL) - url.searchParams.set("exaApiKey", apiKey) - return url.toString() -} - -const callMcp = ( - http: HttpClient.HttpClient, - url: string, - tool: string, - args: Schema.Struct, - value: Schema.Struct.Type, - headers: Record = {}, -) => - Effect.gen(function* () { - const request = yield* HttpClientRequest.post(url).pipe( - HttpClientRequest.accept("application/json, text/event-stream"), - HttpClientRequest.setHeaders(headers), - HttpClientRequest.schemaBodyJson(McpRequest(args))({ - jsonrpc: "2.0" as const, - id: 1 as const, - method: "tools/call" as const, - params: { name: tool, arguments: value }, - }), - ) - return yield* Effect.gen(function* () { - const response = yield* HttpClient.filterStatusOk(http).execute(request) - const body = yield* collectBoundedResponseBody( - response, - MAX_RESPONSE_BYTES, - () => new Error(`${tool} response exceeded ${MAX_RESPONSE_BYTES} bytes`), - ) - return yield* parseResponse(body.toString("utf8")) - }).pipe( - Effect.timeoutOrElse({ - duration: Duration.seconds(25), - orElse: () => Effect.fail(new Error(`${tool} request timed out`)), - }), - ) - }) const Output = Schema.Struct({ - provider: Provider, - text: Schema.String, + provider: WebSearch.ID, + results: Schema.Array(WebSearch.Result), }) export const Plugin = { id: "opencode.tool.websearch", effect: Effect.fn("WebSearchTool.Plugin")(function* (ctx: PluginContext) { - const http = yield* HttpClient.HttpClient - const config = yield* ConfigService const permission = yield* PermissionV2.Service + const forms = yield* Form.Service + const kv = yield* KV.Service yield* ctx.tool .transform((draft) => draft.add( name, - Tool.make({ + { description, input: Input, output: Output, - execute: (input, context) => { - const provider = selectProvider(context.sessionID, config, config.provider) - return Effect.gen(function* () { + execute: (input, context) => + Effect.gen(function* () { yield* permission.assert({ action: name, resources: [input.query], save: ["*"], - metadata: { ...input, provider }, + metadata: input, sessionID: context.sessionID, agent: context.agent, source: { type: "tool", messageID: context.messageID, callID: context.callID }, }) - - const text = - provider === "exa" - ? yield* callMcp(http, exaUrl(config.exaApiKey), "web_search_exa", ExaArgs, { - query: input.query, - type: input.type || "auto", - numResults: input.numResults || 8, - livecrawl: input.livecrawl || "fallback", - contextMaxCharacters: input.contextMaxCharacters, + const result = yield* ctx.websearch.query(input).pipe( + Effect.catch((error) => { + if (!Schema.is(WebSearch.ProviderRequiredError)(error)) return Effect.fail(error) + return Effect.gen(function* () { + const providers = (yield* ctx.websearch.providers()).data + if (providers.length === 0) return yield* new WebSearch.ProviderRequiredError() + const response = yield* forms.ask({ + sessionID: context.sessionID, + title: "Choose a web search provider", + metadata: { kind: "websearch.provider" }, + fields: [ + { + key: "provider", + title: "Provider", + description: "This becomes your default and can be changed later in configuration.", + type: "string", + required: true, + custom: false, + options: [ + ...providers.map((provider) => ({ value: provider.id, label: provider.name })), + { value: "__disable__", label: "Disable web search" }, + ], + }, + ], }) - : yield* callMcp( - http, - PARALLEL_URL, - "web_search", - ParallelArgs, - { - objective: input.query, - search_queries: [input.query], - session_id: context.sessionID, - // V2 invocation context does not safely expose the model yet. - }, - { - "User-Agent": App.useragent(ctx.app), - ...(config.parallelApiKey ? { Authorization: `Bearer ${config.parallelApiKey}` } : {}), - }, - ) + if (response.status === "cancelled") return yield* Effect.fail(new Error("Web search cancelled")) + const answer = response.answer.provider + if (answer === "__disable__") { + yield* kv.set("websearch:provider", false) + return yield* new WebSearch.DisabledError() + } + if (typeof answer !== "string" || !providers.some((provider) => provider.id === answer)) + return yield* new WebSearch.ProviderRequiredError() + yield* kv.set("websearch:provider", answer) + return yield* ctx.websearch.query(input) + }) + }), + ) const output = { - provider, - text: text ?? NO_RESULTS, + provider: result.data.providerID, + results: result.data.results, } - return { output, content: output.text, metadata: { provider: output.provider } } + const content = output.results.length + ? output.results + .map((result) => { + const title = result.title ?? result.url + const published = result.time.published + ? `\nPublished: ${new Date(result.time.published).toISOString()}` + : "" + return `## [${title}](${result.url})${published}${result.content ? `\n\n${result.content}` : ""}` + }) + .join("\n\n") + : NO_RESULTS + return { output, content, metadata: { provider: output.provider } } }).pipe( Effect.mapError( (error) => new ToolFailure({ message: `Unable to search the web for ${input.query}`, error }), ), - ) - }, - }), + ), + }, { codemode: false }, ), ) .pipe(Effect.orDie) + + yield* ctx.session.hook("context", (event) => + Effect.gen(function* () { + if ((yield* kv.get("websearch:provider")) === false) delete event.tools[name] + }), + ) }), } diff --git a/packages/core/src/websearch.ts b/packages/core/src/websearch.ts new file mode 100644 index 000000000000..a1ac9107cd36 --- /dev/null +++ b/packages/core/src/websearch.ts @@ -0,0 +1,144 @@ +export * as WebSearch from "./websearch" + +import { WebSearch } from "@opencode-ai/schema/websearch" +import { Context, Effect, Layer, Schema } from "effect" +import { makeLocationNode } from "@opencode-ai/util/effect/app-node" +import { EventV2 } from "./event" +import { KV } from "./kv" +import { State } from "./state" + +export const ID = WebSearch.ID +export type ID = WebSearch.ID + +export const Provider = WebSearch.Provider +export type Provider = WebSearch.Provider + +export const Event = WebSearch.Event + +export const Input = WebSearch.Input +export type Input = WebSearch.Input +export type ProviderInput = WebSearch.ProviderInput + +export const Result = WebSearch.Result +export type Result = WebSearch.Result + +export const Response = WebSearch.Response +export type Response = WebSearch.Response + +export interface ProviderImplementation extends Provider { + readonly execute: (input: ProviderInput) => Effect.Effect +} + +export class ProviderRequiredError extends Schema.TaggedErrorClass()( + "WebSearch.ProviderRequired", + {}, +) {} + +export class ProviderNotFoundError extends Schema.TaggedErrorClass()( + "WebSearch.ProviderNotFound", + { + providerID: ID, + }, +) {} + +export class DisabledError extends Schema.TaggedErrorClass()("WebSearch.Disabled", {}) {} + +export class RequestError extends Schema.TaggedErrorClass()("WebSearch.Request", { + providerID: ID, + cause: Schema.Defect(), +}) {} + +export type Error = ProviderRequiredError | ProviderNotFoundError | DisabledError | RequestError + +export interface Interface extends State.Transformable { + readonly providers: () => Effect.Effect + readonly default: () => Effect.Effect + readonly query: (input: Input) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/WebSearch") {} + +type Data = { + readonly providers: Map + defaultProviderID?: ID +} + +export type Draft = { + add: (provider: ProviderImplementation) => void + default: { + get: () => ID | undefined + set: (providerID: ID) => void + } +} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const events = yield* EventV2.Service + const kv = yield* KV.Service + const decodeResults = Schema.decodeUnknownEffect(Schema.Array(Result)) + const state = State.create({ + initial: () => ({ providers: new Map() }), + draft: (draft) => ({ + add: (provider) => draft.providers.set(provider.id, provider), + default: { + get: () => draft.defaultProviderID, + set: (providerID) => (draft.defaultProviderID = providerID), + }, + }), + finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid), + }) + + const requireProvider = (providers: Map, providerID: ID) => { + const provider = providers.get(providerID) + return provider ? Effect.succeed(provider) : Effect.fail(new ProviderNotFoundError({ providerID })) + } + + const defaultProvider = Effect.fn("WebSearch.default")(function* () { + const data = state.get() + const configured = data.defaultProviderID ? data.providers.get(data.defaultProviderID) : undefined + if (configured) return configured + const stored = yield* kv.get("websearch:provider") + if (stored === false) return yield* new DisabledError() + if (typeof stored !== "string") return + return data.providers.get(ID.make(stored)) + }) + + const resolve = Effect.fn("WebSearch.resolve")(function* (input: Input) { + const providers = state.get().providers + if (input.providerID) return yield* requireProvider(providers, input.providerID) + const provider = yield* defaultProvider() + if (!provider) return yield* new ProviderRequiredError() + return provider + }) + + return Service.of({ + transform: state.transform, + reload: state.reload, + providers: Effect.fn("WebSearch.providers")(function* () { + return Array.from(state.get().providers.values(), (provider) => ({ + id: provider.id, + name: provider.name, + })).toSorted((a, b) => a.name.localeCompare(b.name)) + }), + default: Effect.fn("WebSearch.defaultInfo")(function* () { + const provider = yield* defaultProvider() + return provider && { id: provider.id, name: provider.name } + }), + query: Effect.fn("WebSearch.query")(function* (input) { + const provider = yield* resolve(input) + const results = yield* provider.execute({ query: input.query }).pipe( + Effect.flatMap(decodeResults), + Effect.mapError((cause) => new RequestError({ providerID: provider.id, cause })), + ) + return new Response({ providerID: provider.id, results }) + }), + }) + }), +) + +export const node = makeLocationNode({ + service: Service, + layer, + deps: [EventV2.node, KV.node], +}) diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index 6584cc1c94cf..b826da794d14 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -121,23 +121,15 @@ describe("Config", () => { const config = yield* Config.Service const entries = yield* config.entries() expect( - entries.flatMap((entry) => - entry.type === "document" && entry.info.shell ? [entry.info.shell] : [], - ), + entries.flatMap((entry) => (entry.type === "document" && entry.info.shell ? [entry.info.shell] : [])), ).toEqual(["global", "explicit", "project", "content"]) expect(Config.latest(entries, "shell")).toBe("content") }).pipe( Effect.provide( - testLayer( - project, - global, - project, - undefined, - undefined, - emptyCredentialNode, - emptyWellknownNode, - { file: explicit, content: JSON.stringify({ shell: "content" }) }, - ), + testLayer(project, global, project, undefined, undefined, emptyCredentialNode, emptyWellknownNode, { + file: explicit, + content: JSON.stringify({ shell: "content" }), + }), ), ), ), @@ -155,31 +147,24 @@ describe("Config", () => { const global = path.join(tmp.path, "global") const project = path.join(tmp.path, "project") return Effect.promise(async () => { - await fs.mkdir(global, { recursive: true }) - await fs.mkdir(project, { recursive: true }) - await fs.writeFile(path.join(global, "opencode.json"), JSON.stringify({ shell: "global" })) - await fs.writeFile(path.join(project, "opencode.json"), JSON.stringify({ shell: "project" })) - }).pipe( - Effect.andThen( - Effect.gen(function* () { - const config = yield* Config.Service - expect(Config.latest(yield* config.entries(), "shell")).toBe("global") - }).pipe( - Effect.provide( - testLayer( - project, - global, - project, - undefined, - undefined, - emptyCredentialNode, - emptyWellknownNode, - { project: false }, - ), - ), + await fs.mkdir(global, { recursive: true }) + await fs.mkdir(project, { recursive: true }) + await fs.writeFile(path.join(global, "opencode.json"), JSON.stringify({ shell: "global" })) + await fs.writeFile(path.join(project, "opencode.json"), JSON.stringify({ shell: "project" })) + }).pipe( + Effect.andThen( + Effect.gen(function* () { + const config = yield* Config.Service + expect(Config.latest(yield* config.entries(), "shell")).toBe("global") + }).pipe( + Effect.provide( + testLayer(project, global, project, undefined, undefined, emptyCredentialNode, emptyWellknownNode, { + project: false, + }), ), ), - ) + ), + ) }), ), ) diff --git a/packages/core/test/lib/tool.ts b/packages/core/test/lib/tool.ts index 34314cdc5899..7bf51cd9320f 100644 --- a/packages/core/test/lib/tool.ts +++ b/packages/core/test/lib/tool.ts @@ -53,13 +53,17 @@ export function waitForCodeModeTool( * full plugin host. Only the tool domain is live; focused tool tests exercise * registration, snapshots, and execution through the same path production uses. */ -export const registerToolPlugin = (plugin: { - readonly id: string - readonly effect: (context: PluginContext) => Effect.Effect -}): Effect.Effect => +export const registerToolPlugin = ( + plugin: { + readonly id: string + readonly effect: (context: PluginContext) => Effect.Effect + }, + overrides: Parameters[0] = {}, +): Effect.Effect => Effect.gen(function* () { const tools = yield* Tools.Service const context = host({ + ...overrides, session: { hook: () => Effect.succeed({ dispose: Effect.void }), }, diff --git a/packages/core/test/plugin/fixture.ts b/packages/core/test/plugin/fixture.ts index ffe005cb63f8..fc56071558d6 100644 --- a/packages/core/test/plugin/fixture.ts +++ b/packages/core/test/plugin/fixture.ts @@ -2,6 +2,7 @@ import { AgentV2 } from "@opencode-ai/core/agent" import { AISDK } from "@opencode-ai/core/aisdk" import { Catalog } from "@opencode-ai/core/catalog" import { CommandV2 } from "@opencode-ai/core/command" +import { Config } from "@opencode-ai/core/config" import { Credential } from "@opencode-ai/core/credential" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNodePlatform } from "@opencode-ai/util/effect/app-node-platform" @@ -9,6 +10,7 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { EventV2 } from "@opencode-ai/core/event" import { FileSystem } from "@opencode-ai/core/filesystem" import { FSUtil } from "@opencode-ai/util/fs-util" +import { Form } from "@opencode-ai/core/form" import { Integration } from "@opencode-ai/core/integration" import { Location } from "@opencode-ai/core/location" import { Npm } from "@opencode-ai/util/npm" @@ -19,6 +21,7 @@ import { Reference } from "@opencode-ai/core/reference" import { SkillV2 } from "@opencode-ai/core/skill" import { ToolHooks } from "@opencode-ai/core/tool/hooks" import { ToolRegistry } from "@opencode-ai/core/tool/registry" +import { WebSearch } from "@opencode-ai/core/websearch" import { Effect, Layer } from "effect" import { tempLocationLayer } from "../fixture/location" @@ -39,6 +42,7 @@ export const PluginTestLayer = AppNodeBuilder.build( Npm.node, Credential.node, EventV2.node, + Form.node, LayerNodePlatform.httpClient, PluginV2.node, AgentV2.node, @@ -52,9 +56,11 @@ export const PluginTestLayer = AppNodeBuilder.build( SkillV2.node, ToolHooks.node, ToolRegistry.toolsNode, + WebSearch.node, ]), [ [Location.node, tempLocationLayer], [Npm.node, npmLayer], + [Config.node, Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) }))], ], ) as unknown as Layer.Layer diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index 1b28d6a84004..b7218ae690b0 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -1,11 +1,16 @@ -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import { Plugin } from "@opencode-ai/plugin/v2/effect" import { AgentV2 } from "@opencode-ai/core/agent" import { Catalog } from "@opencode-ai/core/catalog" import { Credential } from "@opencode-ai/core/credential" import { Integration } from "@opencode-ai/core/integration" +import { Location } from "@opencode-ai/core/location" import { ModelV2 } from "@opencode-ai/core/model" +import { Project } from "@opencode-ai/core/project" import { ProviderV2 } from "@opencode-ai/core/provider" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { WebSearch } from "@opencode-ai/core/websearch" import type { + CredentialOAuth, IntegrationCommandMethod, IntegrationEnvMethod, IntegrationKeyMethod, @@ -13,11 +18,11 @@ import type { } from "@opencode-ai/sdk/v2/types" import { Effect, Stream } from "effect" -type Overrides = Partial> & { - readonly session?: Partial +type Overrides = Partial> & { + readonly session?: Partial } -export function host(overrides: Overrides = {}): PluginContext { +export function host(overrides: Overrides = {}): Plugin.Context { return { app: overrides.app ?? { name: "test", version: "test", channel: "test" }, options: {}, @@ -92,6 +97,12 @@ export function host(overrides: Overrides = {}): PluginContext { transform: () => Effect.die("unused tool.transform"), hook: () => Effect.die("unused tool.hook"), }, + websearch: overrides.websearch ?? { + providers: () => Effect.die("unused websearch.providers"), + query: () => Effect.die("unused websearch.query"), + transform: () => Effect.die("unused websearch.transform"), + reload: () => Effect.die("unused websearch.reload"), + }, session: { hook: overrides.session?.hook ?? (() => Effect.die("unused session.hook")), create: overrides.session?.create ?? (() => Effect.die("unused session.create")), @@ -105,7 +116,7 @@ export function host(overrides: Overrides = {}): PluginContext { } } -export function agentHost(agent: AgentV2.Interface): PluginContext["agent"] { +export function agentHost(agent: AgentV2.Interface): Plugin.Context["agent"] { return { get: (id) => agent.get(AgentV2.ID.make(id)).pipe(Effect.map((value) => value && agentInfo(value))), list: () => Effect.die("unused agent.list"), @@ -131,7 +142,7 @@ export function agentHost(agent: AgentV2.Interface): PluginContext["agent"] { } } -export function catalogHost(catalog: Catalog.Interface): PluginContext["catalog"] { +export function catalogHost(catalog: Catalog.Interface): Plugin.Context["catalog"] { return { provider: { list: () => Effect.die("unused catalog.provider.list"), @@ -207,7 +218,7 @@ export function catalogHost(catalog: Catalog.Interface): PluginContext["catalog" } } -export function integrationHost(integration: Integration.Interface): PluginContext["integration"] { +export function integrationHost(integration: Integration.Interface): Plugin.Context["integration"] { return { list: () => Effect.die("unused integration.list"), get: () => Effect.die("unused integration.get"), @@ -329,6 +340,40 @@ export function integrationHost(integration: Integration.Interface): PluginConte } } +export function webSearchHost(websearch: WebSearch.Interface): Plugin.Context["websearch"] { + const location = Location.Info.make({ + directory: AbsolutePath.make("/tmp/websearch-test"), + project: { id: Project.ID.make("websearch-test"), directory: AbsolutePath.make("/tmp/websearch-test") }, + }) + return { + providers: () => websearch.providers().pipe(Effect.map((data) => ({ location, data }))), + query: (input) => + websearch + .query({ query: input.query, providerID: input.providerID && WebSearch.ID.make(input.providerID) }) + .pipe(Effect.map((data) => ({ location, data }))), + reload: websearch.reload, + transform: (callback) => + websearch.transform((draft) => { + callback({ + add: (definition) => + draft.add({ + id: WebSearch.ID.make(definition.id), + name: definition.name, + execute: definition.execute, + }), + default: { + get: draft.default.get, + set: (providerID) => draft.default.set(WebSearch.ID.make(providerID)), + }, + }) + }), + } +} + +function oauthCredential(value: CredentialOAuth) { + return Credential.OAuth.make({ ...value, methodID: Integration.MethodID.make(value.methodID) }) +} + function method(value: Integration.Method) { if (value.type === "env") return { type: value.type, names: [...value.names] } if (value.type === "key") return { type: value.type, label: value.label } diff --git a/packages/core/test/plugin/promise.test.ts b/packages/core/test/plugin/promise.test.ts index 76e2a1e31b15..97e30386f2be 100644 --- a/packages/core/test/plugin/promise.test.ts +++ b/packages/core/test/plugin/promise.test.ts @@ -8,6 +8,7 @@ import { PluginV2 } from "@opencode-ai/core/plugin" import { PluginHooks } from "@opencode-ai/core/plugin/hooks" import { PluginHost } from "@opencode-ai/core/plugin/host" import { PluginPromise } from "@opencode-ai/core/plugin/promise" +import { WebSearch } from "@opencode-ai/core/websearch" import { SessionV2 } from "@opencode-ai/core/session" import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionPending } from "@opencode-ai/core/session/pending" @@ -244,6 +245,38 @@ describe("fromPromise", () => { }), ) + it.effect("registers a standalone web search provider", () => + Effect.gen(function* () { + const websearch = yield* WebSearch.Service + const plugin = yield* PluginV2.Service + const host = yield* PluginHost.make(plugin) + const promisePlugin = Plugin.define({ + id: "promise-websearch", + setup: async (ctx) => { + await ctx.websearch.transform((draft) => { + draft.add({ + id: "promise-websearch", + name: "Promise Web Search", + execute: async (input) => [{ url: "https://example.com", content: `promise: ${input.query}`, time: {} }], + }) + }) + }, + }) + + yield* PluginPromise.fromPromise(promisePlugin).effect(host) + expect(yield* websearch.providers()).toContainEqual({ + id: WebSearch.ID.make("promise-websearch"), + name: "Promise Web Search", + }) + expect(yield* websearch.query({ query: "effect", providerID: WebSearch.ID.make("promise-websearch") })).toEqual( + new WebSearch.Response({ + providerID: WebSearch.ID.make("promise-websearch"), + results: [{ url: "https://example.com", content: "promise: effect", time: {} }], + }), + ) + }), + ) + it.effect("runs the setup cleanup when the plugin scope closes", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service diff --git a/packages/core/test/plugin/websearch-fixture.ts b/packages/core/test/plugin/websearch-fixture.ts new file mode 100644 index 000000000000..406e093249cf --- /dev/null +++ b/packages/core/test/plugin/websearch-fixture.ts @@ -0,0 +1,50 @@ +import { Effect, Layer } from "effect" +import { HttpClient, HttpClientResponse } from "effect/unstable/http" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { LayerNode } from "@opencode-ai/util/effect/layer-node" +import { Credential } from "@opencode-ai/core/credential" +import { Config } from "@opencode-ai/core/config" +import { EventV2 } from "@opencode-ai/core/event" +import { Form } from "@opencode-ai/core/form" +import { Integration } from "@opencode-ai/core/integration" +import { WebSearch } from "@opencode-ai/core/websearch" +import { testEffect } from "../lib/effect" + +interface WebSearchRequest { + readonly url: string + readonly headers: Record + readonly body: unknown +} + +export const requests: WebSearchRequest[] = [] +let responseBody = "" + +export function resetWebSearchFixture(body: string) { + requests.length = 0 + responseBody = body +} + +const http = Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request) => + Effect.sync(() => { + if (request.body._tag !== "Uint8Array") throw new Error(`Unexpected request body: ${request.body._tag}`) + requests.push({ + url: request.url, + headers: request.headers, + body: JSON.parse(new TextDecoder().decode(request.body.body)), + }) + return HttpClientResponse.fromWeb(request, new Response(responseBody, { status: 200 })) + }), + ), +) + +export const webSearchIntegrationTest = testEffect( + Layer.merge( + AppNodeBuilder.build( + LayerNode.group([Integration.node, Credential.node, EventV2.node, Form.node, WebSearch.node]), + [[Config.node, Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) }))]], + ), + http, + ), +) diff --git a/packages/core/test/plugin/websearch.test.ts b/packages/core/test/plugin/websearch.test.ts new file mode 100644 index 000000000000..a45785e40dd8 --- /dev/null +++ b/packages/core/test/plugin/websearch.test.ts @@ -0,0 +1,170 @@ +import { beforeEach, describe, expect } from "bun:test" +import { Effect } from "effect" +import { Integration } from "@opencode-ai/core/integration" +import { WebSearch } from "@opencode-ai/core/websearch" +import { WebSearchExa } from "@opencode-ai/core/plugin/websearch/exa" +import { WebSearchParallel } from "@opencode-ai/core/plugin/websearch/parallel" +import { host, integrationHost, webSearchHost } from "./host" +import { requests, resetWebSearchFixture, webSearchIntegrationTest } from "./websearch-fixture" + +beforeEach(() => { + resetWebSearchFixture( + `event: message\ndata: ${JSON.stringify({ + jsonrpc: "2.0", + id: 1, + result: { + content: [ + { + type: "text", + text: "Title: Effect\nURL: https://effect.website\nPublished: 2026-07-25T00:00:00.000Z\nAuthor: N/A\nHighlights:\nEffect documentation", + _meta: { searchTime: 123 }, + }, + ], + }, + })}\n\n`, + ) +}) + +const it = webSearchIntegrationTest + +describe("built-in web search providers", () => { + it.effect("registers a provider without an integration", () => + Effect.gen(function* () { + const integrations = yield* Integration.Service + const websearch = yield* WebSearch.Service + const registration = yield* webSearchHost(websearch).transform((draft) => { + draft.add({ + id: "test-websearch", + name: "Test Web Search", + execute: (input) => Effect.succeed([{ url: "https://example.com", content: input.query, time: {} }]), + }) + }) + + expect(yield* integrations.get(Integration.ID.make("test-websearch"))).toBeUndefined() + expect(yield* websearch.providers()).toContainEqual({ + id: WebSearch.ID.make("test-websearch"), + name: "Test Web Search", + }) + yield* registration.dispose + expect(yield* websearch.providers()).not.toContainEqual({ + id: WebSearch.ID.make("test-websearch"), + name: "Test Web Search", + }) + }), + ) + + it.effect("registers Exa with its MCP schema", () => + Effect.gen(function* () { + const integrations = yield* Integration.Service + const websearch = yield* WebSearch.Service + yield* WebSearchExa.Plugin.effect( + host({ integration: integrationHost(integrations), websearch: webSearchHost(websearch) }), + ) + + const info = yield* integrations.get(Integration.ID.make("exa")) + expect(info).toMatchObject({ + id: "exa", + name: "Exa", + methods: [{ type: "key" }, { type: "env", names: ["EXA_API_KEY"] }], + }) + yield* integrations.connection.key({ integrationID: Integration.ID.make("exa"), key: "exa secret" }) + expect(yield* websearch.query({ query: "effect typescript", providerID: WebSearch.ID.make("exa") })).toEqual( + new WebSearch.Response({ + providerID: WebSearch.ID.make("exa"), + results: [ + { + url: "https://effect.website", + title: "Effect", + content: "Effect documentation", + time: { published: Date.parse("2026-07-25T00:00:00.000Z") }, + }, + ], + }), + ) + expect(requests).toEqual([ + { + url: `${WebSearchExa.endpoint}?exaApiKey=exa+secret`, + headers: expect.any(Object), + body: { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { + name: "web_search_exa", + arguments: { query: "effect typescript", numResults: 8 }, + }, + }, + }, + ]) + }), + ) + + it.effect("registers Parallel and keeps its credential in the authorization header", () => + Effect.gen(function* () { + resetWebSearchFixture( + JSON.stringify({ + jsonrpc: "2.0", + id: 1, + result: { + content: [{ type: "text", text: "search results" }], + structuredContent: { + search_id: "search_1", + results: [ + { + url: "https://effect.website", + title: "Effect", + publish_date: null, + excerpts: ["Effect documentation"], + }, + ], + warnings: null, + usage: [{ name: "sku_search", count: 1 }], + session_id: "ses_parallel", + }, + }, + }), + ) + const integrations = yield* Integration.Service + const websearch = yield* WebSearch.Service + yield* WebSearchParallel.Plugin.effect( + host({ integration: integrationHost(integrations), websearch: webSearchHost(websearch) }), + ) + yield* integrations.connection.key({ integrationID: Integration.ID.make("parallel"), key: "parallel-secret" }) + + const output = yield* websearch.query({ + query: "effect layers", + providerID: WebSearch.ID.make("parallel"), + }) + expect(output).toEqual( + new WebSearch.Response({ + providerID: WebSearch.ID.make("parallel"), + results: [ + { + url: "https://effect.website", + title: "Effect", + content: "Effect documentation", + time: {}, + }, + ], + }), + ) + expect(requests[0]).toMatchObject({ + url: WebSearchParallel.endpoint, + headers: { authorization: "Bearer parallel-secret" }, + body: { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { + name: "web_search", + arguments: { + objective: "effect layers", + search_queries: ["effect layers"], + }, + }, + }, + }) + expect(JSON.stringify(output)).not.toContain("parallel-secret") + }), + ) +}) diff --git a/packages/core/test/tool-websearch.test.ts b/packages/core/test/tool-websearch.test.ts index 254dc4221d1b..6c1889680867 100644 --- a/packages/core/test/tool-websearch.test.ts +++ b/packages/core/test/tool-websearch.test.ts @@ -1,10 +1,11 @@ -import { beforeEach, describe, expect, test } from "bun:test" -import { Effect, Layer, Schema } from "effect" -import { HttpClient, HttpClientResponse } from "effect/unstable/http" +import { beforeEach, describe, expect } from "bun:test" +import { Effect, Layer } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { LayerNodePlatform } from "@opencode-ai/util/effect/app-node-platform" import { PermissionV2 } from "@opencode-ai/core/permission" +import { Form } from "@opencode-ai/core/form" +import { KV } from "@opencode-ai/core/kv" +import { WebSearch } from "@opencode-ai/core/websearch" import { SessionV2 } from "@opencode-ai/core/session" import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { WebSearchTool } from "@opencode-ai/core/tool/websearch" @@ -14,93 +15,36 @@ import { Image } from "@opencode-ai/core/image" import { testEffect } from "./lib/effect" import { imagePassthrough } from "./lib/image" import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from "./lib/tool" +import { webSearchHost } from "./plugin/host" const webSearchToolNode = makeLocationNode({ name: "test/websearch-tool-plugin", - layer: Layer.effectDiscard(registerToolPlugin(WebSearchTool.Plugin)), - deps: [ToolRegistry.toolsNode, PermissionV2.node, LayerNodePlatform.httpClient, WebSearchTool.configNode], + layer: Layer.effectDiscard( + Effect.gen(function* () { + const websearch = yield* WebSearch.Service + yield* registerToolPlugin(WebSearchTool.Plugin, { websearch: webSearchHost(websearch) }) + }), + ), + deps: [ToolRegistry.toolsNode, PermissionV2.node, WebSearch.node, Form.node, KV.node], }) const sessionID = SessionV2.ID.make("ses_websearch_test") -const payload = (text: string) => - JSON.stringify({ - jsonrpc: "2.0", - id: 1, - result: { content: [{ type: "text", text }] }, - }) - -describe("WebSearchTool provider selection", () => { - test("rejects out-of-range numeric controls", () => { - const decode = Schema.decodeUnknownSync(WebSearchTool.Input) - expect(() => decode({ query: "x", numResults: 0 })).toThrow() - expect(() => decode({ query: "x", numResults: WebSearchTool.MAX_NUM_RESULTS + 1 })).toThrow() - expect(() => decode({ query: "x", contextMaxCharacters: WebSearchTool.MAX_CONTEXT_CHARACTERS + 1 })).toThrow() - }) - test("selects a stable provider per session", () => { - expect(WebSearchTool.selectProvider(sessionID)).toBe(WebSearchTool.selectProvider(sessionID)) - }) - - test("supports an explicit operational override", () => { - expect(WebSearchTool.selectProvider(sessionID, { enableExa: false, enableParallel: false }, "parallel")).toBe( - "parallel", - ) - expect(WebSearchTool.selectProvider(sessionID, { enableExa: false, enableParallel: false }, "exa")).toBe("exa") - }) - - test("prefers Parallel when both explicit flags are enabled", () => { - expect(WebSearchTool.selectProvider(sessionID, { enableExa: true, enableParallel: true })).toBe("parallel") - }) - - test("prefers Exa when only its explicit flag is enabled", () => { - expect(WebSearchTool.selectProvider(sessionID, { enableExa: true, enableParallel: false })).toBe("exa") - }) -}) - -describe("WebSearchTool MCP response parser", () => { - test("parses plain JSON-RPC responses", async () => { - expect(await Effect.runPromise(WebSearchTool.parseResponse(payload("search results")))).toBe("search results") - }) - - test("parses SSE JSON-RPC responses and ignores non-JSON frames", async () => { - expect( - await Effect.runPromise( - WebSearchTool.parseResponse(`data: [DONE]\nevent: message\ndata: ${payload("search results")}\n\n`), - ), - ).toBe("search results") - }) -}) - -interface Request { - readonly url: string - readonly headers: Record - readonly body: unknown -} - -const requests: Request[] = [] const assertions: PermissionV2.AssertInput[] = [] -let responseBody = payload("search results") -let makeResponse = () => new Response(responseBody, { status: 200 }) -let config: WebSearchTool.Config = { enableExa: false, enableParallel: false } +const queries: WebSearch.Input[] = [] +let result = new WebSearch.Response({ + providerID: WebSearch.ID.make("exa"), + results: [{ url: "https://example.com", title: "Search results", content: "search results", time: {} }], +}) beforeEach(() => { - responseBody = payload("search results") - makeResponse = () => new Response(responseBody, { status: 200 }) + assertions.length = 0 + queries.length = 0 + result = new WebSearch.Response({ + providerID: WebSearch.ID.make("exa"), + results: [{ url: "https://example.com", title: "Search results", content: "search results", time: {} }], + }) }) -const http = Layer.succeed( - HttpClient.HttpClient, - HttpClient.make((request) => - Effect.sync(() => { - if (request.body._tag !== "Uint8Array") throw new Error(`Unexpected request body: ${request.body._tag}`) - requests.push({ - url: request.url, - headers: request.headers, - body: JSON.parse(new TextDecoder().decode(request.body.body)), - }) - return HttpClientResponse.fromWeb(request, makeResponse()) - }), - ), -) const permission = Layer.succeed( PermissionV2.Service, PermissionV2.Service.of({ @@ -112,33 +56,48 @@ const permission = Layer.succeed( list: () => Effect.die("unused"), }), ) -const websearchConfig = Layer.succeed( - WebSearchTool.ConfigService, - WebSearchTool.ConfigService.of({ - get provider() { - return config.provider - }, - get enableExa() { - return config.enableExa - }, - get enableParallel() { - return config.enableParallel - }, - get exaApiKey() { - return config.exaApiKey - }, - get parallelApiKey() { - return config.parallelApiKey - }, +const websearch = Layer.succeed( + WebSearch.Service, + WebSearch.Service.of({ + transform: () => Effect.die("unused"), + reload: () => Effect.die("unused"), + providers: () => Effect.succeed([]), + default: () => Effect.succeed(undefined), + query: (input) => + Effect.sync(() => { + queries.push(input) + return result + }), + }), +) +const form = Layer.succeed( + Form.Service, + Form.Service.of({ + create: () => Effect.die("unused"), + ask: () => Effect.die("unused"), + get: () => Effect.die("unused"), + list: () => Effect.die("unused"), + state: () => Effect.die("unused"), + reply: () => Effect.die("unused"), + cancel: () => Effect.die("unused"), + }), +) +const kv = Layer.succeed( + KV.Service, + KV.Service.of({ + get: () => Effect.succeed(undefined), + set: () => Effect.void, + remove: () => Effect.void, }), ) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, WebSearchTool.configNode, webSearchToolNode]), + LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, WebSearch.node, webSearchToolNode]), [ [PermissionV2.node, permission], - [LayerNodePlatform.httpClient, http], - [WebSearchTool.configNode, websearchConfig], + [WebSearch.node, websearch], + [Form.node, form], + [KV.node, kv], [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], [Image.node, imagePassthrough], ], @@ -146,12 +105,8 @@ const it = testEffect( ) describe("WebSearchTool registration", () => { - it.effect("registers websearch, asserts query permission, and calls Exa", () => + it.effect("asserts permission before delegating to WebSearch", () => Effect.gen(function* () { - requests.length = 0 - assertions.length = 0 - responseBody = payload("exa results") - config = { provider: "exa", enableExa: false, enableParallel: false } const registry = yield* ToolRegistry.Service expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["websearch", "execute"]) @@ -161,20 +116,14 @@ describe("WebSearchTool registration", () => { ...toolIdentity, call: { type: "tool-call", - id: "call-exa", + id: "call-search", name: "websearch", - input: { - query: "effect typescript", - numResults: 3, - livecrawl: "preferred", - type: "fast", - contextMaxCharacters: 2500, - }, + input: { query: "effect typescript" }, }, }), ).toMatchObject({ status: "completed", - content: [{ type: "text", text: "exa results" }], + content: [{ type: "text", text: "## [Search results](https://example.com)\n\nsearch results" }], }) expect(assertions).toMatchObject([ { @@ -182,155 +131,77 @@ describe("WebSearchTool registration", () => { action: "websearch", resources: ["effect typescript"], save: ["*"], - metadata: { - query: "effect typescript", - numResults: 3, - livecrawl: "preferred", - type: "fast", - contextMaxCharacters: 2500, - provider: "exa", - }, + metadata: { query: "effect typescript" }, }, ]) - expect(requests).toEqual([ + expect(queries).toEqual([ { - url: WebSearchTool.EXA_URL, - headers: expect.any(Object), - body: { - jsonrpc: "2.0", - id: 1, - method: "tools/call", - params: { - name: "web_search_exa", - arguments: { - query: "effect typescript", - type: "fast", - numResults: 3, - livecrawl: "preferred", - contextMaxCharacters: 2500, - }, - }, - }, + query: "effect typescript", }, ]) }), ) - it.effect("calls Parallel with session ID and keeps bearer credentials out of output", () => + it.effect("keeps normalized results in structured output", () => Effect.gen(function* () { - requests.length = 0 - assertions.length = 0 - responseBody = payload("parallel results") - config = { provider: "parallel", enableExa: false, enableParallel: false, parallelApiKey: "parallel-secret" } - const registry = yield* ToolRegistry.Service - - const settled = yield* executeTool(registry, { - sessionID, - ...toolIdentity, - call: { type: "tool-call", id: "call-parallel", name: "websearch", input: { query: "effect layers" } }, - }) - - expect(requests[0]).toMatchObject({ - url: WebSearchTool.PARALLEL_URL, - headers: { authorization: "Bearer parallel-secret" }, - body: { - jsonrpc: "2.0", - id: 1, - method: "tools/call", - params: { - name: "web_search", - arguments: { objective: "effect layers", search_queries: ["effect layers"], session_id: sessionID }, + result = new WebSearch.Response({ + providerID: WebSearch.ID.make("parallel"), + results: [ + { + url: "https://effect.website", + title: "Effect", + content: "parallel results", + time: { published: Date.parse("2026-07-25T00:00:00.000Z") }, }, - }, - }) - expect(requests[0]?.body).not.toHaveProperty("params.arguments.model_name") - expect(settled).toEqual({ - status: "completed", - output: { provider: "parallel", text: "parallel results" }, - content: [{ type: "text", text: "parallel results" }], - metadata: { provider: "parallel" }, + ], }) - expect(JSON.stringify(settled)).not.toContain("parallel-secret") - }), - ) - - it.effect("keeps an Exa credential in the transport URL and out of model output", () => - Effect.gen(function* () { - requests.length = 0 - assertions.length = 0 - responseBody = payload("credentialed exa results") - config = { provider: "exa", enableExa: false, enableParallel: false, exaApiKey: "exa secret" } - const registry = yield* ToolRegistry.Service - - const settled = yield* executeTool(registry, { - sessionID, - ...toolIdentity, - call: { type: "tool-call", id: "call-exa-key", name: "websearch", input: { query: "effect schema" } }, - }) - - expect(requests[0]?.url).toBe(`${WebSearchTool.EXA_URL}?exaApiKey=exa+secret`) - expect(JSON.stringify(settled)).not.toContain("exa secret") - }), - ) - - it.effect("returns the legacy no-results fallback as concise model text", () => - Effect.gen(function* () { - requests.length = 0 - assertions.length = 0 - responseBody = "" - config = { provider: "exa", enableExa: false, enableParallel: false } const registry = yield* ToolRegistry.Service expect( yield* executeTool(registry, { sessionID, ...toolIdentity, - call: { type: "tool-call", id: "call-empty", name: "websearch", input: { query: "nothing" } }, + call: { type: "tool-call", id: "call-parallel", name: "websearch", input: { query: "effect layers" } }, }), - ).toMatchObject({ + ).toEqual({ status: "completed", - content: [{ type: "text", text: WebSearchTool.NO_RESULTS }], + output: { + provider: "parallel", + results: [ + { + url: "https://effect.website", + title: "Effect", + content: "parallel results", + time: { published: Date.parse("2026-07-25T00:00:00.000Z") }, + }, + ], + }, + content: [ + { + type: "text", + text: "## [Effect](https://effect.website)\nPublished: 2026-07-25T00:00:00.000Z\n\nparallel results", + }, + ], + metadata: { provider: "parallel" }, }) }), ) - it.effect("rejects oversized MCP response bodies", () => + it.effect("uses the concise no-results fallback", () => Effect.gen(function* () { - requests.length = 0 - assertions.length = 0 - let chunksRead = 0 - let cancelled = false - makeResponse = () => - new Response( - new ReadableStream({ - pull(controller) { - chunksRead++ - if (chunksRead === 10) throw new Error("response was not stopped at the byte limit") - controller.enqueue(new Uint8Array(64 * 1024)) - }, - cancel() { - cancelled = true - }, - }), - { status: 200 }, - ) - config = { provider: "exa", enableExa: false, enableParallel: false } + result = new WebSearch.Response({ providerID: WebSearch.ID.make("exa"), results: [] }) const registry = yield* ToolRegistry.Service expect( yield* executeTool(registry, { sessionID, ...toolIdentity, - call: { type: "tool-call", id: "call-large-response", name: "websearch", input: { query: "too much" } }, + call: { type: "tool-call", id: "call-empty", name: "websearch", input: { query: "nothing" } }, }), - // toSessionError unwraps the "Unable to search the web for " ToolFailure - // to its byte-limit cause message. - ).toEqual({ - status: "error", - error: { type: "unknown", message: expect.stringContaining("response exceeded") }, + ).toMatchObject({ + status: "completed", + content: [{ type: "text", text: WebSearchTool.NO_RESULTS }], }) - expect(chunksRead).toBeLessThan(10) - expect(cancelled).toBe(true) }), ) }) diff --git a/packages/core/test/websearch.test.ts b/packages/core/test/websearch.test.ts new file mode 100644 index 000000000000..104c62860604 --- /dev/null +++ b/packages/core/test/websearch.test.ts @@ -0,0 +1,129 @@ +import { describe, expect } from "bun:test" +import { Effect, Exit, Scope } from "effect" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { LayerNode } from "@opencode-ai/util/effect/layer-node" +import { EventV2 } from "@opencode-ai/core/event" +import { KV } from "@opencode-ai/core/kv" +import { WebSearch } from "@opencode-ai/core/websearch" +import { testEffect } from "./lib/effect" + +const it = testEffect(AppNodeBuilder.build(LayerNode.group([WebSearch.node, EventV2.node, KV.node]))) + +const register = (id: string) => + Effect.gen(function* () { + const websearch = yield* WebSearch.Service + const providerID = WebSearch.ID.make(id) + const calls: WebSearch.ProviderInput[] = [] + yield* websearch.transform((draft) => { + draft.add({ + id: providerID, + name: id.toUpperCase(), + execute: (input) => + Effect.sync(() => { + calls.push(input) + return [ + { + url: `https://${id}.example.com`, + title: input.query, + content: `${id}: ${input.query}`, + time: {}, + }, + ] + }), + }) + }) + return { providerID, calls } + }) + +describe("WebSearch", () => { + it.effect("executes an explicit provider without changing the default", () => + Effect.gen(function* () { + yield* register("exa") + const parallel = yield* register("parallel") + const websearch = yield* WebSearch.Service + + expect(yield* websearch.query({ query: "effect", providerID: parallel.providerID })).toEqual( + new WebSearch.Response({ + providerID: parallel.providerID, + results: [ + { + url: "https://parallel.example.com", + title: "effect", + content: "parallel: effect", + time: {}, + }, + ], + }), + ) + expect((yield* websearch.query({ query: "default" }).pipe(Effect.flip))._tag).toBe("WebSearch.ProviderRequired") + expect(parallel.calls).toEqual([{ query: "effect" }]) + }), + ) + + it.effect("requires a provider when no default is set", () => + Effect.gen(function* () { + yield* register("exa") + yield* register("parallel") + const websearch = yield* WebSearch.Service + + expect((yield* websearch.query({ query: "layers" }).pipe(Effect.flip))._tag).toBe("WebSearch.ProviderRequired") + }), + ) + + it.effect("uses the default set by a transform", () => + Effect.gen(function* () { + yield* register("exa") + const parallel = yield* register("parallel") + const websearch = yield* WebSearch.Service + yield* websearch.transform((draft) => draft.default.set(parallel.providerID)) + + expect((yield* websearch.query({ query: "configured" })).providerID).toBe(parallel.providerID) + }), + ) + + it.effect("uses the provider stored in KV", () => + Effect.gen(function* () { + yield* register("exa") + const parallel = yield* register("parallel") + const websearch = yield* WebSearch.Service + const kv = yield* KV.Service + yield* kv.set("websearch:provider", parallel.providerID) + + expect((yield* websearch.query({ query: "stored" })).providerID).toBe(parallel.providerID) + yield* kv.remove("websearch:provider") + }), + ) + + it.effect("fails when web search is explicitly disabled", () => + Effect.gen(function* () { + yield* register("exa") + const websearch = yield* WebSearch.Service + const kv = yield* KV.Service + yield* kv.set("websearch:provider", false) + + expect((yield* websearch.query({ query: "disabled" }).pipe(Effect.flip))._tag).toBe("WebSearch.Disabled") + yield* kv.remove("websearch:provider") + }), + ) + + it.effect("falls back when the configured default is unavailable", () => + Effect.gen(function* () { + yield* register("exa") + const websearch = yield* WebSearch.Service + yield* websearch.transform((draft) => draft.default.set(WebSearch.ID.make("missing"))) + + expect((yield* websearch.query({ query: "fallback" }).pipe(Effect.flip))._tag).toBe("WebSearch.ProviderRequired") + }), + ) + + it.effect("removes scoped provider registrations", () => + Effect.gen(function* () { + const websearch = yield* WebSearch.Service + const scope = yield* Scope.fork(yield* Scope.Scope) + const provider = yield* register("temporary").pipe(Scope.provide(scope)) + expect(yield* websearch.providers()).toContainEqual({ id: provider.providerID, name: "TEMPORARY" }) + yield* Scope.close(scope, Exit.void) + expect(yield* websearch.providers()).not.toContainEqual({ id: provider.providerID, name: "TEMPORARY" }) + }), + ) +}) diff --git a/packages/plugin/src/v2/effect/index.ts b/packages/plugin/src/v2/effect/index.ts index e00e302b7ada..09c46955a1ff 100644 --- a/packages/plugin/src/v2/effect/index.ts +++ b/packages/plugin/src/v2/effect/index.ts @@ -9,3 +9,4 @@ export { Model } from "@opencode-ai/schema/model" export { Provider } from "@opencode-ai/schema/provider" export { Reference } from "@opencode-ai/schema/reference" export { Skill } from "@opencode-ai/schema/skill" +export { WebSearch } from "@opencode-ai/schema/websearch" diff --git a/packages/plugin/src/v2/effect/plugin.ts b/packages/plugin/src/v2/effect/plugin.ts index 02cb8a681fb7..127e6a70b549 100644 --- a/packages/plugin/src/v2/effect/plugin.ts +++ b/packages/plugin/src/v2/effect/plugin.ts @@ -12,6 +12,7 @@ import type { ReferenceDomain } from "./reference.js" import type { SessionDomain } from "./session.js" import type { SkillDomain } from "./skill.js" import type { ToolDomain } from "./tool.js" +import type { WebSearchDomain } from "./websearch.js" export interface Context { readonly app: App @@ -27,6 +28,7 @@ export interface Context { readonly session: SessionDomain readonly skill: SkillDomain readonly tool: ToolDomain + readonly websearch: WebSearchDomain } export interface Plugin { diff --git a/packages/plugin/src/v2/effect/websearch.ts b/packages/plugin/src/v2/effect/websearch.ts new file mode 100644 index 000000000000..03d913e98f06 --- /dev/null +++ b/packages/plugin/src/v2/effect/websearch.ts @@ -0,0 +1,23 @@ +import type { WebSearch } from "@opencode-ai/schema/websearch" +import type { WebsearchApi } from "@opencode-ai/client/effect/api" +import type { Effect } from "effect" +import type { Transform } from "./registration.js" + +export interface WebSearchDefinition { + readonly id: string + readonly name: string + readonly execute: (input: WebSearch.ProviderInput) => Effect.Effect +} + +export interface WebSearchDomain extends WebsearchApi { + readonly transform: Transform + readonly reload: () => Effect.Effect +} + +export interface WebSearchDraft { + add(definition: WebSearchDefinition): void + readonly default: { + get(): string | undefined + set(providerID: string): void + } +} diff --git a/packages/plugin/src/v2/promise/index.ts b/packages/plugin/src/v2/promise/index.ts index ae8e6d18a193..786d150bb5b6 100644 --- a/packages/plugin/src/v2/promise/index.ts +++ b/packages/plugin/src/v2/promise/index.ts @@ -10,3 +10,4 @@ export { Model } from "@opencode-ai/schema/model" export { Provider } from "@opencode-ai/schema/provider" export { Reference } from "@opencode-ai/schema/reference" export { Skill } from "@opencode-ai/schema/skill" +export { WebSearch } from "@opencode-ai/schema/websearch" diff --git a/packages/plugin/src/v2/promise/integration.ts b/packages/plugin/src/v2/promise/integration.ts index e666b83b8a55..6b121afc8bc1 100644 --- a/packages/plugin/src/v2/promise/integration.ts +++ b/packages/plugin/src/v2/promise/integration.ts @@ -1,10 +1,32 @@ import type { IntegrationApi } from "@opencode-ai/client/promise/api" import type { IntegrationDraft, IntegrationMethodRegistration } from "../effect/integration.js" -import type { CredentialValue } from "@opencode-ai/sdk/v2/types" +import type { + CredentialOAuth, + CredentialValue, + IntegrationEnvMethod, + IntegrationInputs, + IntegrationKeyMethod, + IntegrationOAuthMethod, +} from "@opencode-ai/sdk/v2/types" import type { Transform } from "./registration.js" export type { IntegrationDraft, IntegrationMethodRegistration } +export type IntegrationOAuthAuthorization = { + readonly url: string + readonly instructions: string + readonly expiresAt?: number +} & ( + | { + readonly mode: "auto" + readonly callback: Promise + } + | { + readonly mode: "code" + readonly callback: (code: string) => Promise + } +) + export interface IntegrationDomain extends Omit { readonly transform: Transform readonly reload: () => Promise diff --git a/packages/plugin/src/v2/promise/plugin.ts b/packages/plugin/src/v2/promise/plugin.ts index a6a3389f7acb..35225b076c20 100644 --- a/packages/plugin/src/v2/promise/plugin.ts +++ b/packages/plugin/src/v2/promise/plugin.ts @@ -11,6 +11,7 @@ import type { ReferenceDomain } from "./reference.js" import type { SessionDomain } from "./session.js" import type { SkillDomain } from "./skill.js" import type { ToolDomain } from "./tool.js" +import type { WebSearchDomain } from "./websearch.js" export interface Context { readonly app: App @@ -26,6 +27,7 @@ export interface Context { readonly session: SessionDomain readonly skill: SkillDomain readonly tool: ToolDomain + readonly websearch: WebSearchDomain } export type Cleanup = () => Promise | void diff --git a/packages/plugin/src/v2/promise/websearch.ts b/packages/plugin/src/v2/promise/websearch.ts new file mode 100644 index 000000000000..999f48523578 --- /dev/null +++ b/packages/plugin/src/v2/promise/websearch.ts @@ -0,0 +1,25 @@ +import type { WebSearch } from "@opencode-ai/schema/websearch" +import type { WebSearchApi } from "@opencode-ai/client/promise/api" +import type { Transform } from "./registration.js" + +export interface WebSearchDefinition { + readonly id: string + readonly name: string + readonly execute: ( + input: WebSearch.ProviderInput, + context: { readonly signal: AbortSignal }, + ) => Promise +} + +export interface WebSearchDomain extends WebSearchApi { + readonly transform: Transform + readonly reload: () => Promise +} + +export interface WebSearchDraft { + add(definition: WebSearchDefinition): void + readonly default: { + get(): string | undefined + set(providerID: string): void + } +} diff --git a/packages/plugin/test/contract-identity.test.ts b/packages/plugin/test/contract-identity.test.ts index 243a7aa89be3..264fcaa2f8d4 100644 --- a/packages/plugin/test/contract-identity.test.ts +++ b/packages/plugin/test/contract-identity.test.ts @@ -8,6 +8,7 @@ import { Model } from "@opencode-ai/schema/model" import { Provider } from "@opencode-ai/schema/provider" import { Reference } from "@opencode-ai/schema/reference" import { Skill } from "@opencode-ai/schema/skill" +import { WebSearch } from "@opencode-ai/schema/websearch" const Plugin = await import("../src/v2/effect/index") const PromisePlugin = await import("../src/v2/promise/index") @@ -16,7 +17,7 @@ const TuiPlugin = await import("../src/v2/tui/index") test.each([ ["effect", Plugin], ["promise", PromisePlugin], -])("%s entrypoint exposes its canonical Schema contracts", (name, entrypoint) => { +])("%s entrypoint exposes its canonical Schema contracts", (_name, entrypoint) => { expect(entrypoint.Agent).toBe(Agent) expect(entrypoint.Command).toBe(Command) expect(entrypoint.Connection).toBe(Connection) @@ -26,6 +27,7 @@ test.each([ expect(entrypoint.Provider).toBe(Provider) expect(entrypoint.Reference).toBe(Reference) expect(entrypoint.Skill).toBe(Skill) + expect(entrypoint.WebSearch).toBe(WebSearch) expect(Object.keys(entrypoint).sort()).toEqual([ "Agent", "Command", @@ -37,6 +39,7 @@ test.each([ "Provider", "Reference", "Skill", + "WebSearch", ]) }) diff --git a/packages/protocol/src/api.ts b/packages/protocol/src/api.ts index df48ecb3a266..2249797df4a6 100644 --- a/packages/protocol/src/api.ts +++ b/packages/protocol/src/api.ts @@ -25,6 +25,7 @@ import { ReferenceGroup } from "./groups/reference.js" import { Authorization } from "./middleware/authorization.js" import { LocationGroup } from "./groups/location.js" import { IntegrationGroup } from "./groups/integration.js" +import { WebSearchGroup } from "./groups/websearch.js" import { McpGroup } from "./groups/mcp.js" import { CredentialGroup } from "./groups/credential.js" import { ProjectGroup } from "./groups/project.js" @@ -39,6 +40,7 @@ type LocationGroups = | HttpApiGroup.AddMiddleware | HttpApiGroup.AddMiddleware | HttpApiGroup.AddMiddleware + | HttpApiGroup.AddMiddleware | HttpApiGroup.AddMiddleware | HttpApiGroup.AddMiddleware | HttpApiGroup.AddMiddleware @@ -169,6 +171,7 @@ const makeApiFromGroup = < .add(ProjectCopyGroup.middleware(locationMiddleware)) .add(VcsGroup.middleware(locationMiddleware)) .add(DebugGroup) + .add(WebSearchGroup.middleware(locationMiddleware)) .annotateMerge( OpenApi.annotations({ title: "opencode HttpApi", diff --git a/packages/protocol/src/client.ts b/packages/protocol/src/client.ts index e89525c1ded0..24e68e7a516e 100644 --- a/packages/protocol/src/client.ts +++ b/packages/protocol/src/client.ts @@ -45,6 +45,7 @@ export const groupNames = { "server.generate": "generate", "server.provider": "provider", "server.integration": "integration", + "server.websearch": "websearch", "server.credential": "credential", "server.form": "form", "server.permission": "permission", diff --git a/packages/protocol/src/groups/websearch.ts b/packages/protocol/src/groups/websearch.ts new file mode 100644 index 000000000000..0ebf5674df19 --- /dev/null +++ b/packages/protocol/src/groups/websearch.ts @@ -0,0 +1,46 @@ +import { Location } from "@opencode-ai/schema/location" +import { WebSearch } from "@opencode-ai/schema/websearch" +import { Schema } from "effect" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { InvalidRequestError, ServiceUnavailableError } from "../errors.js" +import { LocationQuery, locationQueryOpenApi } from "./location.js" + +export const WebSearchGroup = HttpApiGroup.make("server.websearch") + .add( + HttpApiEndpoint.get("websearch.providers", "/api/websearch/provider", { + query: LocationQuery, + success: Location.response(Schema.Array(WebSearch.Provider)), + error: ServiceUnavailableError, + }) + .annotateMerge(locationQueryOpenApi) + .annotateMerge( + OpenApi.annotations({ + identifier: "v2.websearch.providers", + summary: "List web search providers", + description: "Return the registered web search providers.", + }), + ), + ) + .add( + HttpApiEndpoint.post("websearch.query", "/api/websearch", { + query: LocationQuery, + payload: Schema.Struct(WebSearch.Input.fields), + success: Location.response(WebSearch.Response), + error: [InvalidRequestError, ServiceUnavailableError], + }) + .annotateMerge(locationQueryOpenApi) + .annotateMerge( + OpenApi.annotations({ + identifier: "v2.websearch.query", + summary: "Search the web", + description: + "Run one web search through the selected provider. Specify a provider to override the configured default.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "websearch", + description: "Location-scoped web search routes.", + }), + ) diff --git a/packages/schema/src/event-manifest.ts b/packages/schema/src/event-manifest.ts index 386d750340be..a9c9aaa52e92 100644 --- a/packages/schema/src/event-manifest.ts +++ b/packages/schema/src/event-manifest.ts @@ -36,6 +36,7 @@ import { TuiEvent } from "./tui-event.js" import { VcsEvent } from "./vcs-event.js" import { WorkspaceEvent } from "./workspace-event.js" import { WorktreeEvent } from "./worktree-event.js" +import { WebSearch } from "./websearch.js" const sessionV1DurableDefinitions = SessionV1.Event.Definitions.filter( (definition) => definition.durability === "durable", @@ -67,6 +68,7 @@ const featureDefinitions = Event.inventory( ...Shell.Event.Definitions, ...Question.Event.Definitions, ...Form.Event.Definitions, + ...WebSearch.Event.Definitions, ) export const ServerDefinitions = Event.inventory( diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index 7b09e92426c6..0cbb430add22 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -18,6 +18,7 @@ export { Project } from "./project.js" export { ProjectCopy } from "./project-copy.js" export { Provider } from "./provider.js" export { Reference } from "./reference.js" +export { WebSearch } from "./websearch.js" export { Session } from "./session.js" export { Vcs } from "./vcs.js" export { SessionPending } from "./session-pending.js" diff --git a/packages/schema/src/websearch.ts b/packages/schema/src/websearch.ts new file mode 100644 index 000000000000..832b888dbd10 --- /dev/null +++ b/packages/schema/src/websearch.ts @@ -0,0 +1,42 @@ +export * as WebSearch from "./websearch.js" + +import { Schema } from "effect" +import { ephemeral, inventory } from "./event.js" +import { optional } from "./schema.js" + +export const ID = Schema.String.pipe(Schema.brand("WebSearch.ID")) +export type ID = typeof ID.Type + +export interface Provider extends Schema.Schema.Type {} +export const Provider = Schema.Struct({ + id: ID, + name: Schema.String, +}).annotate({ identifier: "WebSearch.Provider" }) + +export interface Input extends Schema.Schema.Type {} +export const Input = Schema.Struct({ + query: Schema.String, + providerID: ID.pipe(optional), +}).annotate({ identifier: "WebSearch.Input" }) +export type ProviderInput = Pick + +export interface Result extends Schema.Schema.Type {} +export const Result = Schema.Struct({ + url: Schema.String, + title: Schema.String.pipe(optional), + content: Schema.String.pipe(optional), + time: Schema.Struct({ + published: Schema.Finite.pipe(optional), + }), +}).annotate({ identifier: "WebSearch.Result" }) + +export class Response extends Schema.Class("WebSearch.Response")({ + providerID: ID, + results: Schema.Array(Result), +}) {} + +const Updated = ephemeral({ + type: "websearch.updated", + schema: {}, +}) +export const Event = { Updated, Definitions: inventory(Updated) } diff --git a/packages/sdk-next/src/index.ts b/packages/sdk-next/src/index.ts index a9d91eeec3b5..2693877bba5c 100644 --- a/packages/sdk-next/src/index.ts +++ b/packages/sdk-next/src/index.ts @@ -20,6 +20,7 @@ export { Provider } from "@opencode-ai/schema/provider" export { Pty } from "@opencode-ai/schema/pty" export { Question } from "@opencode-ai/schema/question" export { Reference } from "@opencode-ai/schema/reference" +export { WebSearch } from "@opencode-ai/schema/websearch" export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema" export { Session } from "@opencode-ai/schema/session" export { SessionPending } from "@opencode-ai/schema/session-pending" diff --git a/packages/sdk-next/test/contract-identity.test.ts b/packages/sdk-next/test/contract-identity.test.ts index 4aad96f13ab5..0f662af27b0c 100644 --- a/packages/sdk-next/test/contract-identity.test.ts +++ b/packages/sdk-next/test/contract-identity.test.ts @@ -11,6 +11,7 @@ import { Location } from "@opencode-ai/schema/location" import { Model } from "@opencode-ai/schema/model" import { Project } from "@opencode-ai/schema/project" import { Provider } from "@opencode-ai/schema/provider" +import { WebSearch } from "@opencode-ai/schema/websearch" import { Session } from "@opencode-ai/schema/session" import { SessionPending } from "@opencode-ai/schema/session-pending" import { SessionMessage } from "@opencode-ai/schema/session-message" @@ -24,6 +25,7 @@ const SDK = await import("../src/index") test("re-exports canonical contracts directly from Schema", () => { expect(SDK.Agent).toBe(Agent) expect(SDK.Model).toBe(Model) + expect(SDK.WebSearch).toBe(WebSearch) expect(SDK.Session).toBe(Session) expect(Object.keys(SDK).sort()).toEqual([ "AbsolutePath", @@ -52,6 +54,7 @@ test("re-exports canonical contracts directly from Schema", () => { "SessionPending", "Skill", "Tool", + "WebSearch", ]) }) diff --git a/packages/sdk-next/test/embedded.test.ts b/packages/sdk-next/test/embedded.test.ts index 68b8ef8e6cea..bff122bcad70 100644 --- a/packages/sdk-next/test/embedded.test.ts +++ b/packages/sdk-next/test/embedded.test.ts @@ -328,6 +328,38 @@ it.live( 10_000, ) +it.live("embedded client exposes plugin-backed web search", () => + withEmbedded("opencode-embedded-websearch-", (fixture) => + Effect.gen(function* () { + const opencode = yield* fixture.sdk.OpenCode.create() + const providerID = fixture.sdk.WebSearch.ID.make("embedded-websearch") + yield* opencode.plugin({ + id: `embedded-websearch-${crypto.randomUUID()}`, + effect: (ctx) => + ctx.websearch.transform((draft) => { + draft.add({ + id: providerID, + name: "Embedded web search", + execute: (input) => + Effect.succeed([{ url: "https://example.com", content: `Found ${input.query}`, time: {} }]), + }) + }), + }) + + const result = yield* opencode.websearch.query({ + query: "opencode", + providerID, + location: location(fixture), + }) + + expect(result.data).toEqual({ + providerID, + results: [{ url: "https://example.com", content: "Found opencode", time: {} }], + }) + }), + ), +) + it.live( "Location-owned runner events reach the ready global client", () => diff --git a/packages/server/src/handlers.ts b/packages/server/src/handlers.ts index 4b0b4acaa225..a0126ea89e78 100644 --- a/packages/server/src/handlers.ts +++ b/packages/server/src/handlers.ts @@ -21,6 +21,7 @@ import { QuestionHandler } from "./handlers/question" import { ReferenceHandler } from "./handlers/reference" import { LocationHandler } from "./handlers/location" import { IntegrationHandler } from "./handlers/integration" +import { WebSearchHandler } from "./handlers/websearch" import { McpHandler } from "./handlers/mcp" import { CredentialHandler } from "./handlers/credential" import { ProjectHandler } from "./handlers/project" @@ -41,6 +42,7 @@ export const handlers = Layer.mergeAll( GenerateHandler, ProviderHandler, IntegrationHandler, + WebSearchHandler, McpHandler, CredentialHandler, ProjectHandler, diff --git a/packages/server/src/handlers/websearch.ts b/packages/server/src/handlers/websearch.ts new file mode 100644 index 000000000000..8baa385c7048 --- /dev/null +++ b/packages/server/src/handlers/websearch.ts @@ -0,0 +1,68 @@ +import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" +import { WebSearch } from "@opencode-ai/core/websearch" +import { InvalidRequestError, ServiceUnavailableError } from "@opencode-ai/protocol/errors" +import { Effect } from "effect" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import { Api } from "../api" +import { response } from "../location" + +export const WebSearchHandler = HttpApiBuilder.group(Api, "server.websearch", (handlers) => + Effect.gen(function* () { + const awaitPlugins = Effect.fn("server.websearch.awaitPlugins")(function* () { + const plugins = yield* PluginSupervisor.Service + yield* plugins.flush.pipe( + Effect.timeoutOrElse({ + duration: "5 seconds", + orElse: () => + Effect.fail( + new ServiceUnavailableError({ + message: "Web search provider initialization timed out", + service: "websearch", + }), + ), + }), + ) + }) + return handlers + .handle( + "websearch.providers", + Effect.fn("server.websearch.providers")(function* () { + yield* awaitPlugins() + const websearch = yield* WebSearch.Service + return yield* response(websearch.providers()) + }), + ) + .handle( + "websearch.query", + Effect.fn("server.websearch.query")(function* (request) { + yield* awaitPlugins() + const websearch = yield* WebSearch.Service + return yield* response( + websearch.query(request.payload).pipe( + Effect.catchTags({ + "WebSearch.ProviderRequired": () => + new InvalidRequestError({ + message: "Web search provider is required", + kind: "websearch_provider_required", + field: "providerID", + }), + "WebSearch.ProviderNotFound": (error) => + new InvalidRequestError({ + message: `Web search provider not found: ${error.providerID}`, + kind: "websearch_provider_not_found", + field: "providerID", + }), + "WebSearch.Disabled": () => + new InvalidRequestError({ message: "Web search is disabled", kind: "websearch_disabled" }), + "WebSearch.Request": (error) => + new ServiceUnavailableError({ + message: `Web search request failed: ${error.providerID}`, + service: error.providerID, + }), + }), + ), + ) + }), + ) + }), +) diff --git a/packages/tui/src/component/dialog-integration.tsx b/packages/tui/src/component/dialog-integration.tsx index a2c6a531ab13..8a168b457d3f 100644 --- a/packages/tui/src/component/dialog-integration.tsx +++ b/packages/tui/src/component/dialog-integration.tsx @@ -59,29 +59,43 @@ export function connectionSummary(integration: IntegrationInfo) { .join(", ") } -export function DialogIntegration(props: { onConnected?: OnIntegrationConnected } = {}) { +export function DialogIntegration( + props: { onConnected?: OnIntegrationConnected; integrationID?: string; connectionOnly?: boolean } = {}, +) { const data = useData() const dialog = useDialog() const { themeV2 } = useTheme().contextual("elevated") - const options = createMemo(() => - integrationOptions(data.location.integration.list() ?? []).map((integration) => { + const options = createMemo(() => { + const providers = data.location.websearch.list() ?? [] + const providersByID = new Map(providers.map((provider) => [provider.id, provider])) + const integrations = integrationOptions(data.location.integration.list() ?? []).filter( + (integration) => props.integrationID === undefined || integration.id === props.integrationID, + ) + return integrations.map((integration) => { const methods = connectMethods(integration) - const connected = integration.connections.length > 0 + const provider = providersByID.get(integration.id) + const credentials = credentialConnections(integration) + let category = "Services" + if (integration.id in INTEGRATION_PRIORITY) category = "Popular" + if (provider) category = "Web search" return { title: integration.name, value: integration.id, - description: methods.length ? undefined : "Environment only", + description: methods.length === 0 ? "Environment only" : undefined, footer: connectionSummary(integration) || undefined, - category: integration.id in INTEGRATION_PRIORITY ? "Popular" : "Services", - disabled: methods.length === 0, - gutter: connected ? () => : undefined, - onSelect: () => - credentialConnections(integration).length - ? manageConnections(integration, methods, dialog, props.onConnected) - : selectMethod(integration, methods, dialog, props.onConnected), + category, + disabled: methods.length === 0 && credentials.length === 0, + gutter: + integration.connections.length > 0 + ? () => + : undefined, + onSelect: () => { + if (credentials.length) return manageConnections(integration, methods, dialog, props.onConnected) + return selectMethod(integration, methods, dialog, props.onConnected) + }, } - }), - ) + }) + }) return ( @@ -875,6 +877,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ result.location.provider.sync(event.location), ]) break + case "config.updated": + case "websearch.updated": + void result.location.websearch.refresh(event.location) + break // Authenticating an MCP integration reconnects its server, which emits mcp.status.changed, // so the mcp list syncs here rather than off integration.updated. case "mcp.status.changed": @@ -1251,6 +1257,20 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ sync.invalidate(`location.reference:${locationKey(ref ?? defaultLocation())}`) }, }, + websearch: { + list(location?: LocationRef) { + return store.location[locationKey(location ?? defaultLocation())]?.websearch + }, + async refresh(ref?: LocationRef) { + const input = { location: locationQuery(ref ?? defaultLocation()) } + const providers = await client.api.websearch.providers(input) + const key = locationKey(providers.location) + setStore("location", key, { + ...store.location[key], + websearch: providers.data, + }) + }, + }, skill: { list(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.skill diff --git a/packages/tui/test/cli/tui/data.test.tsx b/packages/tui/test/cli/tui/data.test.tsx index 3b4e5569f875..60a557854292 100644 --- a/packages/tui/test/cli/tui/data.test.tsx +++ b/packages/tui/test/cli/tui/data.test.tsx @@ -213,6 +213,8 @@ test("refreshes resources into reactive getters", async () => { location, data: [{ id: "build", request: { headers: {}, body: {} }, mode: "primary", hidden: false, permissions: [] }], }) + if (url.pathname === "/api/websearch/provider") + return json({ location, data: [{ id: "standalone", name: "Standalone" }] }) return undefined }, events) let data!: ReturnType @@ -248,6 +250,7 @@ test("refreshes resources into reactive getters", async () => { await data.session.sync("ses_test") await data.session.message.sync("ses_test") await data.location.agent.sync() + await data.location.websearch.refresh() expect(data.session.get("ses_test")?.title).toBe("Test session") expect(data.session.message.list("ses_test").map((message) => message.id)).toEqual(["msg_first", "msg_second"]) @@ -256,6 +259,7 @@ test("refreshes resources into reactive getters", async () => { expect(app.captureCharFrame()).toContain("msg_second") expect(data.location.default()).toEqual({ directory, workspaceID: undefined }) expect(data.location.agent.list(location)?.map((agent) => agent.id)).toEqual(["build"]) + expect(data.location.websearch.list(location)).toEqual([{ id: "standalone", name: "Standalone" }]) } finally { app.renderer.destroy() } diff --git a/packages/tui/test/fixture/tui-client.ts b/packages/tui/test/fixture/tui-client.ts index e60f79e964fd..5cf10b8ca6fb 100644 --- a/packages/tui/test/fixture/tui-client.ts +++ b/packages/tui/test/fixture/tui-client.ts @@ -125,6 +125,9 @@ export function createFetch(override?: FetchHandler, events?: ReturnType Date: Sat, 25 Jul 2026 23:00:26 -0500 Subject: [PATCH 122/150] fix(core): clarify custom question answers (#38919) --- packages/core/src/tool/question.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/tool/question.ts b/packages/core/src/tool/question.ts index 7a19f711c8d7..1ad52a8f1b1a 100644 --- a/packages/core/src/tool/question.ts +++ b/packages/core/src/tool/question.ts @@ -17,8 +17,8 @@ export const description = `Use this tool when you need to ask the user question 4. Offer choices to the user about what direction to take. Usage notes: -- When \`custom\` is enabled (default), a "Type your own answer" option is added automatically; don't include "Other" or catch-all options -- Answers are returned as arrays of labels; set \`multiple: true\` to allow selecting more than one +- A "Type your own answer" option is added automatically; don't include a separate option for free form answers +- Set \`multiple: true\` to allow selecting more than one option - If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label` export const Input = Schema.Struct({ From 7affee529b9fe0cadfd83bd4f64597d58cd1c256 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 25 Jul 2026 23:23:53 -0500 Subject: [PATCH 123/150] fix(core): harden grep search behavior (#38922) --- packages/core/src/tool/grep.ts | 26 +++++++---- packages/core/test/tool-search.test.ts | 64 +++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/packages/core/src/tool/grep.ts b/packages/core/src/tool/grep.ts index fa7326302d81..0f8a2d60d3b0 100644 --- a/packages/core/src/tool/grep.ts +++ b/packages/core/src/tool/grep.ts @@ -15,7 +15,9 @@ import { Tool } from "./tool" export const name = "grep" export const Input = Schema.Struct({ - pattern: FileSystem.GrepInput.fields.pattern.annotate({ + pattern: FileSystem.GrepInput.fields.pattern.check( + Schema.isMinLength(1, { message: "Pattern must not be empty" }), + ).annotate({ description: "Regex pattern to search for in file contents", }), path: RelativePath.pipe(Schema.optional).annotate({ @@ -33,7 +35,7 @@ export const Output = Schema.Array(FileSystem.Match) type ModelOutput = typeof Output.Encoded /** Format raw search matches into the familiar concise model output. */ -export const toModelOutput = (output: ModelOutput) => { +export const toModelOutput = (output: ModelOutput, truncated = false) => { const lines = output.length === 0 ? ["No files found"] : [`Found ${output.length} matches`] let current = "" for (const match of output) { @@ -44,6 +46,11 @@ export const toModelOutput = (output: ModelOutput) => { } lines.push(` Line ${match.line}: ${match.text}`) } + if (truncated) + lines.push( + "", + `(Results are truncated: showing first ${output.length} results. Consider using a more specific path or pattern.)`, + ) return lines.join("\n") } @@ -89,13 +96,14 @@ export const Plugin = { Effect.fail(new ToolFailure({ message: `Search path does not exist: ${input.path ?? "."}` })), ), ) - return yield* ripgrep + const limit = input.limit ?? FileSystem.DEFAULT_SEARCH_LIMIT + const matches = yield* ripgrep .grep({ cwd: info?.type === "Directory" ? target : path.dirname(target), pattern: input.pattern, file: info?.type === "File" ? path.basename(target) : undefined, include: input.include, - limit: input.limit ?? FileSystem.DEFAULT_SEARCH_LIMIT, + limit: limit + 1, }) .pipe( Effect.map((result) => @@ -118,16 +126,18 @@ export const Plugin = { ), ), ) + return { matches: matches.slice(0, limit), truncated: matches.length > limit } }).pipe( - Effect.map((output) => ({ - output, + Effect.map((result) => ({ + output: result.matches, content: toModelOutput( - output.map((match) => ({ + result.matches.map((match) => ({ ...match, entry: { ...match.entry, path: path.resolve(location.directory, match.entry.path) }, })), + result.truncated, ), - metadata: { matches: output.length }, + metadata: { matches: result.matches.length, truncated: result.truncated }, })), Effect.mapError((error) => error instanceof ToolFailure diff --git a/packages/core/test/tool-search.test.ts b/packages/core/test/tool-search.test.ts index b9da2007639d..5318e5279801 100644 --- a/packages/core/test/tool-search.test.ts +++ b/packages/core/test/tool-search.test.ts @@ -104,7 +104,7 @@ describe("search tools", () => { const grep = yield* executeTool(registry, call("grep", { pattern: "needle" })) expect(glob.metadata).toEqual({ count: FileSystem.DEFAULT_SEARCH_LIMIT, truncated: true }) - expect(grep.metadata).toEqual({ matches: FileSystem.DEFAULT_SEARCH_LIMIT }) + expect(grep.metadata).toEqual({ matches: FileSystem.DEFAULT_SEARCH_LIMIT, truncated: true }) expect(glob.content).toHaveLength(1) expect(grep.content).toHaveLength(1) const globText = glob.content?.[0]?.type === "text" ? glob.content[0].text : "" @@ -114,6 +114,9 @@ describe("search tools", () => { `(Results are truncated: showing first ${FileSystem.DEFAULT_SEARCH_LIMIT} results. Consider using a more specific path or pattern.)`, ) expect(grepText).toStartWith(`Found ${FileSystem.DEFAULT_SEARCH_LIMIT} matches\n`) + expect(grepText).toEndWith( + `(Results are truncated: showing first ${FileSystem.DEFAULT_SEARCH_LIMIT} results. Consider using a more specific path or pattern.)`, + ) }), ) }), @@ -121,6 +124,65 @@ describe("search tools", () => { ), ) + it.live("rejects an empty grep pattern", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => + withTools(tmp.path, (registry) => + Effect.gen(function* () { + expect(yield* executeTool(registry, call("grep", { pattern: "" }))).toEqual({ + status: "error", + error: { + type: "tool.execution", + message: 'Invalid tool input: Pattern must not be empty\n at ["pattern"]', + }, + }) + }), + ), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("handles explicit grep file and directory paths", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => + Effect.promise(() => + Promise.all([ + fs.writeFile(path.join(tmp.path, "target.txt"), "needle\n"), + fs.writeFile(path.join(tmp.path, "other.txt"), "needle\n"), + ]), + ).pipe( + Effect.andThen( + withTools(tmp.path, (registry) => + Effect.gen(function* () { + const file = yield* executeTool(registry, call("grep", { path: "target.txt", pattern: "needle" })) + expect(file).toMatchObject({ + status: "completed", + output: [{ entry: { path: "target.txt" }, line: 1, text: "needle\n" }], + metadata: { matches: 1, truncated: false }, + }) + + const directory = yield* executeTool(registry, call("grep", { path: ".", pattern: "needle" })) + expect(directory).toMatchObject({ + status: "completed", + metadata: { matches: 2, truncated: false }, + }) + if (directory.status !== "completed") return + expect(directory.output).toEqual( + expect.arrayContaining([ + expect.objectContaining({ entry: expect.objectContaining({ path: "target.txt" }) }), + expect.objectContaining({ entry: expect.objectContaining({ path: "other.txt" }) }), + ]), + ) + }), + ), + ), + ), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + for (const name of ["glob", "grep"] as const) { it.live(`${name} reports a missing search path`, () => Effect.acquireUseRelease( From 28f4284bd77d8c95d8e2f98f8e614f6588ebbe76 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 26 Jul 2026 02:38:49 -0400 Subject: [PATCH 124/150] fix(www): canonicalize production routes --- packages/www/wrangler.jsonc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/www/wrangler.jsonc b/packages/www/wrangler.jsonc index 24624114e9ea..a793b92e14af 100644 --- a/packages/www/wrangler.jsonc +++ b/packages/www/wrangler.jsonc @@ -7,7 +7,7 @@ "assets": { "binding": "ASSETS", "directory": "./dist/client", - "html_handling": "auto-trailing-slash", + "html_handling": "drop-trailing-slash", "not_found_handling": "404-page" }, "env": { @@ -21,7 +21,7 @@ ] }, "production": { - "name": "opencode-www", + "name": "opencode-www-production", "routes": [ { "pattern": "opencode.ai/v2*", From 80865407e084a20d1e8677ac12ad2cc1d7767a4a Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 26 Jul 2026 02:47:43 -0400 Subject: [PATCH 125/150] refactor(sdk): remove local legacy package --- .changeset/bright-sols-write.md | 5 - .changeset/calm-services-start.md | 7 - .changeset/calm-sessions-header.md | 5 - .changeset/canonical-tool-results.md | 8 - .changeset/clean-sessions-generate.md | 7 - .changeset/fresh-composers-slot.md | 5 - .changeset/quick-caches-roll.md | 5 - bun.lock | 91 +- bunfig.toml | 2 +- github/package.json | 2 +- package.json | 3 +- packages/app/package.json | 2 +- .../console/app/src/routes/openapi.json.ts | 2 +- packages/plugin/package.json | 2 +- packages/protocol/openapi.json | 29592 +++++++++++++ packages/protocol/package.json | 2 + packages/protocol/script/generate-openapi.ts | 16 + packages/sdk/.gitignore | 10 - packages/sdk/js/example/example.ts | 56 - packages/sdk/js/package.json | 35 - packages/sdk/js/script/build.ts | 424 - packages/sdk/js/script/publish.ts | 45 - packages/sdk/js/src/client.ts | 57 - packages/sdk/js/src/error-interceptor.ts | 51 - packages/sdk/js/src/gen/client.gen.ts | 22 - packages/sdk/js/src/gen/client/client.gen.ts | 212 - packages/sdk/js/src/gen/client/index.ts | 25 - packages/sdk/js/src/gen/client/types.gen.ts | 222 - packages/sdk/js/src/gen/client/utils.gen.ts | 287 - packages/sdk/js/src/gen/core/auth.gen.ts | 41 - .../sdk/js/src/gen/core/bodySerializer.gen.ts | 74 - packages/sdk/js/src/gen/core/params.gen.ts | 144 - .../sdk/js/src/gen/core/pathSerializer.gen.ts | 167 - .../js/src/gen/core/queryKeySerializer.gen.ts | 111 - .../js/src/gen/core/serverSentEvents.gen.ts | 210 - packages/sdk/js/src/gen/core/types.gen.ts | 91 - packages/sdk/js/src/gen/core/utils.gen.ts | 109 - packages/sdk/js/src/gen/sdk.gen.ts | 1184 - packages/sdk/js/src/gen/types.gen.ts | 3843 -- packages/sdk/js/src/index.ts | 21 - packages/sdk/js/src/process.ts | 31 - packages/sdk/js/src/server.ts | 134 - packages/sdk/js/src/v2/client.ts | 97 - packages/sdk/js/src/v2/data.ts | 32 - packages/sdk/js/src/v2/gen/client.gen.ts | 18 - .../sdk/js/src/v2/gen/client/client.gen.ts | 285 - packages/sdk/js/src/v2/gen/client/index.ts | 25 - .../sdk/js/src/v2/gen/client/types.gen.ts | 202 - .../sdk/js/src/v2/gen/client/utils.gen.ts | 294 - packages/sdk/js/src/v2/gen/core/auth.gen.ts | 41 - .../js/src/v2/gen/core/bodySerializer.gen.ts | 82 - packages/sdk/js/src/v2/gen/core/params.gen.ts | 169 - .../js/src/v2/gen/core/pathSerializer.gen.ts | 167 - .../src/v2/gen/core/queryKeySerializer.gen.ts | 111 - .../src/v2/gen/core/serverSentEvents.gen.ts | 239 - packages/sdk/js/src/v2/gen/core/types.gen.ts | 86 - packages/sdk/js/src/v2/gen/core/utils.gen.ts | 137 - packages/sdk/js/src/v2/gen/sdk.gen.ts | 8828 ---- packages/sdk/js/src/v2/gen/types.gen.ts | 19353 -------- packages/sdk/js/src/v2/index.ts | 23 - packages/sdk/js/src/v2/server.ts | 134 - packages/sdk/js/sst-env.d.ts | 10 - packages/sdk/js/test/session-history.test.ts | 12 - packages/sdk/js/tsconfig.json | 14 - packages/sdk/openapi.json | 36699 ---------------- packages/session-ui/package.json | 2 +- packages/slack/package.json | 2 +- packages/www/openapi.json | 14877 ++++--- packages/www/package.json | 4 +- packages/www/public/openapi.json | 14877 ++++--- packages/www/script/generate-openapi.ts | 18 + script/generate.ts | 4 +- script/publish.ts | 4 - script/raw-changelog.ts | 4 +- 74 files changed, 46983 insertions(+), 87229 deletions(-) delete mode 100644 .changeset/bright-sols-write.md delete mode 100644 .changeset/calm-services-start.md delete mode 100644 .changeset/calm-sessions-header.md delete mode 100644 .changeset/canonical-tool-results.md delete mode 100644 .changeset/clean-sessions-generate.md delete mode 100644 .changeset/fresh-composers-slot.md delete mode 100644 .changeset/quick-caches-roll.md create mode 100644 packages/protocol/openapi.json create mode 100644 packages/protocol/script/generate-openapi.ts delete mode 100644 packages/sdk/.gitignore delete mode 100644 packages/sdk/js/example/example.ts delete mode 100644 packages/sdk/js/package.json delete mode 100755 packages/sdk/js/script/build.ts delete mode 100755 packages/sdk/js/script/publish.ts delete mode 100644 packages/sdk/js/src/client.ts delete mode 100644 packages/sdk/js/src/error-interceptor.ts delete mode 100644 packages/sdk/js/src/gen/client.gen.ts delete mode 100644 packages/sdk/js/src/gen/client/client.gen.ts delete mode 100644 packages/sdk/js/src/gen/client/index.ts delete mode 100644 packages/sdk/js/src/gen/client/types.gen.ts delete mode 100644 packages/sdk/js/src/gen/client/utils.gen.ts delete mode 100644 packages/sdk/js/src/gen/core/auth.gen.ts delete mode 100644 packages/sdk/js/src/gen/core/bodySerializer.gen.ts delete mode 100644 packages/sdk/js/src/gen/core/params.gen.ts delete mode 100644 packages/sdk/js/src/gen/core/pathSerializer.gen.ts delete mode 100644 packages/sdk/js/src/gen/core/queryKeySerializer.gen.ts delete mode 100644 packages/sdk/js/src/gen/core/serverSentEvents.gen.ts delete mode 100644 packages/sdk/js/src/gen/core/types.gen.ts delete mode 100644 packages/sdk/js/src/gen/core/utils.gen.ts delete mode 100644 packages/sdk/js/src/gen/sdk.gen.ts delete mode 100644 packages/sdk/js/src/gen/types.gen.ts delete mode 100644 packages/sdk/js/src/index.ts delete mode 100644 packages/sdk/js/src/process.ts delete mode 100644 packages/sdk/js/src/server.ts delete mode 100644 packages/sdk/js/src/v2/client.ts delete mode 100644 packages/sdk/js/src/v2/data.ts delete mode 100644 packages/sdk/js/src/v2/gen/client.gen.ts delete mode 100644 packages/sdk/js/src/v2/gen/client/client.gen.ts delete mode 100644 packages/sdk/js/src/v2/gen/client/index.ts delete mode 100644 packages/sdk/js/src/v2/gen/client/types.gen.ts delete mode 100644 packages/sdk/js/src/v2/gen/client/utils.gen.ts delete mode 100644 packages/sdk/js/src/v2/gen/core/auth.gen.ts delete mode 100644 packages/sdk/js/src/v2/gen/core/bodySerializer.gen.ts delete mode 100644 packages/sdk/js/src/v2/gen/core/params.gen.ts delete mode 100644 packages/sdk/js/src/v2/gen/core/pathSerializer.gen.ts delete mode 100644 packages/sdk/js/src/v2/gen/core/queryKeySerializer.gen.ts delete mode 100644 packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts delete mode 100644 packages/sdk/js/src/v2/gen/core/types.gen.ts delete mode 100644 packages/sdk/js/src/v2/gen/core/utils.gen.ts delete mode 100644 packages/sdk/js/src/v2/gen/sdk.gen.ts delete mode 100644 packages/sdk/js/src/v2/gen/types.gen.ts delete mode 100644 packages/sdk/js/src/v2/index.ts delete mode 100644 packages/sdk/js/src/v2/server.ts delete mode 100644 packages/sdk/js/sst-env.d.ts delete mode 100644 packages/sdk/js/test/session-history.test.ts delete mode 100644 packages/sdk/js/tsconfig.json delete mode 100644 packages/sdk/openapi.json create mode 100644 packages/www/script/generate-openapi.ts diff --git a/.changeset/bright-sols-write.md b/.changeset/bright-sols-write.md deleted file mode 100644 index b008ad247789..000000000000 --- a/.changeset/bright-sols-write.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@opencode-ai/ai": patch ---- - -Report OpenAI prompt cache write tokens in normalized usage. diff --git a/.changeset/calm-services-start.md b/.changeset/calm-services-start.md deleted file mode 100644 index 94b57738d8a0..000000000000 --- a/.changeset/calm-services-start.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@opencode-ai/client": patch -"@opencode-ai/protocol": patch -"@opencode-ai/cli": patch ---- - -Expose background-service lifecycle status, preserve one process-held owner through startup and failure, reconnect TUIs without activating replacement, and stop exact service instances gracefully. diff --git a/.changeset/calm-sessions-header.md b/.changeset/calm-sessions-header.md deleted file mode 100644 index 666393c22e65..000000000000 --- a/.changeset/calm-sessions-header.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@opencode-ai/cli": patch ---- - -Expose a TUI plugin slot at the top of the session view. diff --git a/.changeset/canonical-tool-results.md b/.changeset/canonical-tool-results.md deleted file mode 100644 index b484966033d4..000000000000 --- a/.changeset/canonical-tool-results.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@opencode-ai/plugin": minor -"@opencode-ai/sdk": minor -"@opencode-ai/client": minor -"@opencode-ai/protocol": minor ---- - -Replace the V2 tool result model with one canonical representation per fact. Tools lose `structured`, projection callbacks, the `Structured` generic, and the exported `Tool.settle` interpreter; tool responses carry schema-validated `output`, model-visible `content`, and optional compact JSON `metadata`. Code Mode receives the validated encoded output. Durable tool success stores non-empty model content plus optional metadata; failure stores one error plus the final bounded partial snapshot. Progress carries metadata only, while `execute.after` hooks receive the canonical terminal outcome and managed `outputPaths`. A one-time migration rewrites existing projected tool rows and moves provider-hosted result payloads into provider-owned result state. diff --git a/.changeset/clean-sessions-generate.md b/.changeset/clean-sessions-generate.md deleted file mode 100644 index c7f0d2c909a5..000000000000 --- a/.changeset/clean-sessions-generate.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@opencode-ai/client": patch -"@opencode-ai/plugin": patch -"@opencode-ai/protocol": patch ---- - -Expose transient, read-only session generation through the HTTP API, generated clients, and V2 plugin session context. diff --git a/.changeset/fresh-composers-slot.md b/.changeset/fresh-composers-slot.md deleted file mode 100644 index 9c6c47b3c60d..000000000000 --- a/.changeset/fresh-composers-slot.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@opencode-ai/cli": patch ---- - -Expose a TUI plugin slot above the session composer. diff --git a/.changeset/quick-caches-roll.md b/.changeset/quick-caches-roll.md deleted file mode 100644 index 3ab9e9b740a9..000000000000 --- a/.changeset/quick-caches-roll.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@opencode-ai/ai": patch ---- - -Improve Anthropic and Bedrock prompt reuse with layered cache breakpoints that roll through long tool loops. diff --git a/bun.lock b/bun.lock index 56620c0a20fe..6b08db9e5cf9 100644 --- a/bun.lock +++ b/bun.lock @@ -8,7 +8,7 @@ "@aws-sdk/client-s3": "3.933.0", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", - "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/sdk": "1.18.5", "heap-snapshot-toolkit": "1.1.3", "typescript": "catalog:", }, @@ -64,7 +64,7 @@ "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/schema": "workspace:*", - "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/sdk": "1.18.5", "@opencode-ai/session-ui": "workspace:*", "@opencode-ai/ui": "workspace:*", "@pierre/trees": "1.0.0-beta.4", @@ -582,7 +582,7 @@ "@opencode-ai/ai": "workspace:*", "@opencode-ai/client": "workspace:*", "@opencode-ai/schema": "workspace:*", - "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/sdk": "1.18.5", "@standard-schema/spec": "^1.1.0", "effect": "catalog:", "zod": "catalog:", @@ -663,21 +663,6 @@ "@typescript/native-preview": "catalog:", }, }, - "packages/sdk/js": { - "name": "@opencode-ai/sdk", - "version": "1.18.4", - "dependencies": { - "cross-spawn": "catalog:", - }, - "devDependencies": { - "@hey-api/openapi-ts": "0.90.10", - "@tsconfig/node22": "catalog:", - "@types/cross-spawn": "catalog:", - "@types/node": "catalog:", - "@typescript/native-preview": "catalog:", - "typescript": "catalog:", - }, - }, "packages/server": { "name": "@opencode-ai/server", "version": "1.18.4", @@ -703,7 +688,7 @@ "@kobalte/core": "catalog:", "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13.tgz", "@opencode-ai/core": "workspace:*", - "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/sdk": "1.18.5", "@opencode-ai/ui": "workspace:*", "@pierre/diffs": "catalog:", "@shikijs/stream": "catalog:", @@ -767,7 +752,7 @@ "name": "@opencode-ai/slack", "version": "1.18.4", "dependencies": { - "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/sdk": "1.18.5", "@slack/bolt": "^3.17.1", }, "devDependencies": { @@ -1716,14 +1701,6 @@ "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="], - "@hey-api/codegen-core": ["@hey-api/codegen-core@0.5.5", "", { "dependencies": { "@hey-api/types": "0.1.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-f2ZHucnA2wBGAY8ipB4wn/mrEYW+WUxU2huJmUvfDO6AE2vfILSHeF3wCO39Pz4wUYPoAWZByaauftLrOfC12Q=="], - - "@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.2.2", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.1", "lodash": "^4.17.21" } }, "sha512-oS+5yAdwnK20lSeFO1d53Ku+yaGCsY8PcrmSq2GtSs3bsBfRnHAbpPKSVzQcaxAOrzj5NB+f34WhZglVrNayBA=="], - - "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.90.10", "", { "dependencies": { "@hey-api/codegen-core": "^0.5.5", "@hey-api/json-schema-ref-parser": "1.2.2", "@hey-api/types": "0.1.2", "ansi-colors": "4.1.3", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.3" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-o0wlFxuLt1bcyIV/ZH8DQ1wrgODTnUYj/VfCHOOYgXUQlLp9Dm2PjihOz+WYrZLowhqUhSKeJRArOGzvLuOTsg=="], - - "@hey-api/types": ["@hey-api/types@0.1.2", "", {}, "sha512-uNNtiVAWL7XNrV/tFXx7GLY9lwaaDazx1173cGW3+UEaw4RUPsHEmiB4DSpcjNxMIcrctfz2sGKLnVx5PBG2RA=="], - "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], "@hono/standard-validator": ["@hono/standard-validator@0.2.0", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-pFq0UVAnjzXcDAgqFpDeVL3MOUPrlIh/kPqBDvbCYoThVhhS+Vf37VcdsakdOFFGiqoiYVxp3LifXFhGhp/rgQ=="], @@ -1814,8 +1791,6 @@ "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], - "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], - "@jsx-email/all": ["@jsx-email/all@2.2.3", "", { "dependencies": { "@jsx-email/body": "1.0.2", "@jsx-email/button": "1.0.4", "@jsx-email/column": "1.0.3", "@jsx-email/container": "1.0.2", "@jsx-email/font": "1.0.3", "@jsx-email/head": "1.0.2", "@jsx-email/heading": "1.0.2", "@jsx-email/hr": "1.0.2", "@jsx-email/html": "1.0.2", "@jsx-email/img": "1.0.2", "@jsx-email/link": "1.0.2", "@jsx-email/markdown": "2.0.4", "@jsx-email/preview": "1.0.2", "@jsx-email/render": "1.1.1", "@jsx-email/row": "1.0.2", "@jsx-email/section": "1.0.2", "@jsx-email/tailwind": "2.4.4", "@jsx-email/text": "1.0.2" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-OBvLe/hVSQc0LlMSTJnkjFoqs3bmxcC4zpy/5pT5agPCSKMvAKQjzmsc2xJ2wO73jSpRV1K/g38GmvdCfrhSoQ=="], "@jsx-email/body": ["@jsx-email/body@1.0.2", "", { "peerDependencies": { "react": "^18.2.0" } }, "sha512-NjR2tgLH4XGfGkm+O8kcVwi9MBqZsXZCLlmk3HlMux3/n/+a5zB+yhJqXWZBJl2i+6cSF+E2O6hK11ekyK9WWQ=="], @@ -2074,7 +2049,7 @@ "@opencode-ai/script": ["@opencode-ai/script@workspace:packages/script"], - "@opencode-ai/sdk": ["@opencode-ai/sdk@workspace:packages/sdk/js"], + "@opencode-ai/sdk": ["@opencode-ai/sdk@1.18.5", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-7KgMvP5/1oxbhHj6kYBtPSTEdFKYpUeEYOzBTKdzSaRpapUpFFdn6Hkus3rr0rljO0kukWZIgRd3DrVBwTULGA=="], "@opencode-ai/sdk-next": ["@opencode-ai/sdk-next@workspace:packages/sdk-next"], @@ -3308,8 +3283,6 @@ "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], - "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], - "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -3500,8 +3473,6 @@ "bytestreamjs": ["bytestreamjs@2.0.1", "", {}, "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ=="], - "c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="], - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], "cacache": ["cacache@20.0.4", "", { "dependencies": { "@npmcli/fs": "^5.0.0", "fs-minipass": "^3.0.0", "glob": "^13.0.0", "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^13.0.0" } }, "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA=="], @@ -3596,13 +3567,11 @@ "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], - "color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="], - "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], + "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="], @@ -3616,8 +3585,6 @@ "conf": ["conf@15.1.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^10.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.5.0" } }, "sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og=="], - "confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], - "config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="], "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], @@ -4006,8 +3973,6 @@ "expressive-code": ["expressive-code@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7", "@expressive-code/plugin-frames": "^0.41.7", "@expressive-code/plugin-shiki": "^0.41.7", "@expressive-code/plugin-text-markers": "^0.41.7" } }, "sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA=="], - "exsolve": ["exsolve@1.1.0", "", {}, "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw=="], - "ext-list": ["ext-list@2.2.2", "", { "dependencies": { "mime-db": "^1.28.0" } }, "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA=="], "ext-name": ["ext-name@5.0.0", "", { "dependencies": { "ext-list": "^2.0.0", "sort-keys-length": "^1.0.0" } }, "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ=="], @@ -4140,8 +4105,6 @@ "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#83c0a07", {}, "anomalyco-ghostty-web-83c0a07", "sha512-Lf2v1agHkVUpMpHBWWuCZrhOEmcwwin5/Hboc9rZwQ7/CKkIh5rU1r1CvfLlhkMoFv+ed8z52RZ8hkzGZZj3MQ=="], - "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], - "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], "gitlab-ai-provider": ["gitlab-ai-provider@6.11.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-SJ6f5qa7P8md6lPrserryER3zerLkrezlnqqYQ2AbvDPpHLbwtbyk0FYJ5kNRcmbI80i/VMcsMBP0YIRdc3ucQ=="], @@ -4374,8 +4337,6 @@ "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], - "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], - "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], @@ -4886,8 +4847,6 @@ "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - "nypm": ["nypm@0.6.8", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.2.4" }, "bin": { "nypm": "./dist/cli.mjs" } }, "sha512-Q9K4Diu6l5u6xJQogeFSs/zKtyMSgFKFtRQV+tHP4kL7KPm2grpBU0dFIwFaXwNxN0MtfKWc43VpCugAa+LPsw=="], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], @@ -5024,8 +4983,6 @@ "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], - "perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], - "piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -5040,8 +4997,6 @@ "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], - "pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], - "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="], "pkijs": ["pkijs@3.4.0", "", { "dependencies": { "@noble/hashes": "1.4.0", "asn1js": "^3.0.6", "bytestreamjs": "^2.0.1", "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw=="], @@ -5078,8 +5033,6 @@ "postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="], - "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], - "preact": ["preact@11.0.0-beta.0", "", {}, "sha512-IcODoASASYwJ9kxz7+MJeiJhvLriwSb4y4mHIyxdgaRZp6kPUud7xytrk/6GZw8U3y6EFJaRb5wi9SrEK+8+lg=="], "preact-render-to-string": ["preact-render-to-string@6.6.5", "", { "peerDependencies": { "preact": ">=10 || >= 11.0.0-0" } }, "sha512-O6MHzYNIKYaiSX3bOw0gGZfEbOmlIDtDfWwN1JJdc/T3ihzRT6tGGSEWE088dWrEDGa1u7101q+6fzQnO9XCPA=="], @@ -5152,8 +5105,6 @@ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], - "react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="], "react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], @@ -5876,7 +5827,7 @@ "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], - "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], + "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], "xdg-app-paths": ["xdg-app-paths@5.5.1", "", { "dependencies": { "os-paths": "^4.0.1", "xdg-portable": "^7.2.0" } }, "sha512-hI3flOB4PLZIy5prbtTpirobtPE2ZtZ52szO+2mM9Efp6ErM398La+C1lIpNWDfNoQk+6Lsi6nMcCwVB7pxeMQ=="], @@ -6224,12 +6175,6 @@ "@expressive-code/plugin-shiki/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], - "@hey-api/json-schema-ref-parser/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], - - "@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], - - "@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], "@jsx-email/cli/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -6604,10 +6549,6 @@ "builder-util/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], - "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - - "c12/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], - "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -6724,8 +6665,6 @@ "jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], - "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], @@ -6764,10 +6703,6 @@ "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], - "nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], - - "nypm/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], - "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], @@ -7222,8 +7157,6 @@ "@expressive-code/plugin-shiki/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], - "@hey-api/json-schema-ref-parser/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "@jsx-email/cli/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], "@jsx-email/cli/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], @@ -7714,8 +7647,6 @@ "blume/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "blume/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], - "blume/node-html-parser/entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="], "blume/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -7734,8 +7665,6 @@ "builder-util/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -7802,8 +7731,6 @@ "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "mermaid/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], - "motion/framer-motion/motion-dom": ["motion-dom@12.42.2", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA=="], "motion/framer-motion/motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], @@ -7822,8 +7749,6 @@ "rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "storybook/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], - "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "temp/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], diff --git a/bunfig.toml b/bunfig.toml index c506ff57c4bf..8393dd5093f8 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -2,7 +2,7 @@ exact = true # Only install newly resolved package versions published at least 3 days ago. minimumReleaseAge = 259200 -minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish"] +minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@opencode-ai/sdk", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish"] [test] root = "./do-not-run-tests-from-root" diff --git a/github/package.json b/github/package.json index e1b913abedcc..a7af1d253284 100644 --- a/github/package.json +++ b/github/package.json @@ -15,6 +15,6 @@ "@actions/github": "6.0.1", "@octokit/graphql": "9.0.1", "@octokit/rest": "catalog:", - "@opencode-ai/sdk": "workspace:*" + "@opencode-ai/sdk": "1.18.5" } } diff --git a/package.json b/package.json index 6332861c9156..3ff72db18e8c 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "packages/*", "packages/console/*", "packages/stats/*", - "packages/sdk/js", "packages/slack" ], "catalog": { @@ -124,7 +123,7 @@ "@aws-sdk/client-s3": "3.933.0", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", - "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/sdk": "1.18.5", "heap-snapshot-toolkit": "1.1.3", "typescript": "catalog:" }, diff --git a/packages/app/package.json b/packages/app/package.json index f6a1bf9d2ea7..05606904810e 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -56,7 +56,7 @@ "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/schema": "workspace:*", - "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/sdk": "1.18.5", "@opencode-ai/session-ui": "workspace:*", "@opencode-ai/ui": "workspace:*", "@pierre/trees": "1.0.0-beta.4", diff --git a/packages/console/app/src/routes/openapi.json.ts b/packages/console/app/src/routes/openapi.json.ts index 2789e85c49e1..3de92f74af48 100644 --- a/packages/console/app/src/routes/openapi.json.ts +++ b/packages/console/app/src/routes/openapi.json.ts @@ -1,6 +1,6 @@ export async function GET() { const response = await fetch( - "https://raw.githubusercontent.com/anomalyco/opencode/refs/heads/dev/packages/sdk/openapi.json", + "https://raw.githubusercontent.com/anomalyco/opencode/refs/heads/dev/packages/protocol/openapi.json", ) const json = await response.json() return json diff --git a/packages/plugin/package.json b/packages/plugin/package.json index ca64df1357ea..942de6460a6a 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -28,7 +28,7 @@ "@opencode-ai/ai": "workspace:*", "@opencode-ai/client": "workspace:*", "@opencode-ai/schema": "workspace:*", - "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/sdk": "1.18.5", "@standard-schema/spec": "^1.1.0", "effect": "catalog:", "zod": "catalog:" diff --git a/packages/protocol/openapi.json b/packages/protocol/openapi.json new file mode 100644 index 000000000000..1c8b3dc9412d --- /dev/null +++ b/packages/protocol/openapi.json @@ -0,0 +1,29592 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "opencode HttpApi", + "version": "0.0.1", + "description": "Experimental HttpApi surface for selected instance routes." + }, + "paths": { + "/api/health": { + "get": { + "tags": [ + "health" + ], + "operationId": "v2.health.get", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "ServiceHealth", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceHealth" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Report the owning server process and its application status.", + "summary": "Check server health" + } + }, + "/api/service/stop": { + "post": { + "tags": [ + "health" + ], + "operationId": "v2.health.stop", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "ServiceStopResponse", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceStopResponse" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Request graceful shutdown of one exact managed server instance.", + "summary": "Stop the managed server", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceStopRequest" + } + } + }, + "required": true + } + } + }, + "/api/server": { + "get": { + "tags": [ + "server" + ], + "operationId": "v2.server.get", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "urls": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "urls" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Return the URLs that can be used to connect to this server.", + "summary": "Get server information" + } + }, + "/api/location": { + "get": { + "tags": [ + "location" + ], + "operationId": "v2.location.get", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Location.Info", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Location.Info" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Resolve the requested location or the server default location.", + "summary": "Get location" + } + }, + "/api/agent": { + "get": { + "tags": [ + "agent" + ], + "operationId": "v2.agent.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Agent.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve currently registered agents.", + "summary": "List agents" + } + }, + "/api/plugin": { + "get": { + "tags": [ + "plugin" + ], + "operationId": "v2.plugin.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Plugin.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve currently loaded plugins.", + "summary": "List plugins" + } + }, + "/api/session": { + "get": { + "tags": [ + "session" + ], + "operationId": "v2.session.list", + "parameters": [ + { + "name": "workspace", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^wrk" + } + ] + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "limit", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Maximum number of sessions to return. Defaults to the newest 50 sessions." + }, + "required": false + }, + { + "name": "order", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + { + "type": "null" + } + ], + "description": "Session order for the first page. Use desc for newest first or asc for oldest first." + }, + "required": false + }, + { + "name": "search", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "parentID", + "in": "query", + "schema": { + "anyOf": [ + { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + { + "type": "string", + "enum": [ + "null" + ] + } + ], + "description": "Filter by parent session. Use null to return only root sessions." + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "directory", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "project", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "subpath", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "cursor", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string", + "description": "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response." + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "SessionsResponse", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionsResponse" + } + } + } + }, + "400": { + "description": "InvalidCursorError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidCursorError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list.", + "summary": "List sessions" + }, + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.create", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Session.Info" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Create a session at the requested location.", + "summary": "Create session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + { + "type": "null" + } + ] + }, + "agent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/Model.Ref" + }, + { + "type": "null" + } + ] + }, + "location": { + "anyOf": [ + { + "$ref": "#/components/schemas/Location.Ref" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/active": { + "get": { + "tags": [ + "session" + ], + "operationId": "v2.session.active", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "patternProperties": { + "^ses": { + "$ref": "#/components/schemas/SessionActive" + } + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve foreground Session drains currently owned by this OpenCode process. Sessions absent from the result are inactive.", + "summary": "List active sessions" + } + }, + "/api/session/{sessionID}": { + "get": { + "tags": [ + "session" + ], + "operationId": "v2.session.get", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Session.Info" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve a session by ID.", + "summary": "Get session" + }, + "delete": { + "tags": [ + "session" + ], + "operationId": "v2.session.remove", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Delete a session and its child sessions.", + "summary": "Delete session" + } + }, + "/api/session/{sessionID}/fork": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.fork", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Session.Info" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | MessageNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/MessageNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Create a child session by copying projected history from the parent. When messageID is supplied, copy messages before that boundary.", + "summary": "Fork session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messageID": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/agent": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.switchAgent", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Switch the agent used by subsequent provider turns.", + "summary": "Switch session agent", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agent": { + "type": "string" + } + }, + "required": [ + "agent" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/model": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.switchModel", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Switch the model used by subsequent provider turns.", + "summary": "Switch session model", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "model": { + "$ref": "#/components/schemas/Model.Ref" + } + }, + "required": [ + "model" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/rename": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.rename", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Update the session title.", + "summary": "Rename session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/move": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.move", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Move a session to another project directory, optionally transferring local changes.", + "summary": "Move session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Location.Ref" + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/prompt": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.prompt", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SessionPending.User" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictError" + } + } + } + } + }, + "description": "Durably admit one session input and schedule agent-loop execution unless resume is false.", + "summary": "Send message", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] + }, + "text": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PromptInput.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + }, + "metadata": { + "type": "object" + }, + "delivery": { + "anyOf": [ + { + "type": "string", + "enum": [ + "steer", + "queue" + ] + }, + { + "type": "null" + } + ] + }, + "resume": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "text" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/command": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.command", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SessionPending.User" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | CommandNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/CommandNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictError" + } + } + } + }, + "500": { + "description": "CommandEvaluationError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommandEvaluationError" + } + } + } + } + }, + "description": "Resolve a slash command into prompt input, admit it durably, and schedule execution unless resume is false.", + "summary": "Run command", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] + }, + "command": { + "type": "string" + }, + "arguments": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "agent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/Model.Ref" + }, + { + "type": "null" + } + ] + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PromptInput.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + }, + "delivery": { + "anyOf": [ + { + "type": "string", + "enum": [ + "steer", + "queue" + ] + }, + { + "type": "null" + } + ] + }, + "resume": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "command" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/skill": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.skill", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | SkillNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SkillNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Activate a skill for a session by appending a skill message and resuming execution.", + "summary": "Activate skill", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] + }, + "skill": { + "type": "string" + }, + "resume": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "skill" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/synthetic": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.synthetic", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SessionPending.Synthetic" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictError" + } + } + } + } + }, + "description": "Durably admit synthetic session input and schedule execution unless resume is false.", + "summary": "Add synthetic message", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] + }, + "text": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "type": "object" + }, + "delivery": { + "anyOf": [ + { + "type": "string", + "enum": [ + "steer", + "queue" + ] + }, + { + "type": "null" + } + ] + }, + "resume": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "text" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/shell": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.shell", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Execute one shell command in the session's working directory. Emits a shell.started event before execution and a shell.ended event with the merged output after.", + "summary": "Run shell command", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + { + "type": "null" + } + ] + }, + "command": { + "type": "string" + } + }, + "required": [ + "command" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/compact": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.compact", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SessionPending.Compaction" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictError" + } + } + } + } + }, + "description": "Queue a durable session compaction request.", + "summary": "Compact session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/wait": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.wait", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Wait for a session agent loop to become idle.", + "summary": "Wait for session" + } + }, + "/api/session/{sessionID}/revert/stage": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.revert.stage", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Session.Revert" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "MessageNotFoundError | SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/MessageNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "SessionBusyError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionBusyError" + } + } + } + }, + "500": { + "description": "UnknownError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnknownError" + } + } + } + } + }, + "description": "Stage or move a reversible session boundary and optionally apply its file changes.", + "summary": "Stage session revert", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "files": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "messageID" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/revert/clear": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.revert.clear", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "SessionBusyError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionBusyError" + } + } + } + }, + "500": { + "description": "UnknownError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnknownError" + } + } + } + } + }, + "summary": "Clear staged revert" + } + }, + "/api/session/{sessionID}/revert/commit": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.revert.commit", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "SessionBusyError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionBusyError" + } + } + } + } + }, + "summary": "Commit staged revert" + } + }, + "/api/session/{sessionID}/context": { + "get": { + "tags": [ + "session" + ], + "operationId": "v2.session.context", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Session.Message.Info" + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "500": { + "description": "UnknownError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnknownError" + } + } + } + } + }, + "description": "Retrieve the active context messages for a session (all messages after the last compaction).", + "summary": "Get session context" + } + }, + "/api/session/{sessionID}/pending": { + "get": { + "tags": [ + "session" + ], + "operationId": "v2.session.pending.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionPending.Info" + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "List durable admitted session work not yet visible in projected history, ordered by admission. Includes unpromoted user and synthetic inputs and unhandled compaction barriers. The runner owns consumption; items disappear once promoted or handled.", + "summary": "List pending session work" + } + }, + "/api/session/{sessionID}/instructions/entries": { + "get": { + "tags": [ + "session" + ], + "operationId": "v2.session.instructions.entry.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InstructionEntry.Info" + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "List API-managed instruction entries attached to the session.", + "summary": "List instruction entries" + } + }, + "/api/session/{sessionID}/instructions/entries/{key}": { + "put": { + "tags": [ + "session" + ], + "operationId": "v2.session.instructions.entry.put", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "key", + "in": "path", + "schema": { + "$ref": "#/components/schemas/InstructionEntry.Key" + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "413": { + "description": "InstructionEntryValueTooLargeError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstructionEntryValueTooLargeError" + } + } + } + } + }, + "description": "Attach or replace one durable instruction entry. Changes announce as updates at the next step boundary.", + "summary": "Put instruction entry", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "value": {} + }, + "required": [ + "value" + ], + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { + "tags": [ + "session" + ], + "operationId": "v2.session.instructions.entry.remove", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "key", + "in": "path", + "schema": { + "$ref": "#/components/schemas/InstructionEntry.Key" + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Remove one instruction entry; the removal is announced to the model at the next step boundary.", + "summary": "Remove instruction entry" + } + }, + "/api/session/{sessionID}/generate": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.generate", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "SessionGenerateResponse", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionGenerateResponse" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Generate transient text from the current session context without mutating session history.", + "summary": "Generate text from session context", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "prompt": { + "type": "string" + } + }, + "required": [ + "prompt" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/experimental/session/{sessionID}/log": { + "get": { + "tags": [ + "session" + ], + "operationId": "v2.session.log", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "after", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "follow", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string", + "enum": [ + "true", + "false" + ] + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "text/event-stream": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "event": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/SessionLogItemJsonString" + } + }, + "required": [ + "id", + "event", + "data" + ], + "additionalProperties": false + }, + "x-effect-stream": { + "encoding": "sse", + "causeSchema": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Fail" + ] + }, + "error": { + "not": {} + } + }, + "required": [ + "_tag", + "error" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Die" + ] + }, + "defect": {} + }, + "required": [ + "_tag", + "defect" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Interrupt" + ] + }, + "fiberId": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "fiberId" + ], + "additionalProperties": false + } + ] + } + }, + "errorSchema": { + "not": {} + }, + "failureEvent": "effect/httpapi/stream/failure" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Experimental durable session event log. Reads events after an exclusive aggregate sequence and continues with live events when follow=true.", + "summary": "Read the session log" + } + }, + "/api/session/{sessionID}/interrupt": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.interrupt", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Interrupt active execution owned by this OpenCode process. Idle interruption is a no-op.", + "summary": "Interrupt session execution" + } + }, + "/api/session/{sessionID}/background": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.background", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Move active foreground backgroundable tools for this session into background observation. Idle requests are a no-op.", + "summary": "Background blocking session tools" + } + }, + "/api/session/{sessionID}/message/{messageID}": { + "get": { + "tags": [ + "session" + ], + "operationId": "v2.session.message", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "messageID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Session.Message.Info" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | MessageNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/MessageNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve one projected message owned by the Session.", + "summary": "Get session message" + } + }, + "/api/session/{sessionID}/message": { + "get": { + "tags": [ + "session" + ], + "operationId": "v2.message.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "limit", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Maximum number of messages to return. When omitted, the endpoint returns its default page size." + }, + "required": false + }, + { + "name": "order", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + { + "type": "null" + } + ], + "description": "Message order for the first page. Use desc for newest first or asc for oldest first." + }, + "required": false + }, + { + "name": "cursor", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string", + "description": "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order." + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "SessionMessagesResponse", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionMessagesResponse" + } + } + } + }, + "400": { + "description": "InvalidCursorError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidCursorError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "500": { + "description": "UnknownError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnknownError" + } + } + } + } + }, + "description": "Retrieve projected messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.", + "summary": "Get session messages" + } + }, + "/api/model": { + "get": { + "tags": [ + "model" + ], + "operationId": "v2.model.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Model.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Retrieve the current snapshot of available models ordered by release date. The snapshot may precede initial plugin settlement.", + "summary": "List models" + } + }, + "/api/model/default": { + "get": { + "tags": [ + "model" + ], + "operationId": "v2.model.default", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "anyOf": [ + { + "$ref": "#/components/schemas/Model.Info" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Retrieve the model used when a session has no explicit model selection.", + "summary": "Get default model" + } + }, + "/api/generate": { + "post": { + "tags": [ + "generate" + ], + "operationId": "v2.generate.text", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "GenerateTextResponse", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenerateTextResponse" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Run one stateless model generation at the requested location and return the assistant text. Uses the location's default model when none is specified.", + "summary": "Generate text", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "prompt": { + "type": "string" + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/Model.Ref" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "prompt" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/provider": { + "get": { + "tags": [ + "provider" + ], + "operationId": "v2.provider.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProviderV2.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Retrieve active AI providers so clients can show provider availability and configuration.", + "summary": "List providers" + } + }, + "/api/provider/{providerID}": { + "get": { + "tags": [ + "provider" + ], + "operationId": "v2.provider.get", + "parameters": [ + { + "name": "providerID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/ProviderV2.Info" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "ProviderNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderNotFoundError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Retrieve a single AI provider so clients can inspect its availability and endpoint settings.", + "summary": "Get provider" + } + }, + "/api/integration": { + "get": { + "tags": [ + "integration" + ], + "operationId": "v2.integration.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Integration.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve available integrations and their authentication methods.", + "summary": "List integrations" + } + }, + "/api/integration/{integrationID}": { + "get": { + "tags": [ + "integration" + ], + "operationId": "v2.integration.get", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "anyOf": [ + { + "$ref": "#/components/schemas/Integration.Info" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve one integration and its authentication methods.", + "summary": "Get integration" + } + }, + "/api/experimental/integration/wellknown": { + "post": { + "tags": [ + "integration" + ], + "operationId": "v2.experimental.integration.wellknown.add", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Discover and persist an experimental wellknown integration source.", + "summary": "Add wellknown integration", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/integration/{integrationID}/connect/key": { + "post": { + "tags": [ + "integration" + ], + "operationId": "v2.integration.connect.key", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Run a key authentication method and store the resulting credential.", + "summary": "Connect with key", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "key" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/integration/{integrationID}/connect/oauth": { + "post": { + "tags": [ + "integration" + ], + "operationId": "v2.integration.oauth.connect", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Integration.Attempt" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Start an OAuth attempt and return the authorization details.", + "summary": "Begin OAuth connection", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "methodID": { + "type": "string" + }, + "inputs": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "methodID", + "inputs" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/integration/{integrationID}/connect/oauth/{attemptID}": { + "get": { + "tags": [ + "integration" + ], + "operationId": "v2.integration.oauth.status", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "attemptID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Integration.AttemptStatus" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Poll the current status of an OAuth attempt.", + "summary": "Get OAuth attempt status" + }, + "delete": { + "tags": [ + "integration" + ], + "operationId": "v2.integration.oauth.cancel", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "attemptID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Cancel an OAuth attempt and release its resources.", + "summary": "Cancel OAuth connection" + } + }, + "/api/integration/{integrationID}/connect/oauth/{attemptID}/complete": { + "post": { + "tags": [ + "integration" + ], + "operationId": "v2.integration.oauth.complete", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "attemptID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Complete a code-based OAuth attempt and store the resulting credential.", + "summary": "Complete OAuth connection", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/integration/{integrationID}/connect/command": { + "post": { + "tags": [ + "integration" + ], + "operationId": "v2.integration.command.connect", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Integration.CommandAttempt" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Start a command authentication attempt.", + "summary": "Begin command connection", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "methodID": { + "type": "string" + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "methodID" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/integration/{integrationID}/connect/command/{attemptID}": { + "get": { + "tags": [ + "integration" + ], + "operationId": "v2.integration.command.status", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "attemptID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Integration.CommandAttemptStatus" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Poll the current status and output of a command authentication attempt.", + "summary": "Get command attempt status" + }, + "delete": { + "tags": [ + "integration" + ], + "operationId": "v2.integration.command.cancel", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "attemptID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Cancel a command authentication attempt and terminate its process.", + "summary": "Cancel command connection" + } + }, + "/api/mcp": { + "get": { + "tags": [ + "mcp" + ], + "operationId": "v2.mcp.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Mcp.Server" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve configured MCP servers and their connection status.", + "summary": "List MCP servers" + } + }, + "/api/mcp/{server}": { + "put": { + "tags": [ + "mcp" + ], + "operationId": "v2.mcp.add", + "parameters": [ + { + "name": "server", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Add an MCP server at runtime or replace an existing one, connecting it immediately.", + "summary": "Add MCP server", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "config": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.LocalConfig" + }, + { + "$ref": "#/components/schemas/Mcp.RemoteConfig" + } + ] + } + }, + "required": [ + "config" + ], + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { + "tags": [ + "mcp" + ], + "operationId": "v2.mcp.remove", + "parameters": [ + { + "name": "server", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "McpServerNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/McpServerNotFoundError" + } + } + } + } + }, + "description": "Stop an MCP server and remove it from the runtime set until restart.", + "summary": "Remove MCP server" + } + }, + "/api/mcp/{server}/connect": { + "post": { + "tags": [ + "mcp" + ], + "operationId": "v2.mcp.connect", + "parameters": [ + { + "name": "server", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "McpServerNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/McpServerNotFoundError" + } + } + } + } + }, + "description": "Connect an MCP server at runtime, overriding a disabled configuration until restart.", + "summary": "Connect MCP server" + } + }, + "/api/mcp/{server}/disconnect": { + "post": { + "tags": [ + "mcp" + ], + "operationId": "v2.mcp.disconnect", + "parameters": [ + { + "name": "server", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "McpServerNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/McpServerNotFoundError" + } + } + } + } + }, + "description": "Disconnect an MCP server at runtime, removing its tools until reconnected.", + "summary": "Disconnect MCP server" + } + }, + "/api/mcp/resource": { + "get": { + "tags": [ + "mcp" + ], + "operationId": "v2.mcp.resource.catalog", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Mcp.ResourceCatalog" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve resources and resource templates from connected MCP servers.", + "summary": "List MCP resources" + } + }, + "/api/credential/{credentialID}": { + "patch": { + "tags": [ + "credential" + ], + "operationId": "v2.credential.update", + "parameters": [ + { + "name": "credentialID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Update a stored credential label.", + "summary": "Update credential", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "label": { + "type": "string" + } + }, + "required": [ + "label" + ], + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { + "tags": [ + "credential" + ], + "operationId": "v2.credential.remove", + "parameters": [ + { + "name": "credentialID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Remove a stored integration credential.", + "summary": "Remove credential" + } + }, + "/api/project": { + "get": { + "tags": [ + "project" + ], + "operationId": "v2.project.list", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Project" + } + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "List known projects.", + "summary": "List projects" + } + }, + "/api/project/current": { + "get": { + "tags": [ + "project" + ], + "operationId": "v2.project.current", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Project.Current", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project.Current" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Resolve the project for the requested location.", + "summary": "Get current project" + } + }, + "/api/project/{projectID}/directories": { + "get": { + "tags": [ + "project" + ], + "operationId": "v2.project.directories", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Project.Directories", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project.Directories" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "List known local absolute directories for a project.", + "summary": "List project directories" + } + }, + "/api/form/request": { + "get": { + "tags": [ + "form" + ], + "operationId": "v2.form.request.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve pending forms for a location.", + "summary": "List pending form requests" + } + }, + "/api/session/{sessionID}/form": { + "get": { + "tags": [ + "form" + ], + "operationId": "v2.session.form.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Info" + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve pending forms for a session.", + "summary": "List session forms" + }, + "post": { + "tags": [ + "form" + ], + "operationId": "v2.session.form.create", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Form.Info" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictError" + } + } + } + } + }, + "description": "Create a form for a session.", + "summary": "Create session form", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Form.CreatePayload" + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/form/{formID}": { + "get": { + "tags": [ + "form" + ], + "operationId": "v2.session.form.get", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Form.Info" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve a form for a session.", + "summary": "Get session form" + } + }, + "/api/session/{sessionID}/form/{formID}/state": { + "get": { + "tags": [ + "form" + ], + "operationId": "v2.session.form.state", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Form.State" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve the current state for a form.", + "summary": "Get form state" + } + }, + "/api/session/{sessionID}/form/{formID}/reply": { + "post": { + "tags": [ + "form" + ], + "operationId": "v2.session.form.reply", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "FormInvalidAnswerError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormInvalidAnswerError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "FormAlreadySettledError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormAlreadySettledError" + } + } + } + } + }, + "description": "Submit an answer to a pending form.", + "summary": "Reply to form", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Form.Reply" + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/form/{formID}/cancel": { + "post": { + "tags": [ + "form" + ], + "operationId": "v2.session.form.cancel", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "FormAlreadySettledError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormAlreadySettledError" + } + } + } + } + }, + "description": "Cancel a pending form.", + "summary": "Cancel form" + } + }, + "/api/permission/request": { + "get": { + "tags": [ + "permission" + ], + "operationId": "v2.permission.request.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionV2.Request" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve pending permission requests for a location.", + "summary": "List pending permission requests" + } + }, + "/api/permission/saved": { + "get": { + "tags": [ + "permission" + ], + "operationId": "v2.permission.saved.list", + "parameters": [ + { + "name": "projectID", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionSaved.Info" + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve saved permissions, optionally filtered by project.", + "summary": "List saved permissions" + } + }, + "/api/permission/saved/{id}": { + "delete": { + "tags": [ + "permission" + ], + "operationId": "v2.permission.saved.remove", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Remove a saved permission by ID.", + "summary": "Remove saved permission" + } + }, + "/api/session/{sessionID}/permission": { + "post": { + "tags": [ + "permission" + ], + "operationId": "v2.session.permission.create", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "effect": { + "$ref": "#/components/schemas/PermissionV2.Effect" + } + }, + "required": [ + "id", + "effect" + ], + "additionalProperties": false + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Evaluate and, when approval is required, create a permission request for a session.", + "summary": "Create permission request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + { + "type": "null" + } + ] + }, + "action": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "save": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": { + "type": "object" + }, + "source": { + "$ref": "#/components/schemas/PermissionV2.Source" + }, + "agent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "action", + "resources" + ], + "additionalProperties": false + } + } + }, + "required": true + } + }, + "get": { + "tags": [ + "permission" + ], + "operationId": "v2.session.permission.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionV2.Request" + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve pending permission requests owned by a session.", + "summary": "List session permission requests" + } + }, + "/api/session/{sessionID}/permission/{requestID}": { + "get": { + "tags": [ + "permission" + ], + "operationId": "v2.session.permission.get", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/PermissionV2.Request" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | PermissionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/PermissionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve a pending permission request owned by a session.", + "summary": "Get permission request" + } + }, + "/api/session/{sessionID}/permission/{requestID}/reply": { + "post": { + "tags": [ + "permission" + ], + "operationId": "v2.session.permission.reply", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | PermissionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/PermissionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Respond to a pending permission request owned by a session.", + "summary": "Reply to pending permission request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "reply": { + "$ref": "#/components/schemas/PermissionV2.Reply" + }, + "message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "reply" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/fs/read/*": { + "get": { + "tags": [ + "filesystem" + ], + "operationId": "v2.fs.read", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Serve one file relative to the requested location.", + "summary": "Read file" + } + }, + "/api/fs/list": { + "get": { + "tags": [ + "filesystem" + ], + "operationId": "v2.fs.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + }, + { + "name": "path", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystem.Entry" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "List direct children of one directory relative to the requested location.", + "summary": "List directory" + } + }, + "/api/fs/find": { + "get": { + "tags": [ + "filesystem" + ], + "operationId": "v2.fs.find", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + }, + { + "name": "query", + "in": "query", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "file", + "directory" + ] + }, + "required": false + }, + { + "name": "limit", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystem.Entry" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Find recursively ranked filesystem entries relative to the requested location.", + "summary": "Find files" + } + }, + "/api/command": { + "get": { + "tags": [ + "command" + ], + "operationId": "v2.command.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Command.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve currently registered commands.", + "summary": "List commands" + } + }, + "/api/skill": { + "get": { + "tags": [ + "skill" + ], + "operationId": "v2.skill.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Skill.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve currently registered skills.", + "summary": "List skills" + } + }, + "/api/event": { + "get": { + "tags": [ + "event" + ], + "operationId": "v2.event.subscribe", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "text/event-stream": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "event": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/V2EventJsonString" + } + }, + "required": [ + "id", + "event", + "data" + ], + "additionalProperties": false + }, + "x-effect-stream": { + "encoding": "sse", + "causeSchema": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Fail" + ] + }, + "error": { + "not": {} + } + }, + "required": [ + "_tag", + "error" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Die" + ] + }, + "defect": {} + }, + "required": [ + "_tag", + "defect" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Interrupt" + ] + }, + "fiberId": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "fiberId" + ], + "additionalProperties": false + } + ] + } + }, + "errorSchema": { + "not": {} + }, + "failureEvent": "effect/httpapi/stream/failure" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Subscribe to native event payloads for the server. Volatile by contract: a slow consumer overflows and fails the stream, and events during disconnection are missed.", + "summary": "Subscribe to events" + } + }, + "/api/pty": { + "get": { + "tags": [ + "pty" + ], + "operationId": "v2.pty.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pty" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "List PTY sessions for a location, including exited sessions retained until removal.", + "summary": "List PTY sessions" + }, + "post": { + "tags": [ + "pty" + ], + "operationId": "v2.pty.create", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Pty" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Create a pseudo-terminal session for a location.", + "summary": "Create PTY session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "cwd": { + "type": "string" + }, + "title": { + "type": "string" + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/pty/{ptyID}": { + "get": { + "tags": [ + "pty" + ], + "operationId": "v2.pty.get", + "parameters": [ + { + "name": "ptyID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Pty" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } + } + }, + "description": "Get one PTY session, including its exit code once exited.", + "summary": "Get PTY session" + }, + "put": { + "tags": [ + "pty" + ], + "operationId": "v2.pty.update", + "parameters": [ + { + "name": "ptyID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Pty" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } + } + }, + "description": "Update the title or viewport size of one PTY session.", + "summary": "Update PTY session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "size": { + "type": "object", + "properties": { + "rows": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "cols": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + } + }, + "required": [ + "rows", + "cols" + ], + "additionalProperties": false + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { + "tags": [ + "pty" + ], + "operationId": "v2.pty.remove", + "parameters": [ + { + "name": "ptyID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } + } + }, + "description": "Terminate and remove one PTY session.", + "summary": "Remove PTY session" + } + }, + "/api/pty/{ptyID}/connect-token": { + "post": { + "tags": [ + "pty" + ], + "operationId": "v2.pty.connect.token", + "parameters": [ + { + "name": "ptyID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/PtyTicket.ConnectToken" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "403": { + "description": "ForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, + "404": { + "description": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } + } + }, + "description": "Create a short-lived single-use ticket for opening a PTY WebSocket connection.", + "summary": "Create PTY WebSocket token" + } + }, + "/api/pty/{ptyID}/connect": { + "get": { + "tags": [ + "pty" + ], + "operationId": "v2.pty.connect", + "parameters": [ + { + "name": "ptyID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "required": true + }, + { + "in": "query", + "name": "location[directory]", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "location[workspace]", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "cursor", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "ticket", + "schema": { + "type": "string" + } + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "403": { + "description": "ForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, + "404": { + "description": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } + } + }, + "description": "Establish a WebSocket connection streaming PTY output and accepting terminal input.", + "summary": "Connect to PTY session", + "x-websocket": true + } + }, + "/api/shell": { + "get": { + "tags": [ + "shell" + ], + "operationId": "v2.shell.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Shell.Info1" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "List currently running shell commands for a location. Exited commands are not included.", + "summary": "List running shell commands" + }, + "post": { + "tags": [ + "shell" + ], + "operationId": "v2.shell.create", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Shell.Info1" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Spawn one non-interactive shell command for a location. Combined stdout/stderr is captured to a file pageable via output.", + "summary": "Run shell command", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "timeout": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "command", + "timeout" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/shell/{id}": { + "get": { + "tags": [ + "shell" + ], + "operationId": "v2.shell.get", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Shell.Info1" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "ShellNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShellNotFoundError" + } + } + } + } + }, + "description": "Get one shell command, including its status and exit code once exited.", + "summary": "Get shell command" + }, + "delete": { + "tags": [ + "shell" + ], + "operationId": "v2.shell.remove", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "ShellNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShellNotFoundError" + } + } + } + } + }, + "description": "Terminate and remove one shell command and its retained output.", + "summary": "Remove shell command" + } + }, + "/api/shell/{id}/timeout": { + "patch": { + "tags": [ + "shell" + ], + "operationId": "v2.shell.timeout", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Shell.Info1" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "ShellNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShellNotFoundError" + } + } + } + } + }, + "description": "Replace a running shell command's timeout from now, or clear it with zero.", + "summary": "Update shell timeout", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "timeout": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "timeout" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/shell/{id}/output": { + "get": { + "tags": [ + "shell" + ], + "operationId": "v2.shell.output", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + }, + { + "name": "cursor", + "in": "query", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" + } + ] + }, + "required": false + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "object", + "properties": { + "output": { + "type": "string" + }, + "cursor": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "size": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "truncated": { + "type": "boolean" + } + }, + "required": [ + "output", + "cursor", + "size", + "truncated" + ], + "additionalProperties": false + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "ShellNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShellNotFoundError" + } + } + } + } + }, + "description": "Page through captured combined output by absolute byte cursor.", + "summary": "Read shell output" + } + }, + "/api/question/request": { + "get": { + "tags": [ + "question" + ], + "operationId": "v2.question.request.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Request" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve pending question requests for a location.", + "summary": "List pending question requests" + } + }, + "/api/session/{sessionID}/question": { + "get": { + "tags": [ + "question" + ], + "operationId": "v2.session.question.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Request" + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve pending question requests owned by a session.", + "summary": "List session question requests" + } + }, + "/api/session/{sessionID}/question/{requestID}/reply": { + "post": { + "tags": [ + "question" + ], + "operationId": "v2.session.question.reply", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | QuestionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Answer a pending question request owned by a session.", + "summary": "Reply to pending question request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QuestionV2.Reply" + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/question/{requestID}/reject": { + "post": { + "tags": [ + "question" + ], + "operationId": "v2.session.question.reject", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | QuestionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Reject a pending question request owned by a session.", + "summary": "Reject pending question request" + } + }, + "/api/reference": { + "get": { + "tags": [ + "reference" + ], + "operationId": "v2.reference.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Reference.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "List references available in the requested location.", + "summary": "List references" + } + }, + "/experimental/project/{projectID}/copy": { + "post": { + "tags": [ + "projectCopy" + ], + "operationId": "v2.projectCopy.create", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "ProjectCopy.Copy", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectCopy.Copy" + } + } + } + }, + "400": { + "description": "ProjectCopyError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "strategy": { + "type": "string" + }, + "directory": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "strategy", + "directory" + ], + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { + "tags": [ + "projectCopy" + ], + "operationId": "v2.projectCopy.remove", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "ProjectCopyError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "force": { + "type": "boolean" + } + }, + "required": [ + "directory", + "force" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/experimental/project/{projectID}/copy/refresh": { + "post": { + "tags": [ + "projectCopy" + ], + "operationId": "v2.projectCopy.refresh", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "ProjectCopyError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + } + } + }, + "/api/vcs/status": { + "get": { + "tags": [ + "vcs" + ], + "operationId": "v2.vcs.status", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Vcs.FileStatus" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "List uncommitted working-copy changes relative to the requested location.", + "summary": "VCS status" + } + }, + "/api/vcs/diff": { + "get": { + "tags": [ + "vcs" + ], + "operationId": "v2.vcs.diff", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + }, + { + "name": "mode", + "in": "query", + "schema": { + "$ref": "#/components/schemas/Vcs.Mode" + }, + "required": true + }, + { + "name": "context", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileDiff.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Diff the working copy against HEAD (mode git) or the default-branch merge base (mode branch) for the requested location.", + "summary": "VCS diff" + } + }, + "/api/debug/location": { + "get": { + "tags": [ + "debug" + ], + "operationId": "v2.debug.location.list", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Location.Ref" + } + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "List locations currently loaded by the server.", + "summary": "List loaded locations" + }, + "delete": { + "tags": [ + "debug" + ], + "operationId": "v2.debug.location.evict", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Dispose the requested location's cached services so its next use boots them fresh.", + "summary": "Evict a loaded location" + } + }, + "/api/websearch/provider": { + "get": { + "tags": [ + "websearch" + ], + "operationId": "v2.websearch.providers", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebSearch.Provider" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Return the registered web search providers.", + "summary": "List web search providers" + } + }, + "/api/websearch": { + "post": { + "tags": [ + "websearch" + ], + "operationId": "v2.websearch.query", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/WebSearch.Response" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Run one web search through the selected provider. Specify a provider to override the configured default.", + "summary": "Search the web", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "query": { + "type": "string" + }, + "providerID": { + "type": "string" + } + }, + "required": [ + "query" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + } + }, + "components": { + "schemas": { + "ServiceHealth": { + "type": "object", + "properties": { + "healthy": { + "type": "boolean", + "enum": [ + true + ] + }, + "version": { + "type": "string" + }, + "pid": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + } + }, + "required": [ + "healthy", + "version", + "pid" + ], + "additionalProperties": false + }, + "UnauthorizedError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "UnauthorizedError" + ] + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "InvalidRequestError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "InvalidRequestError" + ] + }, + "message": { + "type": "string" + }, + "kind": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "field": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "ServiceStopRequest": { + "type": "object", + "properties": { + "instanceID": { + "type": "string" + } + }, + "required": [ + "instanceID" + ], + "additionalProperties": false + }, + "ServiceStopResponse": { + "type": "object", + "properties": { + "accepted": { + "type": "boolean" + } + }, + "required": [ + "accepted" + ], + "additionalProperties": false + }, + "Location.Info": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspaceID": { + "type": "string", + "allOf": [ + { + "pattern": "^wrk" + } + ] + }, + "project": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "directory": { + "type": "string" + } + }, + "required": [ + "id", + "directory" + ], + "additionalProperties": false + } + }, + "required": [ + "directory", + "project" + ], + "additionalProperties": false + }, + "Model.Ref": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": [ + "id", + "providerID" + ], + "additionalProperties": false + }, + "Provider.Settings": { + "type": "object" + }, + "Provider.Request": { + "type": "object", + "properties": { + "settings": { + "$ref": "#/components/schemas/Provider.Settings" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + } + }, + "required": [ + "settings", + "headers", + "body" + ], + "additionalProperties": false + }, + "Agent.Color": { + "type": "string" + }, + "PermissionV2.Effect": { + "type": "string", + "enum": [ + "allow", + "deny", + "ask" + ] + }, + "PermissionV2.Rule": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "effect": { + "$ref": "#/components/schemas/PermissionV2.Effect" + } + }, + "required": [ + "action", + "resource", + "effect" + ], + "additionalProperties": false + }, + "PermissionV2.Ruleset": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionV2.Rule" + } + }, + "Agent.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "request": { + "$ref": "#/components/schemas/Provider.Request" + }, + "system": { + "type": "string" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "hidden": { + "type": "boolean" + }, + "color": { + "$ref": "#/components/schemas/Agent.Color" + }, + "steps": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "permissions": { + "$ref": "#/components/schemas/PermissionV2.Ruleset" + } + }, + "required": [ + "id", + "name", + "request", + "mode", + "hidden", + "permissions" + ], + "additionalProperties": false + }, + "Plugin.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "Money.USD": { + "type": "number" + }, + "TokenUsage.Info": { + "type": "object", + "properties": { + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "reasoning": { + "type": "number" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": [ + "read", + "write" + ], + "additionalProperties": false + } + }, + "required": [ + "input", + "output", + "reasoning", + "cache" + ], + "additionalProperties": false + }, + "Location.Ref": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspaceID": { + "type": "string", + "allOf": [ + { + "pattern": "^wrk" + } + ] + } + }, + "required": [ + "directory" + ], + "additionalProperties": false + }, + "FileDiff.Info": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "patch": { + "type": "string" + }, + "additions": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "deletions": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "status": { + "type": "string", + "enum": [ + "added", + "deleted", + "modified" + ] + } + }, + "required": [ + "file", + "patch", + "additions", + "deletions", + "status" + ], + "additionalProperties": false + }, + "Session.Revert": { + "type": "object", + "properties": { + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "partID": { + "type": "string" + }, + "snapshot": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileDiff.Info" + } + } + }, + "required": [ + "messageID" + ], + "additionalProperties": false + }, + "Session.Info": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "parentID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "fork": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": [ + "sessionID" + ], + "additionalProperties": false + }, + "projectID": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "updated": { + "type": "number" + }, + "archived": { + "type": "number" + } + }, + "required": [ + "created", + "updated" + ], + "additionalProperties": false + }, + "title": { + "type": "string" + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "subpath": { + "type": "string" + }, + "revert": { + "$ref": "#/components/schemas/Session.Revert" + } + }, + "required": [ + "id", + "projectID", + "cost", + "tokens", + "time", + "title", + "location" + ], + "additionalProperties": false + }, + "SessionsResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Session.Info" + } + }, + "cursor": { + "type": "object", + "properties": { + "previous": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "next": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "data", + "cursor" + ], + "additionalProperties": false + }, + "InvalidCursorError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "InvalidCursorError" + ] + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "InvalidRequestError1": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "InvalidRequestError" + ] + }, + "message": { + "type": "string" + }, + "kind": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "field": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "SessionActive": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "running" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "SessionNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "SessionNotFoundError" + ] + }, + "sessionID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "sessionID", + "message" + ], + "additionalProperties": false + }, + "MessageNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "MessageNotFoundError" + ] + }, + "sessionID": { + "type": "string" + }, + "messageID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "sessionID", + "messageID", + "message" + ], + "additionalProperties": false + }, + "Prompt.Mention": { + "type": "object", + "properties": { + "start": { + "type": "number" + }, + "end": { + "type": "number" + }, + "text": { + "type": "string" + } + }, + "required": [ + "start", + "end", + "text" + ], + "additionalProperties": false + }, + "PromptInput.FileAttachment": { + "type": "object", + "properties": { + "uri": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "mention": { + "$ref": "#/components/schemas/Prompt.Mention" + } + }, + "required": [ + "uri" + ], + "additionalProperties": false + }, + "Prompt.AgentAttachment": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "mention": { + "$ref": "#/components/schemas/Prompt.Mention" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "Prompt.Base64": { + "type": "string", + "allOf": [ + { + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$" + } + ] + }, + "Prompt.FileSource": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "inline" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "uri" + ] + }, + "uri": { + "type": "string" + } + }, + "required": [ + "type", + "uri" + ], + "additionalProperties": false + } + ] + }, + "Prompt.FileAttachment": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Prompt.Base64" + }, + "mime": { + "type": "string" + }, + "source": { + "$ref": "#/components/schemas/Prompt.FileSource" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "mention": { + "$ref": "#/components/schemas/Prompt.Mention" + } + }, + "required": [ + "data", + "mime", + "source" + ], + "additionalProperties": false + }, + "SessionPending.UserData": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "text" + ], + "additionalProperties": false + }, + "SessionPending.User": { + "type": "object", + "properties": { + "admittedSeq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "timeCreated": { + "type": "number" + }, + "type": { + "type": "string", + "enum": [ + "user" + ] + }, + "data": { + "$ref": "#/components/schemas/SessionPending.UserData" + }, + "delivery": { + "type": "string", + "enum": [ + "steer", + "queue" + ] + } + }, + "required": [ + "admittedSeq", + "id", + "sessionID", + "timeCreated", + "type", + "data", + "delivery" + ], + "additionalProperties": false + }, + "ConflictError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "ConflictError" + ] + }, + "message": { + "type": "string" + }, + "resource": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "CommandNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "CommandNotFoundError" + ] + }, + "command": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "command", + "message" + ], + "additionalProperties": false + }, + "CommandEvaluationError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "CommandEvaluationError" + ] + }, + "command": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "command", + "message" + ], + "additionalProperties": false + }, + "SkillNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "SkillNotFoundError" + ] + }, + "skill": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "skill", + "message" + ], + "additionalProperties": false + }, + "SessionPending.SyntheticData": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "description": { + "type": "string" + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "text" + ], + "additionalProperties": false + }, + "SessionPending.Synthetic": { + "type": "object", + "properties": { + "admittedSeq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "timeCreated": { + "type": "number" + }, + "type": { + "type": "string", + "enum": [ + "synthetic" + ] + }, + "data": { + "$ref": "#/components/schemas/SessionPending.SyntheticData" + }, + "delivery": { + "type": "string", + "enum": [ + "steer", + "queue" + ] + } + }, + "required": [ + "admittedSeq", + "id", + "sessionID", + "timeCreated", + "type", + "data", + "delivery" + ], + "additionalProperties": false + }, + "SessionPending.Compaction": { + "type": "object", + "properties": { + "admittedSeq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "timeCreated": { + "type": "number" + }, + "type": { + "type": "string", + "enum": [ + "compaction" + ] + } + }, + "required": [ + "admittedSeq", + "id", + "sessionID", + "timeCreated", + "type" + ], + "additionalProperties": false + }, + "ServiceUnavailableError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "ServiceUnavailableError" + ] + }, + "message": { + "type": "string" + }, + "service": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "SessionBusyError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "SessionBusyError" + ] + }, + "sessionID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "sessionID", + "message" + ], + "additionalProperties": false + }, + "UnknownError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "UnknownError" + ] + }, + "message": { + "type": "string" + }, + "ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "Session.Message.AgentSelected": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": [ + "agent-switched" + ] + }, + "agent": { + "type": "string" + } + }, + "required": [ + "id", + "time", + "type", + "agent" + ], + "additionalProperties": false + }, + "Session.Message.ModelSelected": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": [ + "model-switched" + ] + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "previous": { + "$ref": "#/components/schemas/Model.Ref" + } + }, + "required": [ + "id", + "time", + "type", + "model" + ], + "additionalProperties": false + }, + "Session.Message.User": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "text": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + }, + "type": { + "type": "string", + "enum": [ + "user" + ] + } + }, + "required": [ + "id", + "time", + "text", + "type" + ], + "additionalProperties": false + }, + "Session.Message.Synthetic": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "text": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "synthetic" + ] + } + }, + "required": [ + "id", + "time", + "text", + "type" + ], + "additionalProperties": false + }, + "Session.Message.System": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": [ + "system" + ] + }, + "text": { + "type": "string" + } + }, + "required": [ + "id", + "time", + "type", + "text" + ], + "additionalProperties": false + }, + "Session.Message.Skill": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": [ + "skill" + ] + }, + "skill": { + "type": "string" + }, + "name": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": [ + "id", + "time", + "type", + "skill", + "name", + "text" + ], + "additionalProperties": false + }, + "Session.Message.Shell": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "completed": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": [ + "shell" + ] + }, + "shellID": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "command": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "running", + "exited", + "timeout", + "killed" + ] + }, + "exit": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "output": { + "type": "object", + "properties": { + "output": { + "type": "string" + }, + "cursor": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "size": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "truncated": { + "type": "boolean" + } + }, + "required": [ + "output", + "cursor", + "size", + "truncated" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "time", + "type", + "shellID", + "command", + "status" + ], + "additionalProperties": false + }, + "Session.Message.ProviderState": { + "type": "object" + }, + "Session.Message.Assistant.Text": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "text" + ] + }, + "text": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState" + } + }, + "required": [ + "type", + "text" + ], + "additionalProperties": false + }, + "Session.Message.Assistant.Reasoning": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "reasoning" + ] + }, + "text": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "completed": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "text" + ], + "additionalProperties": false + }, + "Session.Message.ToolState.Streaming": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "streaming" + ] + }, + "input": { + "type": "string" + } + }, + "required": [ + "status", + "input" + ], + "additionalProperties": false + }, + "Session.Message.ToolState.Running": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "running" + ] + }, + "input": { + "type": "object" + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "status", + "input", + "metadata" + ], + "additionalProperties": false + }, + "Tool.TextContent": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "text" + ] + }, + "text": { + "type": "string" + } + }, + "required": [ + "type", + "text" + ], + "additionalProperties": false + }, + "Tool.FileContent": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "file" + ] + }, + "uri": { + "type": "string" + }, + "mime": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "type", + "uri", + "mime" + ], + "additionalProperties": false + }, + "LLM.ToolContent": { + "anyOf": [ + { + "$ref": "#/components/schemas/Tool.TextContent" + }, + { + "$ref": "#/components/schemas/Tool.FileContent" + } + ] + }, + "Session.Message.ToolState.Completed": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "completed" + ] + }, + "input": { + "type": "object" + }, + "content": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/LLM.ToolContent" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/LLM.ToolContent" + } + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "status", + "input", + "content" + ], + "additionalProperties": false + }, + "Session.StructuredError": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "type", + "message" + ], + "additionalProperties": false + }, + "Session.Message.ToolState.Error": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "error" + ] + }, + "input": { + "type": "object" + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + }, + "content": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/LLM.ToolContent" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/LLM.ToolContent" + } + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "status", + "input", + "error" + ], + "additionalProperties": false + }, + "Session.Message.Assistant.Tool": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "tool" + ] + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "executed": { + "type": "boolean" + }, + "providerState": { + "$ref": "#/components/schemas/Session.Message.ProviderState" + }, + "providerResultState": { + "$ref": "#/components/schemas/Session.Message.ProviderState" + }, + "state": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.ToolState.Streaming" + }, + { + "$ref": "#/components/schemas/Session.Message.ToolState.Running" + }, + { + "$ref": "#/components/schemas/Session.Message.ToolState.Completed" + }, + { + "$ref": "#/components/schemas/Session.Message.ToolState.Error" + } + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "ran": { + "type": "number" + }, + "completed": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "id", + "name", + "state", + "time" + ], + "additionalProperties": false + }, + "Session.Message.Assistant.Retry": { + "type": "object", + "properties": { + "attempt": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "at": { + "type": "number" + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + } + }, + "required": [ + "attempt", + "at", + "error" + ], + "additionalProperties": false + }, + "Session.Message.Assistant": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "completed": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": [ + "assistant" + ] + }, + "agent": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "content": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.Assistant.Text" + }, + { + "$ref": "#/components/schemas/Session.Message.Assistant.Reasoning" + }, + { + "$ref": "#/components/schemas/Session.Message.Assistant.Tool" + } + ] + } + }, + "snapshot": { + "type": "object", + "properties": { + "start": { + "type": "string" + }, + "end": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "finish": { + "type": "string", + "enum": [ + "stop", + "length", + "tool-calls", + "content-filter", + "error", + "unknown" + ] + }, + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + }, + "retry": { + "$ref": "#/components/schemas/Session.Message.Assistant.Retry" + } + }, + "required": [ + "id", + "time", + "type", + "agent", + "model", + "content" + ], + "additionalProperties": false + }, + "Session.Message.Compaction.Running": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "compaction" + ] + }, + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "status": { + "type": "string", + "enum": [ + "running" + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "summary": { + "type": "string" + }, + "recent": { + "type": "string" + } + }, + "required": [ + "type", + "id", + "time", + "status", + "reason", + "summary", + "recent" + ], + "additionalProperties": false + }, + "Session.Message.Compaction.Completed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "compaction" + ] + }, + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "status": { + "type": "string", + "enum": [ + "completed" + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "summary": { + "type": "string" + }, + "recent": { + "type": "string" + } + }, + "required": [ + "type", + "id", + "time", + "status", + "reason", + "summary", + "recent" + ], + "additionalProperties": false + }, + "Session.Message.Compaction.Failed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "compaction" + ] + }, + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "status": { + "type": "string", + "enum": [ + "failed" + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + } + }, + "required": [ + "type", + "id", + "time", + "status", + "reason", + "error" + ], + "additionalProperties": false + }, + "Session.Message.Compaction": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.Compaction.Running" + }, + { + "$ref": "#/components/schemas/Session.Message.Compaction.Completed" + }, + { + "$ref": "#/components/schemas/Session.Message.Compaction.Failed" + } + ] + }, + "Session.Message.Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.AgentSelected" + }, + { + "$ref": "#/components/schemas/Session.Message.ModelSelected" + }, + { + "$ref": "#/components/schemas/Session.Message.User" + }, + { + "$ref": "#/components/schemas/Session.Message.Synthetic" + }, + { + "$ref": "#/components/schemas/Session.Message.System" + }, + { + "$ref": "#/components/schemas/Session.Message.Skill" + }, + { + "$ref": "#/components/schemas/Session.Message.Shell" + }, + { + "$ref": "#/components/schemas/Session.Message.Assistant" + }, + { + "$ref": "#/components/schemas/Session.Message.Compaction" + } + ] + }, + "SessionPending.Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionPending.User" + }, + { + "$ref": "#/components/schemas/SessionPending.Synthetic" + }, + { + "$ref": "#/components/schemas/SessionPending.Compaction" + } + ] + }, + "InstructionEntry.Key": { + "type": "string", + "allOf": [ + { + "pattern": "^[a-z0-9][a-z0-9._-]*$", + "description": "Instruction entry key (lowercase alphanumerics plus . _ -)" + } + ] + }, + "InstructionEntry.Info": { + "type": "object", + "properties": { + "key": { + "$ref": "#/components/schemas/InstructionEntry.Key" + }, + "value": {} + }, + "required": [ + "key", + "value" + ], + "additionalProperties": false + }, + "InstructionEntryValueTooLargeError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "InstructionEntryValueTooLargeError" + ] + }, + "actualBytes": { + "type": "integer" + }, + "maxBytes": { + "type": "integer" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "actualBytes", + "maxBytes", + "message" + ], + "additionalProperties": false + }, + "SessionGenerateResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": [ + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "data" + ], + "additionalProperties": false + }, + "session.agent.selected": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.agent.selected" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "agent": { + "type": "string" + } + }, + "required": [ + "sessionID", + "agent" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.model.selected": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.model.selected" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + } + }, + "required": [ + "sessionID", + "model" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.moved": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.moved" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "projectID": { + "type": "string" + }, + "subpath": { + "type": "string" + } + }, + "required": [ + "sessionID", + "location" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.renamed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.renamed" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "sessionID", + "title" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.deleted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.deleted" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 2 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + } + }, + "required": [ + "sessionID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.forked": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.forked" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 2 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "parentID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "parentSeq": { + "type": "integer", + "allOf": [ + { + "minimum": -1 + } + ] + }, + "from": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": [ + "sessionID", + "parentID", + "parentSeq" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.input.promoted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.input.promoted" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "inputID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": [ + "sessionID", + "inputID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "SessionPending.UserData1": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "text" + ], + "additionalProperties": false + }, + "SessionPending.UserMessage": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "user" + ] + }, + "data": { + "$ref": "#/components/schemas/SessionPending.UserData1" + }, + "delivery": { + "type": "string", + "enum": [ + "steer", + "queue" + ] + } + }, + "required": [ + "type", + "data", + "delivery" + ], + "additionalProperties": false + }, + "SessionPending.SyntheticData1": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "description": { + "type": "string" + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "text" + ], + "additionalProperties": false + }, + "SessionPending.SyntheticMessage": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "synthetic" + ] + }, + "data": { + "$ref": "#/components/schemas/SessionPending.SyntheticData1" + }, + "delivery": { + "type": "string", + "enum": [ + "steer", + "queue" + ] + } + }, + "required": [ + "type", + "data", + "delivery" + ], + "additionalProperties": false + }, + "SessionPending.Message": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionPending.UserMessage" + }, + { + "$ref": "#/components/schemas/SessionPending.SyntheticMessage" + } + ] + }, + "session.input.admitted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.input.admitted" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "inputID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "input": { + "$ref": "#/components/schemas/SessionPending.Message" + } + }, + "required": [ + "sessionID", + "inputID", + "input" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.execution.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.execution.started" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + } + }, + "required": [ + "sessionID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.execution.succeeded": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.execution.succeeded" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + } + }, + "required": [ + "sessionID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.execution.failed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.execution.failed" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + } + }, + "required": [ + "sessionID", + "error" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.execution.interrupted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.execution.interrupted" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "reason": { + "type": "string", + "enum": [ + "user", + "shutdown", + "superseded" + ] + } + }, + "required": [ + "sessionID", + "reason" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.instructions.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.instructions.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 2 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "delta": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^[a-f0-9]{64}$" + } + ] + }, + { + "type": "string", + "enum": [ + "removed" + ] + } + ] + } + } + }, + "required": [ + "sessionID", + "delta" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.synthetic": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.synthetic" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "text": { + "type": "string" + }, + "description": { + "type": "string" + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "sessionID", + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.skill.activated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.skill.activated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": [ + "sessionID", + "id", + "name", + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "Shell.Info": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "status": { + "type": "string", + "enum": [ + "running", + "exited", + "timeout", + "killed" + ] + }, + "command": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "shell": { + "type": "string" + }, + "file": { + "type": "string" + }, + "pid": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "exit": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "started": { + "type": "number" + }, + "completed": { + "type": "number" + } + }, + "required": [ + "started" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "status", + "command", + "cwd", + "shell", + "file", + "metadata", + "time" + ], + "additionalProperties": false + }, + "session.shell.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.shell.started" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "shell": { + "$ref": "#/components/schemas/Shell.Info" + } + }, + "required": [ + "sessionID", + "shell" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.shell.ended": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.shell.ended" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "shell": { + "$ref": "#/components/schemas/Shell.Info" + }, + "output": { + "type": "object", + "properties": { + "output": { + "type": "string" + }, + "cursor": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "size": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "truncated": { + "type": "boolean" + } + }, + "required": [ + "output", + "cursor", + "size", + "truncated" + ], + "additionalProperties": false + } + }, + "required": [ + "sessionID", + "shell", + "output" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.step.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.step.started" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "agent": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "snapshot": { + "type": "string" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "agent", + "model" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.step.ended": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.step.ended" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "finish": { + "type": "string", + "enum": [ + "stop", + "length", + "tool-calls", + "content-filter", + "error", + "unknown" + ] + }, + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" + }, + "snapshot": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "finish", + "cost", + "tokens" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.step.failed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.step.failed" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + }, + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" + }, + "snapshot": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "error" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.text.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.text.started" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "ordinal" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "Session.Message.ProviderState4": { + "type": "object" + }, + "session.text.ended": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.text.ended" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "text": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState4" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "ordinal", + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "Session.Message.ProviderState5": { + "type": "object" + }, + "session.reasoning.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.reasoning.started" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState5" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "ordinal" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "Session.Message.ProviderState6": { + "type": "object" + }, + "session.reasoning.ended": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.reasoning.ended" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "text": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState6" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "ordinal", + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.tool.input.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.tool.input.started" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "callID", + "name" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.tool.input.ended": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.tool.input.ended" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "callID", + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "Session.Message.ProviderState7": { + "type": "object" + }, + "session.tool.called": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.tool.called" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "input": { + "type": "object" + }, + "executed": { + "type": "boolean" + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState7" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "callID", + "input", + "executed" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "Session.Message.ProviderState8": { + "type": "object" + }, + "session.tool.success": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.tool.success" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 2 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "content": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/LLM.ToolContent" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/LLM.ToolContent" + } + }, + "metadata": { + "type": "object" + }, + "executed": { + "type": "boolean" + }, + "resultState": { + "$ref": "#/components/schemas/Session.Message.ProviderState8" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "callID", + "content", + "executed" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "Session.Message.ProviderState9": { + "type": "object" + }, + "session.tool.failed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.tool.failed" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 2 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + }, + "content": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/LLM.ToolContent" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/LLM.ToolContent" + } + }, + "metadata": { + "type": "object" + }, + "executed": { + "type": "boolean" + }, + "resultState": { + "$ref": "#/components/schemas/Session.Message.ProviderState9" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "callID", + "error", + "executed" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.retry.scheduled": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.retry.scheduled" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "attempt": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "at": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "attempt", + "at", + "error" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.compaction.admitted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.compaction.admitted" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "inputID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": [ + "sessionID", + "inputID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.compaction.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.compaction.started" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "recent": { + "type": "string" + }, + "inputID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": [ + "sessionID", + "reason", + "recent" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.compaction.ended": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.compaction.ended" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "text": { + "type": "string" + }, + "recent": { + "type": "string" + } + }, + "required": [ + "sessionID", + "reason", + "text", + "recent" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.compaction.failed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.compaction.failed" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + }, + "inputID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": [ + "sessionID", + "reason", + "error" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.revert.staged": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.revert.staged" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "revert": { + "$ref": "#/components/schemas/Session.Revert" + } + }, + "required": [ + "sessionID", + "revert" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.revert.cleared": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.revert.cleared" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + } + }, + "required": [ + "sessionID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.revert.committed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.revert.committed" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "to": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": [ + "sessionID", + "to" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.usage.recorded": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.usage.recorded" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "source": { + "type": "string", + "enum": [ + "title", + "compaction" + ] + }, + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" + } + }, + "required": [ + "sessionID", + "source", + "cost", + "tokens" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "Session.Event.Durable": { + "oneOf": [ + { + "$ref": "#/components/schemas/session.agent.selected" + }, + { + "$ref": "#/components/schemas/session.model.selected" + }, + { + "$ref": "#/components/schemas/session.moved" + }, + { + "$ref": "#/components/schemas/session.renamed" + }, + { + "$ref": "#/components/schemas/session.deleted" + }, + { + "$ref": "#/components/schemas/session.forked" + }, + { + "$ref": "#/components/schemas/session.input.promoted" + }, + { + "$ref": "#/components/schemas/session.input.admitted" + }, + { + "$ref": "#/components/schemas/session.execution.started" + }, + { + "$ref": "#/components/schemas/session.execution.succeeded" + }, + { + "$ref": "#/components/schemas/session.execution.failed" + }, + { + "$ref": "#/components/schemas/session.execution.interrupted" + }, + { + "$ref": "#/components/schemas/session.instructions.updated" + }, + { + "$ref": "#/components/schemas/session.synthetic" + }, + { + "$ref": "#/components/schemas/session.skill.activated" + }, + { + "$ref": "#/components/schemas/session.shell.started" + }, + { + "$ref": "#/components/schemas/session.shell.ended" + }, + { + "$ref": "#/components/schemas/session.step.started" + }, + { + "$ref": "#/components/schemas/session.step.ended" + }, + { + "$ref": "#/components/schemas/session.step.failed" + }, + { + "$ref": "#/components/schemas/session.text.started" + }, + { + "$ref": "#/components/schemas/session.text.ended" + }, + { + "$ref": "#/components/schemas/session.reasoning.started" + }, + { + "$ref": "#/components/schemas/session.reasoning.ended" + }, + { + "$ref": "#/components/schemas/session.tool.input.started" + }, + { + "$ref": "#/components/schemas/session.tool.input.ended" + }, + { + "$ref": "#/components/schemas/session.tool.called" + }, + { + "$ref": "#/components/schemas/session.tool.success" + }, + { + "$ref": "#/components/schemas/session.tool.failed" + }, + { + "$ref": "#/components/schemas/session.retry.scheduled" + }, + { + "$ref": "#/components/schemas/session.compaction.admitted" + }, + { + "$ref": "#/components/schemas/session.compaction.started" + }, + { + "$ref": "#/components/schemas/session.compaction.ended" + }, + { + "$ref": "#/components/schemas/session.compaction.failed" + }, + { + "$ref": "#/components/schemas/session.revert.staged" + }, + { + "$ref": "#/components/schemas/session.revert.cleared" + }, + { + "$ref": "#/components/schemas/session.revert.committed" + }, + { + "$ref": "#/components/schemas/session.usage.recorded" + } + ] + }, + "EventLog.Synced": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "log.synced" + ] + }, + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "type", + "aggregateID" + ], + "additionalProperties": false, + "description": "Marker emitted once when a log read reaches its captured watermark. The reader holds every event committed at or below seq." + }, + "SessionLogItem": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Event.Durable" + }, + { + "$ref": "#/components/schemas/EventLog.Synced" + } + ] + }, + "SessionLogItemJsonString": { + "type": "string", + "contentSchema": { + "$ref": "#/components/schemas/SessionLogItem" + }, + "contentMediaType": "application/json" + }, + "SessionMessagesResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Session.Message.Info" + } + }, + "cursor": { + "type": "object", + "properties": { + "previous": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "next": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "data", + "cursor" + ], + "additionalProperties": false + }, + "Model.ReasoningField": { + "anyOf": [ + { + "type": "string", + "enum": [ + "reasoning", + "reasoning_content", + "reasoning_text" + ] + }, + { + "type": "string" + } + ] + }, + "Model.Compatibility": { + "type": "object", + "properties": { + "reasoningField": { + "$ref": "#/components/schemas/Model.ReasoningField" + } + }, + "additionalProperties": false + }, + "Model.Capabilities": { + "type": "object", + "properties": { + "tools": { + "type": "boolean" + }, + "input": { + "type": "array", + "items": { + "type": "string" + } + }, + "output": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "tools", + "input", + "output" + ], + "additionalProperties": false + }, + "Model.Variant": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "settings": { + "type": "object" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "Money.USDPerMillionTokens": { + "type": "number" + }, + "Model.Cost": { + "type": "object", + "properties": { + "tier": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "context" + ] + }, + "size": { + "type": "integer" + } + }, + "required": [ + "type", + "size" + ], + "additionalProperties": false + }, + "input": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + "output": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + "write": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + } + }, + "required": [ + "read", + "write" + ], + "additionalProperties": false + } + }, + "required": [ + "input", + "output", + "cache" + ], + "additionalProperties": false + }, + "Model.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "modelID": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "family": { + "type": "string" + }, + "name": { + "type": "string" + }, + "compatibility": { + "$ref": "#/components/schemas/Model.Compatibility" + }, + "package": { + "type": "string" + }, + "settings": { + "type": "object" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + }, + "capabilities": { + "$ref": "#/components/schemas/Model.Capabilities" + }, + "variants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Model.Variant" + } + }, + "time": { + "type": "object", + "properties": { + "released": { + "type": "number" + } + }, + "required": [ + "released" + ], + "additionalProperties": false + }, + "cost": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Model.Cost" + } + }, + "status": { + "type": "string", + "enum": [ + "alpha", + "beta", + "deprecated", + "active" + ] + }, + "enabled": { + "type": "boolean" + }, + "limit": { + "type": "object", + "properties": { + "context": { + "type": "integer" + }, + "input": { + "type": "integer" + }, + "output": { + "type": "integer" + } + }, + "required": [ + "context", + "output" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "modelID", + "providerID", + "name", + "capabilities", + "variants", + "time", + "cost", + "status", + "enabled", + "limit" + ], + "additionalProperties": false + }, + "GenerateTextResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": [ + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "data" + ], + "additionalProperties": false + }, + "ProviderV2.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "integrationID": { + "type": "string" + }, + "name": { + "type": "string" + }, + "disabled": { + "type": "boolean" + }, + "package": { + "type": "string" + }, + "settings": { + "type": "object" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + } + }, + "required": [ + "id", + "name", + "package" + ], + "additionalProperties": false + }, + "ProviderNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "ProviderNotFoundError" + ] + }, + "providerID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "providerID", + "message" + ], + "additionalProperties": false + }, + "Integration.When": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "op": { + "type": "string", + "enum": [ + "eq", + "neq" + ] + }, + "value": { + "type": "string" + } + }, + "required": [ + "key", + "op", + "value" + ], + "additionalProperties": false + }, + "Integration.TextPrompt": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "text" + ] + }, + "key": { + "type": "string" + }, + "message": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "when": { + "$ref": "#/components/schemas/Integration.When" + } + }, + "required": [ + "type", + "key", + "message" + ], + "additionalProperties": false + }, + "Integration.SelectPrompt": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "select" + ] + }, + "key": { + "type": "string" + }, + "message": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + }, + "hint": { + "type": "string" + } + }, + "required": [ + "label", + "value" + ], + "additionalProperties": false + } + }, + "when": { + "$ref": "#/components/schemas/Integration.When" + } + }, + "required": [ + "type", + "key", + "message", + "options" + ], + "additionalProperties": false + }, + "Integration.OAuthMethod": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "oauth" + ] + }, + "label": { + "type": "string" + }, + "prompts": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Integration.TextPrompt" + }, + { + "$ref": "#/components/schemas/Integration.SelectPrompt" + } + ] + } + } + }, + "required": [ + "id", + "type", + "label" + ], + "additionalProperties": false + }, + "Integration.CommandMethod": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "command" + ] + }, + "label": { + "type": "string" + }, + "command": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "type", + "label", + "command" + ], + "additionalProperties": false + }, + "Integration.KeyMethod": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "key" + ] + }, + "label": { + "type": "string" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "Integration.EnvMethod": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "env" + ] + }, + "names": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "type", + "names" + ], + "additionalProperties": false + }, + "Integration.Method": { + "anyOf": [ + { + "$ref": "#/components/schemas/Integration.OAuthMethod" + }, + { + "$ref": "#/components/schemas/Integration.CommandMethod" + }, + { + "$ref": "#/components/schemas/Integration.KeyMethod" + }, + { + "$ref": "#/components/schemas/Integration.EnvMethod" + } + ] + }, + "Connection.CredentialInfo": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "credential" + ] + }, + "id": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": [ + "type", + "id", + "label" + ], + "additionalProperties": false + }, + "Connection.EnvInfo": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "env" + ] + }, + "name": { + "type": "string" + } + }, + "required": [ + "type", + "name" + ], + "additionalProperties": false + }, + "Connection.Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/Connection.CredentialInfo" + }, + { + "$ref": "#/components/schemas/Connection.EnvInfo" + } + ] + }, + "Integration.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "methods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Integration.Method" + } + }, + "connections": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Connection.Info" + } + } + }, + "required": [ + "id", + "name", + "methods", + "connections" + ], + "additionalProperties": false + }, + "Integration.Attempt": { + "type": "object", + "properties": { + "attemptID": { + "type": "string" + }, + "url": { + "type": "string" + }, + "instructions": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "auto", + "code" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "attemptID", + "url", + "instructions", + "mode", + "time" + ], + "additionalProperties": false + }, + "Integration.AttemptStatus": { + "anyOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "pending" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "time" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "complete" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "time" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "failed" + ] + }, + "message": { + "type": "string" + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "message", + "time" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "expired" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "time" + ], + "additionalProperties": false + } + ] + }, + "Integration.CommandAttempt": { + "type": "object", + "properties": { + "attemptID": { + "type": "string" + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "attemptID", + "time" + ], + "additionalProperties": false + }, + "Integration.CommandAttemptStatus": { + "anyOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "pending" + ] + }, + "message": { + "type": "string" + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "time" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "complete" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "time" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "failed" + ] + }, + "message": { + "type": "string" + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "message", + "time" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "expired" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "time" + ], + "additionalProperties": false + } + ] + }, + "Mcp.Status.Connected": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "connected" + ] + } + }, + "required": [ + "status" + ], + "additionalProperties": false + }, + "Mcp.Status.Pending": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "pending" + ] + } + }, + "required": [ + "status" + ], + "additionalProperties": false + }, + "Mcp.Status.Disabled": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "disabled" + ] + } + }, + "required": [ + "status" + ], + "additionalProperties": false + }, + "Mcp.Status.Failed": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "failed" + ] + }, + "error": { + "type": "string" + } + }, + "required": [ + "status", + "error" + ], + "additionalProperties": false + }, + "Mcp.Status.NeedsAuth": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "needs_auth" + ] + } + }, + "required": [ + "status" + ], + "additionalProperties": false + }, + "Mcp.Status.NeedsClientRegistration": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "needs_client_registration" + ] + }, + "error": { + "type": "string" + } + }, + "required": [ + "status", + "error" + ], + "additionalProperties": false + }, + "Mcp.Server": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "status": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.Status.Connected" + }, + { + "$ref": "#/components/schemas/Mcp.Status.Pending" + }, + { + "$ref": "#/components/schemas/Mcp.Status.Disabled" + }, + { + "$ref": "#/components/schemas/Mcp.Status.Failed" + }, + { + "$ref": "#/components/schemas/Mcp.Status.NeedsAuth" + }, + { + "$ref": "#/components/schemas/Mcp.Status.NeedsClientRegistration" + } + ] + }, + "integrationID": { + "type": "string" + } + }, + "required": [ + "name", + "status" + ], + "additionalProperties": false + }, + "Mcp.TimeoutConfig": { + "type": "object", + "properties": { + "startup": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ], + "description": "Maximum time in milliseconds to establish and initialize the MCP server." + }, + "catalog": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ], + "description": "Maximum time in milliseconds to wait for MCP discovery requests such as tools/list and prompts/list." + }, + "execution": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ], + "description": "Maximum time in milliseconds to wait for MCP tool and prompt execution." + } + }, + "additionalProperties": false + }, + "Mcp.LocalConfig": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "local" + ] + }, + "command": { + "type": "array", + "items": { + "type": "string" + } + }, + "cwd": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Working directory for the MCP server process. Relative paths resolve from the workspace directory." + }, + "environment": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "disabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "codemode": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Expose this server's tools through Code Mode. Defaults to true." + }, + "timeout": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.TimeoutConfig" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "type", + "command" + ], + "additionalProperties": false + }, + "Mcp.OAuthConfig": { + "type": "object", + "properties": { + "client_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "client_secret": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "scope": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "callback_port": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 1, + "maximum": 65535 + } + ] + }, + { + "type": "null" + } + ] + }, + "redirect_uri": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Mcp.RemoteConfig": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "remote" + ] + }, + "url": { + "type": "string" + }, + "headers": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "oauth": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.OAuthConfig" + }, + { + "type": "boolean", + "enum": [ + false + ] + } + ] + }, + { + "type": "null" + } + ] + }, + "disabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "codemode": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Expose this server's tools through Code Mode. Defaults to true." + }, + "timeout": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.TimeoutConfig" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "type", + "url" + ], + "additionalProperties": false + }, + "McpServerNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "McpServerNotFoundError" + ] + }, + "server": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "server", + "message" + ], + "additionalProperties": false + }, + "Mcp.Resource": { + "type": "object", + "properties": { + "server": { + "type": "string" + }, + "name": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "description": { + "type": "string" + }, + "mimeType": { + "type": "string" + } + }, + "required": [ + "server", + "name", + "uri" + ], + "additionalProperties": false + }, + "Mcp.ResourceTemplate": { + "type": "object", + "properties": { + "server": { + "type": "string" + }, + "name": { + "type": "string" + }, + "uriTemplate": { + "type": "string" + }, + "description": { + "type": "string" + }, + "mimeType": { + "type": "string" + } + }, + "required": [ + "server", + "name", + "uriTemplate" + ], + "additionalProperties": false + }, + "Mcp.ResourceCatalog": { + "type": "object", + "properties": { + "resources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Mcp.Resource" + } + }, + "templates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Mcp.ResourceTemplate" + } + } + }, + "required": [ + "resources", + "templates" + ], + "additionalProperties": false + }, + "Project.Vcs": { + "type": "string", + "enum": [ + "git", + "hg" + ] + }, + "Project.Icon": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "override": { + "type": "string" + }, + "color": { + "type": "string" + } + }, + "additionalProperties": false + }, + "Project.Commands": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "Startup script to run when creating a new workspace (worktree)" + } + }, + "additionalProperties": false + }, + "Project.Time": { + "type": "object", + "properties": { + "created": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "updated": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "initialized": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "created", + "updated" + ], + "additionalProperties": false + }, + "Project": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "worktree": { + "type": "string" + }, + "vcs": { + "$ref": "#/components/schemas/Project.Vcs" + }, + "name": { + "type": "string" + }, + "icon": { + "$ref": "#/components/schemas/Project.Icon" + }, + "commands": { + "$ref": "#/components/schemas/Project.Commands" + }, + "time": { + "$ref": "#/components/schemas/Project.Time" + }, + "sandboxes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "worktree", + "time", + "sandboxes" + ], + "additionalProperties": false + }, + "Project.Current": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "directory": { + "type": "string" + } + }, + "required": [ + "id", + "directory" + ], + "additionalProperties": false + }, + "Project.Directory": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "strategy": { + "type": "string" + } + }, + "required": [ + "directory" + ], + "additionalProperties": false + }, + "Project.Directories": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Project.Directory" + } + }, + "Form.Metadata": { + "type": "object" + }, + "Form.When": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "op": { + "type": "string", + "enum": [ + "eq", + "neq" + ] + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + { + "type": "boolean" + } + ] + } + }, + "required": [ + "key", + "op", + "value" + ], + "additionalProperties": false + }, + "Form.Option": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "label": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "value", + "label" + ], + "additionalProperties": false + }, + "Form.StringField": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When" + } + }, + "type": { + "type": "string", + "enum": [ + "string" + ] + }, + "format": { + "type": "string", + "enum": [ + "email", + "uri", + "date", + "date-time" + ] + }, + "minLength": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "maxLength": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "pattern": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "default": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Option" + } + }, + "custom": { + "type": "boolean" + } + }, + "required": [ + "key", + "type" + ], + "additionalProperties": false + }, + "Form.NumberField": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When" + } + }, + "type": { + "type": "string", + "enum": [ + "number" + ] + }, + "minimum": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "maximum": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "default": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "key", + "type" + ], + "additionalProperties": false + }, + "Form.IntegerField": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When" + } + }, + "type": { + "type": "string", + "enum": [ + "integer" + ] + }, + "minimum": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "maximum": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "default": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "key", + "type" + ], + "additionalProperties": false + }, + "Form.BooleanField": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When" + } + }, + "type": { + "type": "string", + "enum": [ + "boolean" + ] + }, + "default": { + "type": "boolean" + } + }, + "required": [ + "key", + "type" + ], + "additionalProperties": false + }, + "Form.MultiselectField": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When" + } + }, + "type": { + "type": "string", + "enum": [ + "multiselect" + ] + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Option" + } + }, + "minItems": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "maxItems": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "custom": { + "type": "boolean" + }, + "default": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "key", + "type", + "options" + ], + "additionalProperties": false + }, + "Form.ExternalField": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "external" + ] + }, + "url": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "key", + "type", + "url" + ], + "additionalProperties": false + }, + "Form.Field": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.StringField" + }, + { + "$ref": "#/components/schemas/Form.NumberField" + }, + { + "$ref": "#/components/schemas/Form.IntegerField" + }, + { + "$ref": "#/components/schemas/Form.BooleanField" + }, + { + "$ref": "#/components/schemas/Form.MultiselectField" + }, + { + "$ref": "#/components/schemas/Form.ExternalField" + } + ] + }, + "Form.Fields": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/Form.Field" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/Form.Field" + } + }, + "Form.Info": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "sessionID": { + "type": "string" + }, + "title": { + "type": "string" + }, + "metadata": { + "$ref": "#/components/schemas/Form.Metadata" + }, + "fields": { + "$ref": "#/components/schemas/Form.Fields" + } + }, + "required": [ + "id", + "sessionID", + "title", + "fields" + ], + "additionalProperties": false + }, + "Form.CreatePayload": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + { + "type": "null" + } + ] + }, + "title": { + "type": "string" + }, + "metadata": { + "$ref": "#/components/schemas/Form.Metadata" + }, + "fields": { + "$ref": "#/components/schemas/Form.Fields" + } + }, + "required": [ + "title", + "fields" + ], + "additionalProperties": false + }, + "FormNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "FormNotFoundError" + ] + }, + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "id", + "message" + ], + "additionalProperties": false + }, + "Form.Value": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "Form.Answer": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Form.Value" + } + }, + "Form.State": { + "anyOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "pending" + ] + } + }, + "required": [ + "status" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "answered" + ] + }, + "answer": { + "$ref": "#/components/schemas/Form.Answer" + } + }, + "required": [ + "status", + "answer" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "cancelled" + ] + } + }, + "required": [ + "status" + ], + "additionalProperties": false + } + ] + }, + "Form.Reply": { + "type": "object", + "properties": { + "answer": { + "$ref": "#/components/schemas/Form.Answer" + } + }, + "required": [ + "answer" + ], + "additionalProperties": false + }, + "FormAlreadySettledError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "FormAlreadySettledError" + ] + }, + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "id", + "message" + ], + "additionalProperties": false + }, + "FormInvalidAnswerError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "FormInvalidAnswerError" + ] + }, + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "id", + "message" + ], + "additionalProperties": false + }, + "PermissionV2.Source": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "tool" + ] + }, + "messageID": { + "type": "string" + }, + "callID": { + "type": "string" + } + }, + "required": [ + "type", + "messageID", + "callID" + ], + "additionalProperties": false + } + ] + }, + "PermissionV2.Request": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "action": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "save": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": { + "type": "object" + }, + "source": { + "$ref": "#/components/schemas/PermissionV2.Source" + } + }, + "required": [ + "id", + "sessionID", + "action", + "resources" + ], + "additionalProperties": false + }, + "PermissionSaved.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "projectID": { + "type": "string" + }, + "action": { + "type": "string" + }, + "resource": { + "type": "string" + } + }, + "required": [ + "id", + "projectID", + "action", + "resource" + ], + "additionalProperties": false + }, + "PermissionNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "PermissionNotFoundError" + ] + }, + "requestID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "requestID", + "message" + ], + "additionalProperties": false + }, + "PermissionV2.Reply": { + "type": "string", + "enum": [ + "once", + "always", + "reject" + ] + }, + "FileSystem.Entry": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "file", + "directory" + ] + } + }, + "required": [ + "path", + "type" + ], + "additionalProperties": false + }, + "Command.Info": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "template": { + "type": "string" + }, + "description": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "subtask": { + "type": "boolean" + } + }, + "required": [ + "name", + "template" + ], + "additionalProperties": false + }, + "Skill.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "slash": { + "type": "boolean" + }, + "autoinvoke": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "content": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "location", + "content" + ], + "additionalProperties": false + }, + "models-dev.refreshed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "models-dev.refreshed" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "integration.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "integration.updated" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "integration.connection.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "integration.connection.updated" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "integrationID": { + "type": "string" + } + }, + "required": [ + "integrationID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "catalog.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "catalog.updated" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "agent.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "agent.updated" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "FileDiff.LegacyInfo": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "patch": { + "type": "string" + }, + "additions": { + "type": "number" + }, + "deletions": { + "type": "number" + }, + "status": { + "type": "string", + "enum": [ + "added", + "deleted", + "modified" + ] + } + }, + "required": [ + "additions", + "deletions" + ], + "additionalProperties": false + }, + "PermissionAction": { + "type": "string", + "enum": [ + "allow", + "deny", + "ask" + ] + }, + "PermissionRule": { + "type": "object", + "properties": { + "permission": { + "type": "string" + }, + "pattern": { + "type": "string" + }, + "action": { + "$ref": "#/components/schemas/PermissionAction" + } + }, + "required": [ + "permission", + "pattern", + "action" + ], + "additionalProperties": false + }, + "PermissionRuleset": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionRule" + } + }, + "SessionV1.Info": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "slug": { + "type": "string" + }, + "projectID": { + "type": "string" + }, + "workspaceID": { + "type": "string", + "allOf": [ + { + "pattern": "^wrk" + } + ] + }, + "directory": { + "type": "string" + }, + "path": { + "type": "string" + }, + "parentID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "summary": { + "type": "object", + "properties": { + "additions": { + "type": "number" + }, + "deletions": { + "type": "number" + }, + "files": { + "type": "number" + }, + "diffs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileDiff.LegacyInfo" + } + } + }, + "required": [ + "additions", + "deletions", + "files" + ], + "additionalProperties": false + }, + "cost": { + "type": "number" + }, + "tokens": { + "type": "object", + "properties": { + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "reasoning": { + "type": "number" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": [ + "read", + "write" + ], + "additionalProperties": false + } + }, + "required": [ + "input", + "output", + "reasoning", + "cache" + ], + "additionalProperties": false + }, + "share": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "additionalProperties": false + }, + "title": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": [ + "id", + "providerID" + ], + "additionalProperties": false + }, + "version": { + "type": "string" + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "updated": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "compacting": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "archived": { + "type": "number" + } + }, + "required": [ + "created", + "updated" + ], + "additionalProperties": false + }, + "permission": { + "$ref": "#/components/schemas/PermissionRuleset" + }, + "revert": { + "type": "object", + "properties": { + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "partID": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "snapshot": { + "type": "string" + }, + "diff": { + "type": "string" + } + }, + "required": [ + "messageID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "slug", + "projectID", + "directory", + "title", + "version", + "time" + ], + "additionalProperties": false + }, + "session.created": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.created" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "info": { + "$ref": "#/components/schemas/SessionV1.Info" + } + }, + "required": [ + "sessionID", + "info" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "info": { + "$ref": "#/components/schemas/SessionV1.Info" + } + }, + "required": [ + "sessionID", + "info" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.deleted1": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.deleted" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "info": { + "$ref": "#/components/schemas/SessionV1.Info" + } + }, + "required": [ + "sessionID", + "info" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "JSONSchema": { + "type": "object" + }, + "OutputFormat": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "text" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "json_schema" + ] + }, + "schema": { + "$ref": "#/components/schemas/JSONSchema" + }, + "retryCount": { + "anyOf": [ + { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "type", + "schema" + ], + "additionalProperties": false + } + ] + }, + "UserMessage": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "role": { + "type": "string", + "enum": [ + "user" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "format": { + "anyOf": [ + { + "$ref": "#/components/schemas/OutputFormat" + }, + { + "type": "null" + } + ] + }, + "summary": { + "anyOf": [ + { + "type": "object", + "properties": { + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "body": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "diffs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileDiff.LegacyInfo" + } + } + }, + "required": [ + "diffs" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "agent": { + "type": "string" + }, + "model": { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "modelID": { + "type": "string" + }, + "variant": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "providerID", + "modelID" + ], + "additionalProperties": false + }, + "system": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "tools": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "role", + "time", + "agent", + "model" + ], + "additionalProperties": false + }, + "ProviderAuthError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "ProviderAuthError" + ] + }, + "data": { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "providerID", + "message" + ], + "additionalProperties": false + } + }, + "required": [ + "name", + "data" + ], + "additionalProperties": false + }, + "UnknownError1": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "UnknownError" + ] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + }, + "required": [ + "name", + "data" + ], + "additionalProperties": false + }, + "MessageOutputLengthError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "MessageOutputLengthError" + ] + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "name", + "data" + ], + "additionalProperties": false + }, + "MessageAbortedError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "MessageAbortedError" + ] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + }, + "required": [ + "name", + "data" + ], + "additionalProperties": false + }, + "StructuredOutputError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "StructuredOutputError" + ] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "retries": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "message", + "retries" + ], + "additionalProperties": false + } + }, + "required": [ + "name", + "data" + ], + "additionalProperties": false + }, + "ContextOverflowError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "ContextOverflowError" + ] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "responseBody": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + }, + "required": [ + "name", + "data" + ], + "additionalProperties": false + }, + "ContentFilterError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "ContentFilterError" + ] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + }, + "required": [ + "name", + "data" + ], + "additionalProperties": false + }, + "APIError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "APIError" + ] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "statusCode": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + "isRetryable": { + "type": "boolean" + }, + "responseHeaders": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "responseBody": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "message", + "isRetryable" + ], + "additionalProperties": false + } + }, + "required": [ + "name", + "data" + ], + "additionalProperties": false + }, + "AssistantMessage": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "role": { + "type": "string", + "enum": [ + "assistant" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "completed": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "error": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/components/schemas/ProviderAuthError" + }, + { + "$ref": "#/components/schemas/UnknownError1" + }, + { + "$ref": "#/components/schemas/MessageOutputLengthError" + }, + { + "$ref": "#/components/schemas/MessageAbortedError" + }, + { + "$ref": "#/components/schemas/StructuredOutputError" + }, + { + "$ref": "#/components/schemas/ContextOverflowError" + }, + { + "$ref": "#/components/schemas/ContentFilterError" + }, + { + "$ref": "#/components/schemas/APIError" + } + ] + }, + { + "type": "null" + } + ] + }, + "parentID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "modelID": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "path": { + "type": "object", + "properties": { + "cwd": { + "type": "string" + }, + "root": { + "type": "string" + } + }, + "required": [ + "cwd", + "root" + ], + "additionalProperties": false + }, + "summary": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "cost": { + "type": "number" + }, + "tokens": { + "type": "object", + "properties": { + "total": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "reasoning": { + "type": "number" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": [ + "read", + "write" + ], + "additionalProperties": false + } + }, + "required": [ + "input", + "output", + "reasoning", + "cache" + ], + "additionalProperties": false + }, + "structured": { + "anyOf": [ + {}, + { + "type": "null" + } + ] + }, + "variant": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "finish": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "role", + "time", + "parentID", + "modelID", + "providerID", + "mode", + "agent", + "path", + "cost", + "tokens" + ], + "additionalProperties": false + }, + "Message": { + "anyOf": [ + { + "$ref": "#/components/schemas/UserMessage" + }, + { + "$ref": "#/components/schemas/AssistantMessage" + } + ] + }, + "message.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "message.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "info": { + "$ref": "#/components/schemas/Message" + } + }, + "required": [ + "sessionID", + "info" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "message.removed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "message.removed" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + } + }, + "required": [ + "sessionID", + "messageID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "TextPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "text" + ] + }, + "text": { + "type": "string" + }, + "synthetic": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "ignored": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "time": { + "anyOf": [ + { + "type": "object", + "properties": { + "start": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "end": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "start" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "metadata": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "text" + ], + "additionalProperties": false + }, + "SubtaskPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "subtask" + ] + }, + "prompt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { + "anyOf": [ + { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "modelID": { + "type": "string" + } + }, + "required": [ + "providerID", + "modelID" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "command": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "prompt", + "description", + "agent" + ], + "additionalProperties": false + }, + "ReasoningPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "reasoning" + ] + }, + "text": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "end": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "start" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "text", + "time" + ], + "additionalProperties": false + }, + "FilePartSourceText": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "start": { + "type": "number" + }, + "end": { + "type": "number" + } + }, + "required": [ + "value", + "start", + "end" + ], + "additionalProperties": false + }, + "FileSource": { + "type": "object", + "properties": { + "text": { + "$ref": "#/components/schemas/FilePartSourceText" + }, + "type": { + "type": "string", + "enum": [ + "file" + ] + }, + "path": { + "type": "string" + } + }, + "required": [ + "text", + "type", + "path" + ], + "additionalProperties": false + }, + "Range": { + "type": "object", + "properties": { + "start": { + "type": "object", + "properties": { + "line": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "character": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "line", + "character" + ], + "additionalProperties": false + }, + "end": { + "type": "object", + "properties": { + "line": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "character": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "line", + "character" + ], + "additionalProperties": false + } + }, + "required": [ + "start", + "end" + ], + "additionalProperties": false + }, + "SymbolSource": { + "type": "object", + "properties": { + "text": { + "$ref": "#/components/schemas/FilePartSourceText" + }, + "type": { + "type": "string", + "enum": [ + "symbol" + ] + }, + "path": { + "type": "string" + }, + "range": { + "$ref": "#/components/schemas/Range" + }, + "name": { + "type": "string" + }, + "kind": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "text", + "type", + "path", + "range", + "name", + "kind" + ], + "additionalProperties": false + }, + "ResourceSource": { + "type": "object", + "properties": { + "text": { + "$ref": "#/components/schemas/FilePartSourceText" + }, + "type": { + "type": "string", + "enum": [ + "resource" + ] + }, + "clientName": { + "type": "string" + }, + "uri": { + "type": "string" + } + }, + "required": [ + "text", + "type", + "clientName", + "uri" + ], + "additionalProperties": false + }, + "FilePartSource": { + "anyOf": [ + { + "$ref": "#/components/schemas/FileSource" + }, + { + "$ref": "#/components/schemas/SymbolSource" + }, + { + "$ref": "#/components/schemas/ResourceSource" + } + ] + }, + "FilePart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "file" + ] + }, + "mime": { + "type": "string" + }, + "filename": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "url": { + "type": "string" + }, + "source": { + "anyOf": [ + { + "$ref": "#/components/schemas/FilePartSource" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "mime", + "url" + ], + "additionalProperties": false + }, + "ToolStatePending": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "pending" + ] + }, + "input": { + "type": "object" + }, + "raw": { + "type": "string" + } + }, + "required": [ + "status", + "input", + "raw" + ], + "additionalProperties": false + }, + "ToolStateRunning": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "running" + ] + }, + "input": { + "type": "object" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "start" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "input", + "time" + ], + "additionalProperties": false + }, + "ToolStateCompleted": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "completed" + ] + }, + "input": { + "type": "object" + }, + "output": { + "type": "string" + }, + "title": { + "type": "string" + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "end": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "compacted": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "start", + "end" + ], + "additionalProperties": false + }, + "attachments": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/FilePart" + } + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "status", + "input", + "output", + "title", + "metadata", + "time" + ], + "additionalProperties": false + }, + "ToolStateError": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "error" + ] + }, + "input": { + "type": "object" + }, + "error": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "end": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "start", + "end" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "input", + "error", + "time" + ], + "additionalProperties": false + }, + "ToolState": { + "anyOf": [ + { + "$ref": "#/components/schemas/ToolStatePending" + }, + { + "$ref": "#/components/schemas/ToolStateRunning" + }, + { + "$ref": "#/components/schemas/ToolStateCompleted" + }, + { + "$ref": "#/components/schemas/ToolStateError" + } + ] + }, + "ToolPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "tool" + ] + }, + "callID": { + "type": "string" + }, + "tool": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/ToolState" + }, + "metadata": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "callID", + "tool", + "state" + ], + "additionalProperties": false + }, + "StepStartPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "step-start" + ] + }, + "snapshot": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type" + ], + "additionalProperties": false + }, + "StepFinishPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "step-finish" + ] + }, + "reason": { + "type": "string" + }, + "snapshot": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "cost": { + "type": "number" + }, + "tokens": { + "type": "object", + "properties": { + "total": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "reasoning": { + "type": "number" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": [ + "read", + "write" + ], + "additionalProperties": false + } + }, + "required": [ + "input", + "output", + "reasoning", + "cache" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "reason", + "cost", + "tokens" + ], + "additionalProperties": false + }, + "SnapshotPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "snapshot" + ] + }, + "snapshot": { + "type": "string" + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "snapshot" + ], + "additionalProperties": false + }, + "PatchPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "patch" + ] + }, + "hash": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "hash", + "files" + ], + "additionalProperties": false + }, + "AgentPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "agent" + ] + }, + "name": { + "type": "string" + }, + "source": { + "anyOf": [ + { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "start": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "end": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "value", + "start", + "end" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "name" + ], + "additionalProperties": false + }, + "RetryPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "retry" + ] + }, + "attempt": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "error": { + "$ref": "#/components/schemas/APIError" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "created" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "attempt", + "error", + "time" + ], + "additionalProperties": false + }, + "CompactionPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "compaction" + ] + }, + "auto": { + "type": "boolean" + }, + "overflow": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "tail_start_id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "auto" + ], + "additionalProperties": false + }, + "Part": { + "anyOf": [ + { + "$ref": "#/components/schemas/TextPart" + }, + { + "$ref": "#/components/schemas/SubtaskPart" + }, + { + "$ref": "#/components/schemas/ReasoningPart" + }, + { + "$ref": "#/components/schemas/FilePart" + }, + { + "$ref": "#/components/schemas/ToolPart" + }, + { + "$ref": "#/components/schemas/StepStartPart" + }, + { + "$ref": "#/components/schemas/StepFinishPart" + }, + { + "$ref": "#/components/schemas/SnapshotPart" + }, + { + "$ref": "#/components/schemas/PatchPart" + }, + { + "$ref": "#/components/schemas/AgentPart" + }, + { + "$ref": "#/components/schemas/RetryPart" + }, + { + "$ref": "#/components/schemas/CompactionPart" + } + ] + }, + "message.part.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "message.part.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "part": { + "$ref": "#/components/schemas/Part" + }, + "time": { + "type": "number" + } + }, + "required": [ + "sessionID", + "part", + "time" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "message.part.removed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "message.part.removed" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "partID": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + } + }, + "required": [ + "sessionID", + "messageID", + "partID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.usage.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.usage.updated" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" + } + }, + "required": [ + "sessionID", + "cost", + "tokens" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "session.text.delta": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.text.delta" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "delta": { + "type": "string" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "ordinal", + "delta" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "session.reasoning.delta": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.reasoning.delta" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "delta": { + "type": "string" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "ordinal", + "delta" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "session.tool.input.delta": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.tool.input.delta" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "delta": { + "type": "string" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "callID", + "delta" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "session.tool.progress": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.tool.progress" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "callID", + "metadata" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "session.compaction.delta": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.compaction.delta" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "text": { + "type": "string" + } + }, + "required": [ + "sessionID", + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "filesystem.changed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "filesystem.changed" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "event": { + "type": "string", + "enum": [ + "add", + "change", + "unlink" + ] + } + }, + "required": [ + "file", + "event" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "reference.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "reference.updated" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "permission.v2.asked": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "permission.v2.asked" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "action": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "save": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": { + "type": "object" + }, + "source": { + "$ref": "#/components/schemas/PermissionV2.Source" + } + }, + "required": [ + "id", + "sessionID", + "action", + "resources" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "permission.v2.replied": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "permission.v2.replied" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "requestID": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "reply": { + "$ref": "#/components/schemas/PermissionV2.Reply" + } + }, + "required": [ + "sessionID", + "requestID", + "reply" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "plugin.added": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "plugin.added" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "plugin.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "plugin.updated" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "project.directories.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "project.directories.updated" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "projectID": { + "type": "string" + } + }, + "required": [ + "projectID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "command.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "command.updated" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "config.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "config.updated" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "skill.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "skill.updated" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "Pty": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "title": { + "type": "string" + }, + "command": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "cwd": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "running", + "exited" + ] + }, + "pid": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "exitCode": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "id", + "title", + "command", + "args", + "cwd", + "status", + "pid" + ], + "additionalProperties": false + }, + "pty.created": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "pty.created" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/Pty" + } + }, + "required": [ + "info" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "pty.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "pty.updated" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/Pty" + } + }, + "required": [ + "info" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "pty.exited": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "pty.exited" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "exitCode": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "id", + "exitCode" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "pty.deleted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "pty.deleted" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "shell.created": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "shell.created" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/Shell.Info" + } + }, + "required": [ + "info" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "shell.exited": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "shell.exited" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "exit": { + "type": "number" + }, + "status": { + "type": "string", + "enum": [ + "running", + "exited", + "timeout", + "killed" + ] + } + }, + "required": [ + "id", + "status" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "shell.deleted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "shell.deleted" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "QuestionV2.Option": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Display text (1-5 words, concise)" + }, + "description": { + "type": "string", + "description": "Explanation of choice" + } + }, + "required": [ + "label", + "description" + ], + "additionalProperties": false + }, + "QuestionV2.Info": { + "type": "object", + "properties": { + "question": { + "type": "string", + "description": "Complete question" + }, + "header": { + "type": "string", + "description": "Very short label (max 30 chars)" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Option" + }, + "description": "Available choices" + }, + "multiple": { + "type": "boolean" + }, + "custom": { + "type": "boolean" + } + }, + "required": [ + "question", + "header", + "options" + ], + "additionalProperties": false + }, + "QuestionV2.Tool": { + "type": "object", + "properties": { + "messageID": { + "type": "string" + }, + "callID": { + "type": "string" + } + }, + "required": [ + "messageID", + "callID" + ], + "additionalProperties": false + }, + "question.v2.asked": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "question.v2.asked" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "questions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Info" + }, + "description": "Questions to ask" + }, + "tool": { + "$ref": "#/components/schemas/QuestionV2.Tool" + } + }, + "required": [ + "id", + "sessionID", + "questions" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "QuestionV2.Answer": { + "type": "array", + "items": { + "type": "string" + } + }, + "question.v2.replied": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "question.v2.replied" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "requestID": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Answer" + } + } + }, + "required": [ + "sessionID", + "requestID", + "answers" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "question.v2.rejected": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "question.v2.rejected" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "requestID": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + } + }, + "required": [ + "sessionID", + "requestID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "Form.Metadata1": { + "type": "object" + }, + "Form.When1": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "op": { + "type": "string", + "enum": [ + "eq", + "neq" + ] + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "boolean" + } + ] + } + }, + "required": [ + "key", + "op", + "value" + ], + "additionalProperties": false + }, + "Form.StringField1": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When1" + } + }, + "type": { + "type": "string", + "enum": [ + "string" + ] + }, + "format": { + "type": "string", + "enum": [ + "email", + "uri", + "date", + "date-time" + ] + }, + "minLength": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "maxLength": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "pattern": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "default": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Option" + } + }, + "custom": { + "type": "boolean" + } + }, + "required": [ + "key", + "type" + ], + "additionalProperties": false + }, + "Form.NumberField1": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When1" + } + }, + "type": { + "type": "string", + "enum": [ + "number" + ] + }, + "minimum": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + "maximum": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + "default": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + } + }, + "required": [ + "key", + "type" + ], + "additionalProperties": false + }, + "Form.IntegerField1": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When1" + } + }, + "type": { + "type": "string", + "enum": [ + "integer" + ] + }, + "minimum": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + "maximum": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + "default": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + } + }, + "required": [ + "key", + "type" + ], + "additionalProperties": false + }, + "Form.BooleanField1": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When1" + } + }, + "type": { + "type": "string", + "enum": [ + "boolean" + ] + }, + "default": { + "type": "boolean" + } + }, + "required": [ + "key", + "type" + ], + "additionalProperties": false + }, + "Form.MultiselectField1": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When1" + } + }, + "type": { + "type": "string", + "enum": [ + "multiselect" + ] + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Option" + } + }, + "minItems": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "maxItems": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "custom": { + "type": "boolean" + }, + "default": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "key", + "type", + "options" + ], + "additionalProperties": false + }, + "Form.Field1": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.StringField1" + }, + { + "$ref": "#/components/schemas/Form.NumberField1" + }, + { + "$ref": "#/components/schemas/Form.IntegerField1" + }, + { + "$ref": "#/components/schemas/Form.BooleanField1" + }, + { + "$ref": "#/components/schemas/Form.MultiselectField1" + }, + { + "$ref": "#/components/schemas/Form.ExternalField" + } + ] + }, + "Form.Fields1": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/Form.Field1" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/Form.Field1" + } + }, + "Form.Info1": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "sessionID": { + "type": "string" + }, + "title": { + "type": "string" + }, + "metadata": { + "$ref": "#/components/schemas/Form.Metadata1" + }, + "fields": { + "$ref": "#/components/schemas/Form.Fields1" + } + }, + "required": [ + "id", + "sessionID", + "title", + "fields" + ], + "additionalProperties": false + }, + "form.created": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "form.created" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "form": { + "$ref": "#/components/schemas/Form.Info1" + } + }, + "required": [ + "form" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "Form.Value1": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "Form.Answer1": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Form.Value1" + } + }, + "form.replied": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "form.replied" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "sessionID": { + "type": "string" + }, + "answer": { + "$ref": "#/components/schemas/Form.Answer1" + } + }, + "required": [ + "id", + "sessionID", + "answer" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "form.cancelled": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "form.cancelled" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "sessionID": { + "type": "string" + } + }, + "required": [ + "id", + "sessionID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "websearch.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "websearch.updated" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "SessionStatus": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "idle" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "retry" + ] + }, + "attempt": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "message": { + "type": "string" + }, + "action": { + "type": "object", + "properties": { + "reason": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "title": { + "type": "string" + }, + "message": { + "type": "string" + }, + "label": { + "type": "string" + }, + "link": { + "type": "string" + } + }, + "required": [ + "reason", + "provider", + "title", + "message", + "label" + ], + "additionalProperties": false + }, + "next": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "type", + "attempt", + "message", + "next" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "busy" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + ] + }, + "session.status": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.status" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "status": { + "$ref": "#/components/schemas/SessionStatus" + } + }, + "required": [ + "sessionID", + "status" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "session.idle": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.idle" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + } + }, + "required": [ + "sessionID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "tui.prompt.append": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "tui.prompt.append" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": [ + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "tui.command.execute": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "tui.command.execute" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "command": { + "anyOf": [ + { + "type": "string", + "enum": [ + "session.list", + "session.new", + "session.share", + "session.interrupt", + "session.background", + "session.compact", + "session.page.up", + "session.page.down", + "session.line.up", + "session.line.down", + "session.half.page.up", + "session.half.page.down", + "session.first", + "session.last", + "prompt.clear", + "prompt.submit", + "agent.cycle" + ] + }, + { + "type": "string" + } + ] + } + }, + "required": [ + "command" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "tui.toast.show": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "tui.toast.show" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "message": { + "type": "string" + }, + "variant": { + "type": "string", + "enum": [ + "info", + "success", + "warning", + "error" + ] + }, + "duration": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "message", + "variant" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "tui.session.select": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "tui.session.select" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses", + "description": "Session ID to navigate to" + } + ] + } + }, + "required": [ + "sessionID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "installation.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "installation.updated" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": [ + "version" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "installation.update-available": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "installation.update-available" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": [ + "version" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "vcs.branch.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "vcs.branch.updated" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "branch": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "mcp.status.changed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "mcp.status.changed" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "server": { + "type": "string" + } + }, + "required": [ + "server" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "mcp.resources.changed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "mcp.resources.changed" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "server": { + "type": "string" + } + }, + "required": [ + "server" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "permission.asked": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "permission.asked" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "permission": { + "type": "string" + }, + "patterns": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": { + "type": "object" + }, + "always": { + "type": "array", + "items": { + "type": "string" + } + }, + "tool": { + "anyOf": [ + { + "type": "object", + "properties": { + "messageID": { + "type": "string" + }, + "callID": { + "type": "string" + } + }, + "required": [ + "messageID", + "callID" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "permission", + "patterns", + "metadata", + "always" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "permission.replied": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "permission.replied" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "requestID": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "reply": { + "type": "string", + "enum": [ + "once", + "always", + "reject" + ] + } + }, + "required": [ + "sessionID", + "requestID", + "reply" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "QuestionOption": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Display text (1-5 words, concise)" + }, + "description": { + "type": "string", + "description": "Explanation of choice" + } + }, + "required": [ + "label", + "description" + ], + "additionalProperties": false + }, + "QuestionInfo": { + "type": "object", + "properties": { + "question": { + "type": "string", + "description": "Complete question" + }, + "header": { + "type": "string", + "description": "Very short label (max 30 chars)" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionOption" + }, + "description": "Available choices" + }, + "multiple": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Allow selecting multiple choices" + }, + "custom": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Allow typing a custom answer (default: true)" + } + }, + "required": [ + "question", + "header", + "options" + ], + "additionalProperties": false + }, + "QuestionTool": { + "type": "object", + "properties": { + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "callID": { + "type": "string" + } + }, + "required": [ + "messageID", + "callID" + ], + "additionalProperties": false + }, + "question.asked": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "question.asked" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "questions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionInfo" + }, + "description": "Questions to ask" + }, + "tool": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionTool" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "questions" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "QuestionAnswer": { + "type": "array", + "items": { + "type": "string" + } + }, + "question.replied": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "question.replied" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "requestID": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionAnswer" + } + } + }, + "required": [ + "sessionID", + "requestID", + "answers" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "question.rejected": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "question.rejected" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "requestID": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + } + }, + "required": [ + "sessionID", + "requestID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "session.error": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.error" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + { + "type": "null" + } + ] + }, + "error": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/components/schemas/ProviderAuthError" + }, + { + "$ref": "#/components/schemas/UnknownError1" + }, + { + "$ref": "#/components/schemas/MessageOutputLengthError" + }, + { + "$ref": "#/components/schemas/MessageAbortedError" + }, + { + "$ref": "#/components/schemas/StructuredOutputError" + }, + { + "$ref": "#/components/schemas/ContextOverflowError" + }, + { + "$ref": "#/components/schemas/ContentFilterError" + }, + { + "$ref": "#/components/schemas/APIError" + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "V2Event.server.connected": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "location": { + "anyOf": [ + { + "$ref": "#/components/schemas/Location.Ref" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "server.connected" + ] + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "V2Event": { + "anyOf": [ + { + "$ref": "#/components/schemas/models-dev.refreshed" + }, + { + "$ref": "#/components/schemas/integration.updated" + }, + { + "$ref": "#/components/schemas/integration.connection.updated" + }, + { + "$ref": "#/components/schemas/catalog.updated" + }, + { + "$ref": "#/components/schemas/agent.updated" + }, + { + "$ref": "#/components/schemas/session.created" + }, + { + "$ref": "#/components/schemas/session.updated" + }, + { + "$ref": "#/components/schemas/session.deleted1" + }, + { + "$ref": "#/components/schemas/message.updated" + }, + { + "$ref": "#/components/schemas/message.removed" + }, + { + "$ref": "#/components/schemas/message.part.updated" + }, + { + "$ref": "#/components/schemas/message.part.removed" + }, + { + "$ref": "#/components/schemas/session.agent.selected" + }, + { + "$ref": "#/components/schemas/session.model.selected" + }, + { + "$ref": "#/components/schemas/session.moved" + }, + { + "$ref": "#/components/schemas/session.renamed" + }, + { + "$ref": "#/components/schemas/session.usage.updated" + }, + { + "$ref": "#/components/schemas/session.deleted" + }, + { + "$ref": "#/components/schemas/session.forked" + }, + { + "$ref": "#/components/schemas/session.input.promoted" + }, + { + "$ref": "#/components/schemas/session.input.admitted" + }, + { + "$ref": "#/components/schemas/session.execution.started" + }, + { + "$ref": "#/components/schemas/session.execution.succeeded" + }, + { + "$ref": "#/components/schemas/session.execution.failed" + }, + { + "$ref": "#/components/schemas/session.execution.interrupted" + }, + { + "$ref": "#/components/schemas/session.instructions.updated" + }, + { + "$ref": "#/components/schemas/session.synthetic" + }, + { + "$ref": "#/components/schemas/session.skill.activated" + }, + { + "$ref": "#/components/schemas/session.shell.started" + }, + { + "$ref": "#/components/schemas/session.shell.ended" + }, + { + "$ref": "#/components/schemas/session.step.started" + }, + { + "$ref": "#/components/schemas/session.step.ended" + }, + { + "$ref": "#/components/schemas/session.step.failed" + }, + { + "$ref": "#/components/schemas/session.text.started" + }, + { + "$ref": "#/components/schemas/session.text.delta" + }, + { + "$ref": "#/components/schemas/session.text.ended" + }, + { + "$ref": "#/components/schemas/session.reasoning.started" + }, + { + "$ref": "#/components/schemas/session.reasoning.delta" + }, + { + "$ref": "#/components/schemas/session.reasoning.ended" + }, + { + "$ref": "#/components/schemas/session.tool.input.started" + }, + { + "$ref": "#/components/schemas/session.tool.input.delta" + }, + { + "$ref": "#/components/schemas/session.tool.input.ended" + }, + { + "$ref": "#/components/schemas/session.tool.called" + }, + { + "$ref": "#/components/schemas/session.tool.progress" + }, + { + "$ref": "#/components/schemas/session.tool.success" + }, + { + "$ref": "#/components/schemas/session.tool.failed" + }, + { + "$ref": "#/components/schemas/session.retry.scheduled" + }, + { + "$ref": "#/components/schemas/session.compaction.admitted" + }, + { + "$ref": "#/components/schemas/session.compaction.started" + }, + { + "$ref": "#/components/schemas/session.compaction.delta" + }, + { + "$ref": "#/components/schemas/session.compaction.ended" + }, + { + "$ref": "#/components/schemas/session.compaction.failed" + }, + { + "$ref": "#/components/schemas/session.revert.staged" + }, + { + "$ref": "#/components/schemas/session.revert.cleared" + }, + { + "$ref": "#/components/schemas/session.revert.committed" + }, + { + "$ref": "#/components/schemas/filesystem.changed" + }, + { + "$ref": "#/components/schemas/reference.updated" + }, + { + "$ref": "#/components/schemas/permission.v2.asked" + }, + { + "$ref": "#/components/schemas/permission.v2.replied" + }, + { + "$ref": "#/components/schemas/plugin.added" + }, + { + "$ref": "#/components/schemas/plugin.updated" + }, + { + "$ref": "#/components/schemas/project.directories.updated" + }, + { + "$ref": "#/components/schemas/command.updated" + }, + { + "$ref": "#/components/schemas/config.updated" + }, + { + "$ref": "#/components/schemas/skill.updated" + }, + { + "$ref": "#/components/schemas/pty.created" + }, + { + "$ref": "#/components/schemas/pty.updated" + }, + { + "$ref": "#/components/schemas/pty.exited" + }, + { + "$ref": "#/components/schemas/pty.deleted" + }, + { + "$ref": "#/components/schemas/shell.created" + }, + { + "$ref": "#/components/schemas/shell.exited" + }, + { + "$ref": "#/components/schemas/shell.deleted" + }, + { + "$ref": "#/components/schemas/question.v2.asked" + }, + { + "$ref": "#/components/schemas/question.v2.replied" + }, + { + "$ref": "#/components/schemas/question.v2.rejected" + }, + { + "$ref": "#/components/schemas/form.created" + }, + { + "$ref": "#/components/schemas/form.replied" + }, + { + "$ref": "#/components/schemas/form.cancelled" + }, + { + "$ref": "#/components/schemas/websearch.updated" + }, + { + "$ref": "#/components/schemas/session.status" + }, + { + "$ref": "#/components/schemas/session.idle" + }, + { + "$ref": "#/components/schemas/tui.prompt.append" + }, + { + "$ref": "#/components/schemas/tui.command.execute" + }, + { + "$ref": "#/components/schemas/tui.toast.show" + }, + { + "$ref": "#/components/schemas/tui.session.select" + }, + { + "$ref": "#/components/schemas/installation.updated" + }, + { + "$ref": "#/components/schemas/installation.update-available" + }, + { + "$ref": "#/components/schemas/vcs.branch.updated" + }, + { + "$ref": "#/components/schemas/mcp.status.changed" + }, + { + "$ref": "#/components/schemas/mcp.resources.changed" + }, + { + "$ref": "#/components/schemas/permission.asked" + }, + { + "$ref": "#/components/schemas/permission.replied" + }, + { + "$ref": "#/components/schemas/question.asked" + }, + { + "$ref": "#/components/schemas/question.replied" + }, + { + "$ref": "#/components/schemas/question.rejected" + }, + { + "$ref": "#/components/schemas/session.error" + }, + { + "$ref": "#/components/schemas/V2Event.server.connected" + } + ] + }, + "V2EventJsonString": { + "type": "string", + "contentSchema": { + "$ref": "#/components/schemas/V2Event" + }, + "contentMediaType": "application/json" + }, + "PtyNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "PtyNotFoundError" + ] + }, + "ptyID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "ptyID", + "message" + ], + "additionalProperties": false + }, + "PtyTicket.ConnectToken": { + "type": "object", + "properties": { + "ticket": { + "type": "string" + }, + "expires_in": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + } + }, + "required": [ + "ticket", + "expires_in" + ], + "additionalProperties": false + }, + "ForbiddenError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "ForbiddenError" + ] + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "Shell.Info1": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "status": { + "type": "string", + "enum": [ + "running", + "exited", + "timeout", + "killed" + ] + }, + "command": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "shell": { + "type": "string" + }, + "file": { + "type": "string" + }, + "pid": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "exit": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "started": { + "type": "number" + }, + "completed": { + "type": "number" + } + }, + "required": [ + "started" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "status", + "command", + "cwd", + "shell", + "file", + "metadata", + "time" + ], + "additionalProperties": false + }, + "ShellNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "ShellNotFoundError" + ] + }, + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "id", + "message" + ], + "additionalProperties": false + }, + "QuestionV2.Request": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "questions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Info" + }, + "description": "Questions to ask" + }, + "tool": { + "$ref": "#/components/schemas/QuestionV2.Tool" + } + }, + "required": [ + "id", + "sessionID", + "questions" + ], + "additionalProperties": false + }, + "QuestionV2.Reply": { + "type": "object", + "properties": { + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Answer" + }, + "description": "User answers in order of questions (each answer is an array of selected labels)" + } + }, + "required": [ + "answers" + ], + "additionalProperties": false + }, + "QuestionNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "QuestionNotFoundError" + ] + }, + "requestID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "requestID", + "message" + ], + "additionalProperties": false + }, + "Reference.LocalSource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "local" + ] + }, + "path": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + } + }, + "required": [ + "type", + "path" + ], + "additionalProperties": false + }, + "Reference.GitSource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "git" + ] + }, + "repository": { + "type": "string" + }, + "branch": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + } + }, + "required": [ + "type", + "repository" + ], + "additionalProperties": false + }, + "Reference.Source": { + "anyOf": [ + { + "$ref": "#/components/schemas/Reference.LocalSource" + }, + { + "$ref": "#/components/schemas/Reference.GitSource" + } + ] + }, + "Reference.Info": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + }, + "source": { + "$ref": "#/components/schemas/Reference.Source" + } + }, + "required": [ + "name", + "path", + "source" + ], + "additionalProperties": false + }, + "ProjectCopy.Copy": { + "type": "object", + "properties": { + "directory": { + "type": "string" + } + }, + "required": [ + "directory" + ], + "additionalProperties": false + }, + "ProjectCopyError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "ProjectCopyError" + ] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "forceRequired": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + }, + "required": [ + "name", + "data" + ], + "additionalProperties": false + }, + "Vcs.FileStatus": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "additions": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "deletions": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "status": { + "type": "string", + "enum": [ + "added", + "deleted", + "modified" + ] + } + }, + "required": [ + "file", + "additions", + "deletions", + "status" + ], + "additionalProperties": false + }, + "Vcs.Mode": { + "type": "string", + "enum": [ + "working", + "branch" + ] + }, + "WebSearch.Provider": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "additionalProperties": false + }, + "WebSearch.Result": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "title": { + "type": "string" + }, + "content": { + "type": "string" + }, + "time": { + "type": "object", + "properties": { + "published": { + "type": "number" + } + }, + "additionalProperties": false + } + }, + "required": [ + "url", + "time" + ], + "additionalProperties": false + }, + "WebSearch.Response": { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebSearch.Result" + } + } + }, + "required": [ + "providerID", + "results" + ], + "additionalProperties": false + } + }, + "securitySchemes": {} + }, + "security": [], + "tags": [ + { + "name": "health" + }, + { + "name": "server" + }, + { + "name": "location" + }, + { + "name": "agent" + }, + { + "name": "plugin", + "description": "Experimental plugin routes." + }, + { + "name": "session", + "description": "Experimental session routes." + }, + { + "name": "session", + "description": "Experimental message routes." + }, + { + "name": "model", + "description": "Experimental model routes." + }, + { + "name": "generate", + "description": "Experimental one-shot generation routes." + }, + { + "name": "provider", + "description": "Experimental provider routes." + }, + { + "name": "integration", + "description": "Integration discovery and authentication routes." + }, + { + "name": "mcp", + "description": "MCP server and resource routes." + }, + { + "name": "credential" + }, + { + "name": "project", + "description": "Location-scoped project routes." + }, + { + "name": "form", + "description": "Session form routes." + }, + { + "name": "permission", + "description": "Experimental permission routes." + }, + { + "name": "filesystem", + "description": "Experimental location-scoped filesystem routes." + }, + { + "name": "command", + "description": "Experimental command routes." + }, + { + "name": "skill", + "description": "Experimental skill routes." + }, + { + "name": "event", + "description": "Experimental event stream routes." + }, + { + "name": "pty", + "description": "Experimental location-scoped PTY routes." + }, + { + "name": "shell", + "description": "Experimental location-scoped shell command routes." + }, + { + "name": "question", + "description": "Experimental session question routes." + }, + { + "name": "reference", + "description": "Location-scoped project references." + }, + { + "name": "projectCopy", + "description": "Project copy management routes." + }, + { + "name": "vcs", + "description": "Location-scoped version control routes." + }, + { + "name": "debug" + }, + { + "name": "websearch", + "description": "Location-scoped web search routes." + } + ] +} diff --git a/packages/protocol/package.json b/packages/protocol/package.json index c79d9dbdf710..8d6ae38b0d2e 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -20,6 +20,8 @@ }, "scripts": { "build": "bun run script/build.ts", + "generate": "bun run script/generate-openapi.ts", + "check:generated": "bun run script/generate-openapi.ts --check", "typecheck": "tsgo --noEmit" }, "dependencies": { diff --git a/packages/protocol/script/generate-openapi.ts b/packages/protocol/script/generate-openapi.ts new file mode 100644 index 000000000000..9d799a261ddb --- /dev/null +++ b/packages/protocol/script/generate-openapi.ts @@ -0,0 +1,16 @@ +import { OpenApi } from "effect/unstable/httpapi" +import { fileURLToPath } from "url" +import { ClientApi } from "../src/client.js" + +const document = JSON.stringify(OpenApi.fromApi(ClientApi), null, 2) + "\n" +const target = fileURLToPath(new URL("../openapi.json", import.meta.url)) + +if (process.argv.includes("--check")) { + if ((await Bun.file(target).text()) !== document) { + console.error("Generated OpenAPI document is stale. Run `bun run generate` from packages/protocol.") + process.exit(1) + } + process.exit(0) +} + +await Bun.write(target, document) diff --git a/packages/sdk/.gitignore b/packages/sdk/.gitignore deleted file mode 100644 index d98d51a88028..000000000000 --- a/packages/sdk/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -.prism.log -node_modules -yarn-error.log -codegen.log -Brewfile.lock.json -dist -dist-deno -/*.tgz -.idea/ - diff --git a/packages/sdk/js/example/example.ts b/packages/sdk/js/example/example.ts deleted file mode 100644 index 42838a82a7e6..000000000000 --- a/packages/sdk/js/example/example.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { createOpencodeClient, createOpencodeServer } from "@opencode-ai/sdk" -import { pathToFileURL } from "bun" - -const server = await createOpencodeServer() -const client = createOpencodeClient({ baseUrl: server.url }) - -const input = await Array.fromAsync(new Bun.Glob("packages/core/*.ts").scan()) - -const tasks: Promise[] = [] -for await (const file of input) { - console.log("processing", file) - const session = await client.session.create() - tasks.push( - client.session.prompt({ - path: { id: session.data.id }, - body: { - parts: [ - { - type: "file", - mime: "text/plain", - url: pathToFileURL(file).href, - }, - { - type: "text", - text: `Write tests for every public function in this file.`, - }, - ], - }, - }), - ) - console.log("done", file) -} - -await Promise.all( - input.map(async (file) => { - const session = await client.session.create() - console.log("processing", file) - await client.session.prompt({ - path: { id: session.data.id }, - body: { - parts: [ - { - type: "file", - mime: "text/plain", - url: pathToFileURL(file).href, - }, - { - type: "text", - text: `Write tests for every public function in this file.`, - }, - ], - }, - }) - console.log("done", file) - }), -) diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json deleted file mode 100644 index d16ee0f054fb..000000000000 --- a/packages/sdk/js/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/package.json", - "name": "@opencode-ai/sdk", - "version": "1.18.4", - "type": "module", - "license": "MIT", - "scripts": { - "test": "bun test", - "typecheck": "tsgo --noEmit" - }, - "exports": { - ".": "./src/index.ts", - "./client": "./src/client.ts", - "./server": "./src/server.ts", - "./v2": "./src/v2/index.ts", - "./v2/client": "./src/v2/client.ts", - "./v2/gen/client": "./src/v2/gen/client/index.ts", - "./v2/server": "./src/v2/server.ts", - "./v2/types": "./src/v2/gen/types.gen.ts" - }, - "files": [ - "dist" - ], - "devDependencies": { - "@hey-api/openapi-ts": "0.90.10", - "@tsconfig/node22": "catalog:", - "@types/cross-spawn": "catalog:", - "@types/node": "catalog:", - "@typescript/native-preview": "catalog:", - "typescript": "catalog:" - }, - "dependencies": { - "cross-spawn": "catalog:" - } -} diff --git a/packages/sdk/js/script/build.ts b/packages/sdk/js/script/build.ts deleted file mode 100755 index c8fef5b1a724..000000000000 --- a/packages/sdk/js/script/build.ts +++ /dev/null @@ -1,424 +0,0 @@ -#!/usr/bin/env bun -import { fileURLToPath } from "url" - -const dir = fileURLToPath(new URL("..", import.meta.url)) -process.chdir(dir) - -import { $ } from "bun" -import path from "path" - -import { createClient } from "@hey-api/openapi-ts" - -const opencode = path.resolve(dir, "../../opencode") -const client = path.resolve(dir, "../../client") - -if (!(await Bun.file(path.join(opencode, "package.json")).exists())) { - await $`rm -rf dist` - await $`bun tsc` - process.exit(0) -} - -await $`bun dev generate > ${dir}/openapi.json`.cwd(opencode) -await $`bun -e ${` - import { OpenApi } from "effect/unstable/httpapi" - import { ClientApi } from "@opencode-ai/protocol/client" - - const output = process.argv.at(-1) - if (!output) throw new Error("Missing OpenAPI output path") - await Bun.write(output, JSON.stringify(OpenApi.fromApi(ClientApi))) -`} ${path.join(dir, "openapi-v2.json")}`.cwd(client) - -type OpenApiDocument = { - components?: { schemas?: Record } - paths?: Record - [key: string]: unknown -} - -const document = (await Bun.file("./openapi.json").json()) as OpenApiDocument -const v2Document = (await Bun.file("./openapi-v2.json").json()) as OpenApiDocument -normalizeComponentNames(v2Document) -deduplicateEquivalentComponent(v2Document, "Shell", "Shell1") -renameCollidingComponents(document, v2Document) -document.paths = { ...document.paths, ...v2Document.paths } -document.components = { - ...document.components, - schemas: { ...document.components?.schemas, ...v2Document.components?.schemas }, -} -inlineTypedAllOfConstraints(document) -const schemas = document.components?.schemas -if (schemas) { - const reachable = new Set() - const visit = (value: unknown) => { - if (Array.isArray(value)) { - value.forEach(visit) - return - } - if (typeof value !== "object" || value === null) return - for (const [key, child] of Object.entries(value)) { - if (key === "$ref" && typeof child === "string" && child.startsWith("#/components/schemas/")) { - const name = child.slice("#/components/schemas/".length) - if (reachable.has(name)) continue - reachable.add(name) - visit(schemas[name]) - } else { - visit(child) - } - } - } - visit({ ...document, components: { ...document.components, schemas: undefined } }) - for (const name of Object.keys(schemas)) { - if ( - /^(SessionAgentSelected|SessionModelSelected|SessionMoved|SessionRenamed|SessionForked|SessionInputPromoted|SessionInputAdmitted|SessionExecutionStarted|SessionExecutionSucceeded|SessionExecutionFailed|SessionExecutionInterrupted|SessionInstructionsUpdated|SessionSynthetic|SessionSkillActivated|SessionShellStarted|SessionShellEnded|SessionStepStarted|SessionStepEnded|SessionStepFailed|SessionTextStarted|SessionTextDelta|SessionTextEnded|SessionToolInputStarted|SessionToolInputDelta|SessionToolInputEnded|SessionToolCalled|SessionToolProgress|SessionToolSuccess|SessionToolFailed|SessionRetryScheduled|SessionCompactionStarted|SessionCompactionDelta|SessionCompactionEnded|SessionRevertStaged|SessionRevertCleared|SessionRevertCommitted)\d+$/.test( - name, - ) && - !reachable.has(name) - ) - delete schemas[name] - } - await Bun.write("./openapi.json", JSON.stringify(document)) -} - -await createClient({ - input: "./openapi.json", - output: { - path: "./src/v2/gen", - tsConfigPath: path.join(dir, "tsconfig.json"), - clean: true, - }, - plugins: [ - { - name: "@hey-api/typescript", - exportFromIndex: false, - }, - { - name: "@hey-api/sdk", - instance: "OpencodeClient", - exportFromIndex: false, - auth: false, - paramsStructure: "flat", - }, - { - name: "@hey-api/client-fetch", - exportFromIndex: false, - baseUrl: "http://localhost:4096", - }, - ], -}) - -const generatedTypesPath = "./src/v2/gen/types.gen.ts" -const generatedTypes = await Bun.file(generatedTypesPath).text() -if ( - /export type (SessionAgentSelected|SessionModelSelected|SessionMoved|SessionRenamed|SessionForked|SessionInputPromoted|SessionInputAdmitted|SessionExecutionStarted|SessionExecutionSucceeded|SessionExecutionFailed|SessionExecutionInterrupted|SessionInstructionsUpdated|SessionSynthetic|SessionSkillActivated|SessionShellStarted|SessionShellEnded|SessionStepStarted|SessionStepEnded|SessionStepFailed|SessionTextStarted|SessionTextDelta|SessionTextEnded|SessionToolInputStarted|SessionToolInputDelta|SessionToolInputEnded|SessionToolCalled|SessionToolProgress|SessionToolSuccess|SessionToolFailed|SessionRetryScheduled|SessionCompactionStarted|SessionCompactionDelta|SessionCompactionEnded|SessionRevertStaged|SessionRevertCleared|SessionRevertCommitted)\d+ =/.test( - generatedTypes, - ) -) { - throw new Error("Session history generated duplicate Session event variants") -} -const sessionErrorTypesPatched = deduplicateEquivalentGeneratedTypes( - generatedTypes, - "SessionStructuredError", - /^SessionStructuredError\d+$/, -) -const obsoleteSessionNext = [...sessionErrorTypesPatched.matchAll(/export type (SessionNext\w*) =/g)].map( - (match) => match[1], -) -if (obsoleteSessionNext.length > 0) { - throw new Error(`Obsolete SessionNext generated type noise reintroduced: ${obsoleteSessionNext.join(", ")}`) -} -const logTypesPatched = sessionErrorTypesPatched.replace( - /(export type V2SessionLogData = \{[\s\S]*?query\?: \{\s*after\?: )string/, - "$1number", -) -if (logTypesPatched === sessionErrorTypesPatched) { - throw new Error("Session log numeric query patch did not apply") -} -const sessionListTypesPatched = logTypesPatched.replace( - /(export type V2SessionListData = \{[\s\S]*?query\?: \{[\s\S]*?limit\?: )string( \| null)/, - "$1number$2", -) -if (sessionListTypesPatched === logTypesPatched) { - throw new Error("Session list numeric query patch did not apply") -} -const sessionMessagesTypesPatched = sessionListTypesPatched.replace( - /(export type V2MessageListData = \{[\s\S]*?query\?: \{[\s\S]*?limit\?: )string( \| null)/, - "$1number$2", -) -if (sessionMessagesTypesPatched === sessionListTypesPatched) { - throw new Error("Session messages numeric query patch did not apply") -} -const eventSubscribeTypesPatched = sessionMessagesTypesPatched.replace( - /(export type V2EventSubscribeResponses = \{\s*\/\*\*[\s\S]*?\*\/\s*200: )\{\s*id: string \| null;?\s*event: string;?\s*data: (?:V2EventStream(?:V2)?|V2EventJsonString);?\s*\};?/, - "$1V2Event", -) -if (eventSubscribeTypesPatched === sessionMessagesTypesPatched) { - throw new Error("Event subscribe response patch did not apply") -} -if (/SessionStructuredError\d/.test(eventSubscribeTypesPatched)) { - throw new Error("Session structured error generated a name-mangled duplicate") -} -if (/\bSessionNext\w*\b/.test(eventSubscribeTypesPatched)) { - throw new Error("Obsolete SessionNext generated type noise reintroduced") -} -if (/export type Shell\d+V2 =/.test(eventSubscribeTypesPatched)) { - throw new Error("Shell generated a name-mangled duplicate") -} -await Bun.write(generatedTypesPath, eventSubscribeTypesPatched) - -const querySerializerPath = "./src/v2/gen/client/utils.gen.ts" -const querySerializerSource = await Bun.file(querySerializerPath).text() -const querySerializerPatched = querySerializerSource.replace( - /if \(value === undefined \|\| value === null\) \{\s*continue;?\s*\}/, - "if (value === undefined) {\n continue;\n }\n\n if (value === null) {\n search.push(`${name}=null`);\n continue;\n }", -) -if (querySerializerPatched === querySerializerSource) { - throw new Error( - `Query serializer null patch did not apply; @hey-api/openapi-ts output may have changed (${querySerializerPath})`, - ) -} -await Bun.write(querySerializerPath, querySerializerPatched) - -const generatedSdkPath = "./src/v2/gen/sdk.gen.ts" -const generatedSdk = await Bun.file(generatedSdkPath).text() -const logSdkPatched = generatedSdk.replace( - /(Read the session log[\s\S]*?parameters: \{[\s\S]*?after\?: )string(\s*\|\s*null)?/, - "$1number$2", -) -if (logSdkPatched === generatedSdk) { - throw new Error("Session log numeric SDK patch did not apply") -} -const sessionListSdkPatched = logSdkPatched.replace( - /(List sessions[\s\S]*?parameters\?: \{[\s\S]*?limit\?: )string( \| null)/, - "$1number$2", -) -if (sessionListSdkPatched === logSdkPatched) { - throw new Error("Session list numeric SDK patch did not apply") -} -const sessionMessagesSdkPatched = sessionListSdkPatched.replace( - /(Get session messages[\s\S]*?parameters: \{[\s\S]*?limit\?: )string( \| null)/, - "$1number$2", -) -if (sessionMessagesSdkPatched === sessionListSdkPatched) { - throw new Error("Session messages numeric SDK patch did not apply") -} -await Bun.write(generatedSdkPath, sessionMessagesSdkPatched) - -// Patch a @hey-api/openapi-ts codegen bug: SseFn incorrectly passes the -// endpoint's TError into the second generic of ServerSentEventsResult, which -// is the AsyncGenerator's TReturn slot. Iterator return values have nothing -// to do with HTTP errors, and any consumer that calls `.return()` or returns -// from a mock generator gets type-checked against the wrong shape. Drop the -// arg so TReturn defaults to void. -const sseTypesPath = "./src/v2/gen/client/types.gen.ts" -const sseTypesFile = Bun.file(sseTypesPath) -const sseTypesSource = await sseTypesFile.text() -const sseTypesPatched = sseTypesSource.replace( - "=> Promise>", - "=> Promise>", -) -if (sseTypesPatched === sseTypesSource) { - throw new Error(`SseFn patch did not apply; @hey-api/openapi-ts output may have changed (${sseTypesPath})`) -} -await Bun.write(sseTypesPath, sseTypesPatched) - -await $`bun prettier --write src/gen` -await $`bun prettier --write src/v2` -await $`rm -rf dist` -await $`bun tsc` -await $`rm openapi.json openapi-v2.json` - -function renameCollidingComponents(target: OpenApiDocument, source: OpenApiDocument) { - const targetSchemas = target.components?.schemas - const sourceSchemas = source.components?.schemas - if (!targetSchemas || !sourceSchemas) return - - const renames = new Map() - for (const name of Object.keys(sourceSchemas)) { - if (!Object.hasOwn(targetSchemas, name)) continue - if (JSON.stringify(normalizeSchema(sourceSchemas[name])) === JSON.stringify(normalizeSchema(targetSchemas[name]))) { - delete sourceSchemas[name] - continue - } - let renamed = `${name}V2` - let index = 2 - while (Object.hasOwn(targetSchemas, renamed) || Object.hasOwn(sourceSchemas, renamed)) { - renamed = `${name}V2${index}` - index++ - } - renames.set(name, renamed) - } - if (renames.size === 0) return - - source.components = { - ...source.components, - schemas: Object.fromEntries( - Object.entries(sourceSchemas).map(([name, schema]) => [renames.get(name) ?? name, rewriteRefs(schema, renames)]), - ), - } - source.paths = rewriteRefs(source.paths, renames) as Record | undefined -} - -function normalizeComponentNames(document: OpenApiDocument) { - const schemas = document.components?.schemas - if (!schemas) return - - const canonical = new Map(Object.entries(schemas)) - const renames = new Map() - for (const name of Object.keys(schemas)) { - const next = componentTypeName(name) - if (next === name) continue - const existing = canonical.get(next) - if (existing !== undefined) { - if (JSON.stringify(normalizeSchema(schemas[name])) !== JSON.stringify(normalizeSchema(existing))) continue - renames.set(name, next) - continue - } - renames.set(name, next) - canonical.set(next, schemas[name]) - } - if (renames.size === 0) return - - const renamed = new Set() - document.components = { - ...document.components, - schemas: Object.fromEntries( - [ - ...Object.entries(schemas).filter(([name]) => !renames.has(name)), - ...Object.entries(schemas).flatMap(([name, schema]) => { - const next = renames.get(name) - if (!next || Object.hasOwn(schemas, next) || renamed.has(next)) return [] - renamed.add(next) - return [[next, schema] as const] - }), - ].map(([name, schema]) => [name, rewriteRefs(schema, renames)]), - ), - } - document.paths = rewriteRefs(document.paths, renames) as Record | undefined -} - -function componentTypeName(name: string) { - if (!name.includes(".")) return name - return name - .split(".") - .filter((part) => !/^\d+$/.test(part)) - .map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)) - .join("") -} - -function deduplicateEquivalentComponent(document: OpenApiDocument, canonical: string, duplicate: string) { - const schemas = document.components?.schemas - if (!schemas?.[canonical] || !schemas[duplicate]) return - if (JSON.stringify(normalizeSchema(schemas[canonical])) !== JSON.stringify(normalizeSchema(schemas[duplicate]))) { - throw new Error(`${duplicate} no longer has the same wire shape as ${canonical}`) - } - - const renames = new Map([[duplicate, canonical]]) - const rewritten = rewriteRefs(schemas, renames) as Record - delete rewritten[duplicate] - document.components = { ...document.components, schemas: rewritten } - document.paths = rewriteRefs(document.paths, renames) as Record | undefined -} - -function deduplicateEquivalentGeneratedTypes(source: string, canonical: string, duplicates: RegExp) { - const canonicalType = generatedType(source, canonical) - if (!canonicalType) throw new Error(`Generated canonical type missing: ${canonical}`) - const names = [...source.matchAll(/export type (\w+) =/g)] - .map((match) => match[1]) - .filter((name): name is string => name !== undefined && duplicates.test(name)) - - return names.reduce((patched, name) => { - const duplicate = generatedType(patched, name) - const currentCanonical = generatedType(patched, canonical) - if (!duplicate || !currentCanonical) throw new Error(`Generated type declaration missing while comparing ${name}`) - if (normalizeGeneratedType(currentCanonical.shape) !== normalizeGeneratedType(duplicate.shape)) { - throw new Error(`${name} no longer has the same generated type shape as ${canonical}`) - } - return (patched.slice(0, duplicate.start) + patched.slice(duplicate.end)).replaceAll(name, canonical) - }, source) -} - -function generatedType(source: string, name: string) { - const start = source.indexOf(`export type ${name} =`) - if (start === -1) return undefined - const next = source.indexOf("\n\nexport type ", start + 1) - const shapeEnd = next === -1 ? source.length : next - return { - start, - end: next === -1 ? source.length : next + 2, - shape: source.slice(source.indexOf("=", start) + 1, shapeEnd), - } -} - -function normalizeGeneratedType(shape: string) { - return shape.replaceAll(/\s/g, "") -} - -function normalizeSchema(value: unknown, key?: string): unknown { - if (Array.isArray(value)) { - const flattened = - key === "anyOf" - ? value.flatMap((item) => - typeof item === "object" && item !== null && Object.keys(item).length === 1 && "anyOf" in item - ? Array.isArray(item.anyOf) - ? item.anyOf - : [item] - : [item], - ) - : value - const expanded = - key === "anyOf" - ? flattened.flatMap((item) => { - if (typeof item !== "object" || item === null || !("type" in item) || !("enum" in item)) return [item] - if (Object.keys(item).some((property) => property !== "type" && property !== "enum")) return [item] - if (!Array.isArray(item.enum)) return [item] - return item.enum.map((member) => ({ type: item.type, enum: [member] })) - }) - : flattened - const normalized = expanded.map((item) => normalizeSchema(item)) - if (key !== "anyOf" && key !== "required" && key !== "enum") return normalized - return [...new Map(normalized.map((item) => [JSON.stringify(item), item])).values()].sort((a, b) => - JSON.stringify(a).localeCompare(JSON.stringify(b)), - ) - } - if (typeof value !== "object" || value === null) return value - return Object.fromEntries( - Object.entries(value) - .sort(([left], [right]) => left.localeCompare(right)) - .map(([property, child]) => [property, normalizeSchema(child, property)]), - ) -} - -function rewriteRefs(value: unknown, renames: Map): unknown { - if (Array.isArray(value)) return value.map((item) => rewriteRefs(item, renames)) - if (typeof value !== "object" || value === null) return value - - return Object.fromEntries( - Object.entries(value).map(([key, child]) => { - if (key !== "$ref" || typeof child !== "string") return [key, rewriteRefs(child, renames)] - const prefix = "#/components/schemas/" - if (!child.startsWith(prefix)) return [key, child] - return [key, `${prefix}${renames.get(child.slice(prefix.length)) ?? child.slice(prefix.length)}`] - }), - ) -} - -function inlineTypedAllOfConstraints(value: unknown): void { - if (Array.isArray(value)) { - value.forEach(inlineTypedAllOfConstraints) - return - } - if (typeof value !== "object" || value === null) return - - const schema = value as { allOf?: unknown; type?: unknown; [key: string]: unknown } - if (typeof schema.type === "string" && Array.isArray(schema.allOf) && schema.allOf.every(isConstraintSchema)) { - for (const item of schema.allOf) Object.assign(schema, item) - delete schema.allOf - } - Object.values(schema).forEach(inlineTypedAllOfConstraints) -} - -function isConstraintSchema(value: unknown): value is Record { - if (typeof value !== "object" || value === null || Array.isArray(value)) return false - return !Object.keys(value).some( - (key) => key === "$ref" || key === "type" || key === "allOf" || key === "anyOf" || key === "oneOf", - ) -} diff --git a/packages/sdk/js/script/publish.ts b/packages/sdk/js/script/publish.ts deleted file mode 100755 index 29426a41b7dd..000000000000 --- a/packages/sdk/js/script/publish.ts +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bun - -import { Script } from "@opencode-ai/script" -import { $ } from "bun" -import { fileURLToPath } from "url" - -const dir = fileURLToPath(new URL("..", import.meta.url)) -process.chdir(dir) - -async function published(name: string, version: string) { - return (await $`npm view ${name}@${version} version`.nothrow()).exitCode === 0 -} - -const originalText = await Bun.file("package.json").text() -const pkg = JSON.parse(originalText) as { - name: string - version: string - exports: Record -} -function transformExports(exports: Record) { - return Object.fromEntries( - Object.entries(exports).map(([key, value]) => { - if (typeof value === "string") { - const file = value.replace("./src/", "./dist/").replace(".ts", "") - return [key, { import: file + ".js", types: file + ".d.ts" }] - } - if (typeof value === "object" && value !== null && !Array.isArray(value)) { - return [key, transformExports(value)] - } - return [key, value] - }), - ) -} -if (await published(pkg.name, pkg.version)) { - console.log(`already published ${pkg.name}@${pkg.version}`) -} else { - pkg.exports = transformExports(pkg.exports) - await Bun.write("package.json", JSON.stringify(pkg, null, 2)) - try { - await $`bun pm pack` - await $`npm publish *.tgz --tag ${Script.channel} --access public` - } finally { - await Bun.write("package.json", originalText) - } -} diff --git a/packages/sdk/js/src/client.ts b/packages/sdk/js/src/client.ts deleted file mode 100644 index 5cf071e7b7ab..000000000000 --- a/packages/sdk/js/src/client.ts +++ /dev/null @@ -1,57 +0,0 @@ -export * from "./gen/types.gen.js" - -import { createClient } from "./gen/client/client.gen.js" -import { type Config } from "./gen/client/types.gen.js" -import { OpencodeClient } from "./gen/sdk.gen.js" -import { wrapClientError } from "./error-interceptor.js" -export { type Config as OpencodeClientConfig, OpencodeClient } - -function pick(value: string | null, fallback?: string) { - if (!value) return - if (!fallback) return value - if (value === fallback) return fallback - if (value === encodeURIComponent(fallback)) return fallback - return value -} - -function rewrite(request: Request, directory?: string) { - if (request.method !== "GET" && request.method !== "HEAD") return request - - const value = pick(request.headers.get("x-opencode-directory"), directory) - if (!value) return request - - const url = new URL(request.url) - if (!url.searchParams.has("directory")) { - url.searchParams.set("directory", value) - } - - const next = new Request(url, request) - next.headers.delete("x-opencode-directory") - return next -} - -export function createOpencodeClient(config?: Config & { directory?: string }) { - if (!config?.fetch) { - const customFetch: any = (req: any) => { - // @ts-ignore - req.timeout = false - return fetch(req) - } - config = { - ...config, - fetch: customFetch, - } - } - - if (config?.directory) { - config.headers = { - ...config.headers, - "x-opencode-directory": encodeURIComponent(config.directory), - } - } - - const client = createClient(config) - client.interceptors.request.use((request) => rewrite(request, config?.directory)) - client.interceptors.error.use(wrapClientError) - return new OpencodeClient({ client }) -} diff --git a/packages/sdk/js/src/error-interceptor.ts b/packages/sdk/js/src/error-interceptor.ts deleted file mode 100644 index 26407ecfc903..000000000000 --- a/packages/sdk/js/src/error-interceptor.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Wrap whatever the generated client decoded from a non-2xx error body - * into a real `Error` so downstream formatters (TUI, plugins) get a - * useful `.message` instead of `[object Object]` or blank. The original - * parsed body and status live under `.cause` for callers that need - * structured fields. - * - * Only fires when the caller used `{ throwOnError: true }`. Callers that - * read `result.error` directly (the result-tuple path) get the parsed - * body unchanged so existing field-level reads (`.error.name`, - * `JSON.stringify(error)`, etc.) are byte-for-byte identical to before. - */ -export function wrapClientError( - error: unknown, - response: Response | undefined, - request: Request | undefined, - opts: { throwOnError?: boolean } | undefined, -): unknown { - if (!opts?.throwOnError) return error - if (error instanceof Error) return error - - // NamedError-shaped responses (the common case for opencode 4xx) come - // through as POJOs — extract a useful message first, then wrap. - if (typeof error === "object" && error !== null && Object.keys(error).length > 0) { - const obj = error as { data?: { message?: unknown }; message?: unknown; name?: unknown } - const message = - (typeof obj.data?.message === "string" && obj.data.message) || - (typeof obj.message === "string" && obj.message) || - (typeof obj.name === "string" && obj.name) || - describe(request, response) - return new Error(message, { cause: { body: error, status: response?.status } }) - } - - if (typeof error === "string" && error.length > 0) { - return new Error(error, { cause: { body: error, status: response?.status } }) - } - - // Empty body / network failure / undefined / null / empty object. - const reason = response ? "(empty response body)" : "network error (no response)" - return new Error(`opencode server ${describe(request, response)}: ${reason}`, { - cause: { body: error, status: response?.status }, - }) -} - -function describe(request: Request | undefined, response: Response | undefined) { - const method = request?.method ?? "?" - const url = request?.url ?? "?" - const status = response?.status - const statusText = response?.statusText - return `${method} ${url}${status ? " → " + status : ""}${statusText ? " " + statusText : ""}` -} diff --git a/packages/sdk/js/src/gen/client.gen.ts b/packages/sdk/js/src/gen/client.gen.ts deleted file mode 100644 index e7cdb292c680..000000000000 --- a/packages/sdk/js/src/gen/client.gen.ts +++ /dev/null @@ -1,22 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { ClientOptions } from "./types.gen.js" -import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from "./client/index.js" - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = ( - override?: Config, -) => Config & T> - -export const client = createClient( - createConfig({ - baseUrl: "http://localhost:4096", - }), -) diff --git a/packages/sdk/js/src/gen/client/client.gen.ts b/packages/sdk/js/src/gen/client/client.gen.ts deleted file mode 100644 index 34a8d0beceb9..000000000000 --- a/packages/sdk/js/src/gen/client/client.gen.ts +++ /dev/null @@ -1,212 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { createSseClient } from "../core/serverSentEvents.gen.js" -import type { Client, Config, RequestOptions, ResolvedRequestOptions } from "./types.gen.js" -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from "./utils.gen.js" - -type ReqInit = Omit & { - body?: any - headers: ReturnType -} - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config) - - const getConfig = (): Config => ({ ..._config }) - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config) - return getConfig() - } - - const interceptors = createInterceptors() - - const beforeRequest = async (options: RequestOptions) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - serializedBody: undefined, - } - - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }) - } - - if (opts.requestValidator) { - await opts.requestValidator(opts) - } - - if (opts.body && opts.bodySerializer) { - opts.serializedBody = opts.bodySerializer(opts.body) - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.serializedBody === undefined || opts.serializedBody === "") { - opts.headers.delete("Content-Type") - } - - const url = buildUrl(opts) - - return { opts, url } - } - - const request: Client["request"] = async (options) => { - // @ts-expect-error - const { opts, url } = await beforeRequest(options) - const requestInit: ReqInit = { - redirect: "follow", - ...opts, - body: opts.serializedBody, - } - - let request = new Request(url, requestInit) - - for (const fn of interceptors.request._fns) { - if (fn) { - request = await fn(request, opts) - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch! - let response = await _fetch(request) - - for (const fn of interceptors.response._fns) { - if (fn) { - response = await fn(response, request, opts) - } - } - - const result = { - request, - response, - } - - if (response.ok) { - if (response.status === 204 || response.headers.get("Content-Length") === "0") { - return opts.responseStyle === "data" - ? {} - : { - data: {}, - ...result, - } - } - - const parseAs = - (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json" - - let data: any - switch (parseAs) { - case "arrayBuffer": - case "blob": - case "formData": - case "json": - case "text": - data = await response[parseAs]() - break - case "stream": - return opts.responseStyle === "data" - ? response.body - : { - data: response.body, - ...result, - } - } - - if (parseAs === "json") { - if (opts.responseValidator) { - await opts.responseValidator(data) - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data) - } - } - - return opts.responseStyle === "data" - ? data - : { - data, - ...result, - } - } - - const textError = await response.text() - let jsonError: unknown - - try { - jsonError = JSON.parse(textError) - } catch { - // noop - } - - const error = jsonError ?? textError - let finalError = error - - for (const fn of interceptors.error._fns) { - if (fn) { - finalError = (await fn(error, response, request, opts)) as string - } - } - - finalError = finalError || ({} as string) - - if (opts.throwOnError) { - throw finalError - } - - // TODO: we probably want to return error and improve types - return opts.responseStyle === "data" - ? undefined - : { - error: finalError, - ...result, - } - } - - const makeMethod = (method: Required["method"]) => { - const fn = (options: RequestOptions) => request({ ...options, method }) - fn.sse = async (options: RequestOptions) => { - const { opts, url } = await beforeRequest(options) - return createSseClient({ - ...opts, - body: opts.body as BodyInit | null | undefined, - headers: opts.headers as unknown as Record, - method, - url, - }) - } - return fn - } - - return { - buildUrl, - connect: makeMethod("CONNECT"), - delete: makeMethod("DELETE"), - get: makeMethod("GET"), - getConfig, - head: makeMethod("HEAD"), - interceptors, - options: makeMethod("OPTIONS"), - patch: makeMethod("PATCH"), - post: makeMethod("POST"), - put: makeMethod("PUT"), - request, - setConfig, - trace: makeMethod("TRACE"), - } as Client -} diff --git a/packages/sdk/js/src/gen/client/index.ts b/packages/sdk/js/src/gen/client/index.ts deleted file mode 100644 index 06f21e3d802b..000000000000 --- a/packages/sdk/js/src/gen/client/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type { Auth } from "../core/auth.gen.js" -export type { QuerySerializerOptions } from "../core/bodySerializer.gen.js" -export { - formDataBodySerializer, - jsonBodySerializer, - urlSearchParamsBodySerializer, -} from "../core/bodySerializer.gen.js" -export { buildClientParams } from "../core/params.gen.js" -export { createClient } from "./client.gen.js" -export type { - Client, - ClientOptions, - Config, - CreateClientConfig, - Options, - OptionsLegacyParser, - RequestOptions, - RequestResult, - ResolvedRequestOptions, - ResponseStyle, - TDataShape, -} from "./types.gen.js" -export { createConfig, mergeHeaders } from "./utils.gen.js" diff --git a/packages/sdk/js/src/gen/client/types.gen.ts b/packages/sdk/js/src/gen/client/types.gen.ts deleted file mode 100644 index db8e544cfde3..000000000000 --- a/packages/sdk/js/src/gen/client/types.gen.ts +++ /dev/null @@ -1,222 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth } from "../core/auth.gen.js" -import type { ServerSentEventsOptions, ServerSentEventsResult } from "../core/serverSentEvents.gen.js" -import type { Client as CoreClient, Config as CoreConfig } from "../core/types.gen.js" -import type { Middleware } from "./utils.gen.js" - -export type ResponseStyle = "data" | "fields" - -export interface Config - extends Omit, - CoreConfig { - /** - * Base URL for all requests made by this client. - */ - baseUrl?: T["baseUrl"] - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: (request: Request) => ReturnType - /** - * Please don't use the Fetch client for Next.js applications. The `next` - * options won't have any effect. - * - * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. - */ - next?: never - /** - * Return the response data parsed in a specified format. By default, `auto` - * will infer the appropriate method from the `Content-Type` response header. - * You can override this behavior with any of the {@link Body} methods. - * Select `stream` if you don't want to parse response data at all. - * - * @default 'auto' - */ - parseAs?: "arrayBuffer" | "auto" | "blob" | "formData" | "json" | "stream" | "text" - /** - * Should we return only data or multiple fields (data, error, response, etc.)? - * - * @default 'fields' - */ - responseStyle?: ResponseStyle - /** - * Throw an error instead of returning it in the response? - * - * @default false - */ - throwOnError?: T["throwOnError"] -} - -export interface RequestOptions< - TData = unknown, - TResponseStyle extends ResponseStyle = "fields", - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends Config<{ - responseStyle: TResponseStyle - throwOnError: ThrowOnError - }>, - Pick< - ServerSentEventsOptions, - "onSseError" | "onSseEvent" | "sseDefaultRetryDelay" | "sseMaxRetryAttempts" | "sseMaxRetryDelay" - > { - /** - * Any body that you want to add to your request. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} - */ - body?: unknown - path?: Record - query?: Record - /** - * Security mechanism(s) to use for the request. - */ - security?: ReadonlyArray - url: Url -} - -export interface ResolvedRequestOptions< - TResponseStyle extends ResponseStyle = "fields", - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends RequestOptions { - serializedBody?: string -} - -export type RequestResult< - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = "fields", -> = ThrowOnError extends true - ? Promise< - TResponseStyle extends "data" - ? TData extends Record - ? TData[keyof TData] - : TData - : { - data: TData extends Record ? TData[keyof TData] : TData - request: Request - response: Response - } - > - : Promise< - TResponseStyle extends "data" - ? (TData extends Record ? TData[keyof TData] : TData) | undefined - : ( - | { - data: TData extends Record ? TData[keyof TData] : TData - error: undefined - } - | { - data: undefined - error: TError extends Record ? TError[keyof TError] : TError - } - ) & { - request: Request - response: Response - } - > - -export interface ClientOptions { - baseUrl?: string - responseStyle?: ResponseStyle - throwOnError?: boolean -} - -type MethodFnBase = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = "fields", ->( - options: Omit, "method">, -) => RequestResult - -type MethodFnServerSentEvents = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = "fields", ->( - options: Omit, "method">, -) => Promise> - -type MethodFn = MethodFnBase & { - sse: MethodFnServerSentEvents -} - -type RequestFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = "fields", ->( - options: Omit, "method"> & - Pick>, "method">, -) => RequestResult - -type BuildUrlFn = < - TData extends { - body?: unknown - path?: Record - query?: Record - url: string - }, ->( - options: Pick & Options, -) => string - -export type Client = CoreClient & { - interceptors: Middleware -} - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = ( - override?: Config, -) => Config & T> - -export interface TDataShape { - body?: unknown - headers?: unknown - path?: unknown - query?: unknown - url: string -} - -type OmitKeys = Pick> - -export type Options< - TData extends TDataShape = TDataShape, - ThrowOnError extends boolean = boolean, - TResponse = unknown, - TResponseStyle extends ResponseStyle = "fields", -> = OmitKeys, "body" | "path" | "query" | "url"> & - Omit - -export type OptionsLegacyParser< - TData = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = "fields", -> = TData extends { body?: any } - ? TData extends { headers?: any } - ? OmitKeys, "body" | "headers" | "url"> & TData - : OmitKeys, "body" | "url"> & - TData & - Pick, "headers"> - : TData extends { headers?: any } - ? OmitKeys, "headers" | "url"> & - TData & - Pick, "body"> - : OmitKeys, "url"> & TData diff --git a/packages/sdk/js/src/gen/client/utils.gen.ts b/packages/sdk/js/src/gen/client/utils.gen.ts deleted file mode 100644 index 209bfbe8e620..000000000000 --- a/packages/sdk/js/src/gen/client/utils.gen.ts +++ /dev/null @@ -1,287 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { getAuthToken } from "../core/auth.gen.js" -import type { QuerySerializerOptions } from "../core/bodySerializer.gen.js" -import { jsonBodySerializer } from "../core/bodySerializer.gen.js" -import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.gen.js" -import { getUrl } from "../core/utils.gen.js" -import type { Client, ClientOptions, Config, RequestOptions } from "./types.gen.js" - -export const createQuerySerializer = ({ allowReserved, array, object }: QuerySerializerOptions = {}) => { - const querySerializer = (queryParams: T) => { - const search: string[] = [] - if (queryParams && typeof queryParams === "object") { - for (const name in queryParams) { - const value = queryParams[name] - - if (value === undefined || value === null) { - continue - } - - if (Array.isArray(value)) { - const serializedArray = serializeArrayParam({ - allowReserved, - explode: true, - name, - style: "form", - value, - ...array, - }) - if (serializedArray) search.push(serializedArray) - } else if (typeof value === "object") { - const serializedObject = serializeObjectParam({ - allowReserved, - explode: true, - name, - style: "deepObject", - value: value as Record, - ...object, - }) - if (serializedObject) search.push(serializedObject) - } else { - const serializedPrimitive = serializePrimitiveParam({ - allowReserved, - name, - value: value as string, - }) - if (serializedPrimitive) search.push(serializedPrimitive) - } - } - } - return search.join("&") - } - return querySerializer -} - -/** - * Infers parseAs value from provided Content-Type header. - */ -export const getParseAs = (contentType: string | null): Exclude => { - if (!contentType) { - // If no Content-Type header is provided, the best we can do is return the raw response body, - // which is effectively the same as the 'stream' option. - return "stream" - } - - const cleanContent = contentType.split(";")[0]?.trim() - - if (!cleanContent) { - return - } - - if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) { - return "json" - } - - if (cleanContent === "multipart/form-data") { - return "formData" - } - - if (["application/", "audio/", "image/", "video/"].some((type) => cleanContent.startsWith(type))) { - return "blob" - } - - if (cleanContent.startsWith("text/")) { - return "text" - } - - return -} - -const checkForExistence = ( - options: Pick & { - headers: Headers - }, - name?: string, -): boolean => { - if (!name) { - return false - } - if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) { - return true - } - return false -} - -export const setAuthParams = async ({ - security, - ...options -}: Pick, "security"> & - Pick & { - headers: Headers - }) => { - for (const auth of security) { - if (checkForExistence(options, auth.name)) { - continue - } - - const token = await getAuthToken(auth, options.auth) - - if (!token) { - continue - } - - const name = auth.name ?? "Authorization" - - switch (auth.in) { - case "query": - if (!options.query) { - options.query = {} - } - options.query[name] = token - break - case "cookie": - options.headers.append("Cookie", `${name}=${token}`) - break - case "header": - default: - options.headers.set(name, token) - break - } - } -} - -export const buildUrl: Client["buildUrl"] = (options) => - getUrl({ - baseUrl: options.baseUrl as string, - path: options.path, - query: options.query, - querySerializer: - typeof options.querySerializer === "function" - ? options.querySerializer - : createQuerySerializer(options.querySerializer), - url: options.url, - }) - -export const mergeConfigs = (a: Config, b: Config): Config => { - const config = { ...a, ...b } - if (config.baseUrl?.endsWith("/")) { - config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1) - } - config.headers = mergeHeaders(a.headers, b.headers) - return config -} - -export const mergeHeaders = (...headers: Array["headers"] | undefined>): Headers => { - const mergedHeaders = new Headers() - for (const header of headers) { - if (!header || typeof header !== "object") { - continue - } - - const iterator = header instanceof Headers ? header.entries() : Object.entries(header) - - for (const [key, value] of iterator) { - if (value === null) { - mergedHeaders.delete(key) - } else if (Array.isArray(value)) { - for (const v of value) { - mergedHeaders.append(key, v as string) - } - } else if (value !== undefined) { - // assume object headers are meant to be JSON stringified, i.e. their - // content value in OpenAPI specification is 'application/json' - mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : (value as string)) - } - } - } - return mergedHeaders -} - -type ErrInterceptor = ( - error: Err, - response: Res, - request: Req, - options: Options, -) => Err | Promise - -type ReqInterceptor = (request: Req, options: Options) => Req | Promise - -type ResInterceptor = (response: Res, request: Req, options: Options) => Res | Promise - -class Interceptors { - _fns: (Interceptor | null)[] - - constructor() { - this._fns = [] - } - - clear() { - this._fns = [] - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === "number") { - return this._fns[id] ? id : -1 - } else { - return this._fns.indexOf(id) - } - } - exists(id: number | Interceptor) { - const index = this.getInterceptorIndex(id) - return !!this._fns[index] - } - - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id) - if (this._fns[index]) { - this._fns[index] = null - } - } - - update(id: number | Interceptor, fn: Interceptor) { - const index = this.getInterceptorIndex(id) - if (this._fns[index]) { - this._fns[index] = fn - return id - } else { - return false - } - } - - use(fn: Interceptor) { - this._fns = [...this._fns, fn] - return this._fns.length - 1 - } -} - -// `createInterceptors()` response, meant for external use as it does not -// expose internals -export interface Middleware { - error: Pick>, "eject" | "use"> - request: Pick>, "eject" | "use"> - response: Pick>, "eject" | "use"> -} - -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ - error: new Interceptors>(), - request: new Interceptors>(), - response: new Interceptors>(), -}) - -const defaultQuerySerializer = createQuerySerializer({ - allowReserved: false, - array: { - explode: true, - style: "form", - }, - object: { - explode: true, - style: "deepObject", - }, -}) - -const defaultHeaders = { - "Content-Type": "application/json", -} - -export const createConfig = ( - override: Config & T> = {}, -): Config & T> => ({ - ...jsonBodySerializer, - headers: defaultHeaders, - parseAs: "auto", - querySerializer: defaultQuerySerializer, - ...override, -}) diff --git a/packages/sdk/js/src/gen/core/auth.gen.ts b/packages/sdk/js/src/gen/core/auth.gen.ts deleted file mode 100644 index bc7b230f4475..000000000000 --- a/packages/sdk/js/src/gen/core/auth.gen.ts +++ /dev/null @@ -1,41 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type AuthToken = string | undefined - -export interface Auth { - /** - * Which part of the request do we use to send the auth? - * - * @default 'header' - */ - in?: "header" | "query" | "cookie" - /** - * Header or query parameter name. - * - * @default 'Authorization' - */ - name?: string - scheme?: "basic" | "bearer" - type: "apiKey" | "http" -} - -export const getAuthToken = async ( - auth: Auth, - callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, -): Promise => { - const token = typeof callback === "function" ? await callback(auth) : callback - - if (!token) { - return - } - - if (auth.scheme === "bearer") { - return `Bearer ${token}` - } - - if (auth.scheme === "basic") { - return `Basic ${btoa(token)}` - } - - return token -} diff --git a/packages/sdk/js/src/gen/core/bodySerializer.gen.ts b/packages/sdk/js/src/gen/core/bodySerializer.gen.ts deleted file mode 100644 index 0660616052bb..000000000000 --- a/packages/sdk/js/src/gen/core/bodySerializer.gen.ts +++ /dev/null @@ -1,74 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { ArrayStyle, ObjectStyle, SerializerOptions } from "./pathSerializer.gen.js" - -export type QuerySerializer = (query: Record) => string - -export type BodySerializer = (body: any) => any - -export interface QuerySerializerOptions { - allowReserved?: boolean - array?: SerializerOptions - object?: SerializerOptions -} - -const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { - if (typeof value === "string" || value instanceof Blob) { - data.append(key, value) - } else if (value instanceof Date) { - data.append(key, value.toISOString()) - } else { - data.append(key, JSON.stringify(value)) - } -} - -const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { - if (typeof value === "string") { - data.append(key, value) - } else { - data.append(key, JSON.stringify(value)) - } -} - -export const formDataBodySerializer = { - bodySerializer: | Array>>(body: T): FormData => { - const data = new FormData() - - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return - } - if (Array.isArray(value)) { - value.forEach((v) => serializeFormDataPair(data, key, v)) - } else { - serializeFormDataPair(data, key, value) - } - }) - - return data - }, -} - -export const jsonBodySerializer = { - bodySerializer: (body: T): string => - JSON.stringify(body, (_key, value) => (typeof value === "bigint" ? value.toString() : value)), -} - -export const urlSearchParamsBodySerializer = { - bodySerializer: | Array>>(body: T): string => { - const data = new URLSearchParams() - - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return - } - if (Array.isArray(value)) { - value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)) - } else { - serializeUrlSearchParamsPair(data, key, value) - } - }) - - return data.toString() - }, -} diff --git a/packages/sdk/js/src/gen/core/params.gen.ts b/packages/sdk/js/src/gen/core/params.gen.ts deleted file mode 100644 index 68ad1a778ee9..000000000000 --- a/packages/sdk/js/src/gen/core/params.gen.ts +++ /dev/null @@ -1,144 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -type Slot = "body" | "headers" | "path" | "query" - -export type Field = - | { - in: Exclude - /** - * Field name. This is the name we want the user to see and use. - */ - key: string - /** - * Field mapped name. This is the name we want to use in the request. - * If omitted, we use the same value as `key`. - */ - map?: string - } - | { - in: Extract - /** - * Key isn't required for bodies. - */ - key?: string - map?: string - } - -export interface Fields { - allowExtra?: Partial> - args?: ReadonlyArray -} - -export type FieldsConfig = ReadonlyArray - -const extraPrefixesMap: Record = { - $body_: "body", - $headers_: "headers", - $path_: "path", - $query_: "query", -} -const extraPrefixes = Object.entries(extraPrefixesMap) - -type KeyMap = Map< - string, - { - in: Slot - map?: string - } -> - -const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { - if (!map) { - map = new Map() - } - - for (const config of fields) { - if ("in" in config) { - if (config.key) { - map.set(config.key, { - in: config.in, - map: config.map, - }) - } - } else if (config.args) { - buildKeyMap(config.args, map) - } - } - - return map -} - -interface Params { - body: unknown - headers: Record - path: Record - query: Record -} - -const stripEmptySlots = (params: Params) => { - for (const [slot, value] of Object.entries(params)) { - if (value && typeof value === "object" && !Object.keys(value).length) { - delete params[slot as Slot] - } - } -} - -export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { - const params: Params = { - body: {}, - headers: {}, - path: {}, - query: {}, - } - - const map = buildKeyMap(fields) - - let config: FieldsConfig[number] | undefined - - for (const [index, arg] of args.entries()) { - if (fields[index]) { - config = fields[index] - } - - if (!config) { - continue - } - - if ("in" in config) { - if (config.key) { - const field = map.get(config.key)! - const name = field.map || config.key - ;(params[field.in] as Record)[name] = arg - } else { - params.body = arg - } - } else { - for (const [key, value] of Object.entries(arg ?? {})) { - const field = map.get(key) - - if (field) { - const name = field.map || key - ;(params[field.in] as Record)[name] = value - } else { - const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)) - - if (extra) { - const [prefix, slot] = extra - ;(params[slot] as Record)[key.slice(prefix.length)] = value - } else { - for (const [slot, allowed] of Object.entries(config.allowExtra ?? {})) { - if (allowed) { - ;(params[slot as Slot] as Record)[key] = value - break - } - } - } - } - } - } - } - - stripEmptySlots(params) - - return params -} diff --git a/packages/sdk/js/src/gen/core/pathSerializer.gen.ts b/packages/sdk/js/src/gen/core/pathSerializer.gen.ts deleted file mode 100644 index 96be3bc5a397..000000000000 --- a/packages/sdk/js/src/gen/core/pathSerializer.gen.ts +++ /dev/null @@ -1,167 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} - -interface SerializePrimitiveOptions { - allowReserved?: boolean - name: string -} - -export interface SerializerOptions { - /** - * @default true - */ - explode: boolean - style: T -} - -export type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited" -export type ArraySeparatorStyle = ArrayStyle | MatrixStyle -type MatrixStyle = "label" | "matrix" | "simple" -export type ObjectStyle = "form" | "deepObject" -type ObjectSeparatorStyle = ObjectStyle | MatrixStyle - -interface SerializePrimitiveParam extends SerializePrimitiveOptions { - value: string -} - -export const separatorArrayExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case "label": - return "." - case "matrix": - return ";" - case "simple": - return "," - default: - return "&" - } -} - -export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case "form": - return "," - case "pipeDelimited": - return "|" - case "spaceDelimited": - return "%20" - default: - return "," - } -} - -export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { - switch (style) { - case "label": - return "." - case "matrix": - return ";" - case "simple": - return "," - default: - return "&" - } -} - -export const serializeArrayParam = ({ - allowReserved, - explode, - name, - style, - value, -}: SerializeOptions & { - value: unknown[] -}) => { - if (!explode) { - const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v as string))).join( - separatorArrayNoExplode(style), - ) - switch (style) { - case "label": - return `.${joinedValues}` - case "matrix": - return `;${name}=${joinedValues}` - case "simple": - return joinedValues - default: - return `${name}=${joinedValues}` - } - } - - const separator = separatorArrayExplode(style) - const joinedValues = value - .map((v) => { - if (style === "label" || style === "simple") { - return allowReserved ? v : encodeURIComponent(v as string) - } - - return serializePrimitiveParam({ - allowReserved, - name, - value: v as string, - }) - }) - .join(separator) - return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues -} - -export const serializePrimitiveParam = ({ allowReserved, name, value }: SerializePrimitiveParam) => { - if (value === undefined || value === null) { - return "" - } - - if (typeof value === "object") { - throw new Error( - "Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.", - ) - } - - return `${name}=${allowReserved ? value : encodeURIComponent(value)}` -} - -export const serializeObjectParam = ({ - allowReserved, - explode, - name, - style, - value, - valueOnly, -}: SerializeOptions & { - value: Record | Date - valueOnly?: boolean -}) => { - if (value instanceof Date) { - return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}` - } - - if (style !== "deepObject" && !explode) { - let values: string[] = [] - Object.entries(value).forEach(([key, v]) => { - values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)] - }) - const joinedValues = values.join(",") - switch (style) { - case "form": - return `${name}=${joinedValues}` - case "label": - return `.${joinedValues}` - case "matrix": - return `;${name}=${joinedValues}` - default: - return joinedValues - } - } - - const separator = separatorObjectExplode(style) - const joinedValues = Object.entries(value) - .map(([key, v]) => - serializePrimitiveParam({ - allowReserved, - name: style === "deepObject" ? `${name}[${key}]` : key, - value: v as string, - }), - ) - .join(separator) - return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues -} diff --git a/packages/sdk/js/src/gen/core/queryKeySerializer.gen.ts b/packages/sdk/js/src/gen/core/queryKeySerializer.gen.ts deleted file mode 100644 index 320204aef108..000000000000 --- a/packages/sdk/js/src/gen/core/queryKeySerializer.gen.ts +++ /dev/null @@ -1,111 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -/** - * JSON-friendly union that mirrors what Pinia Colada can hash. - */ -export type JsonValue = null | string | number | boolean | JsonValue[] | { [key: string]: JsonValue } - -/** - * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. - */ -export const queryKeyJsonReplacer = (_key: string, value: unknown) => { - if (value === undefined || typeof value === "function" || typeof value === "symbol") { - return undefined - } - if (typeof value === "bigint") { - return value.toString() - } - if (value instanceof Date) { - return value.toISOString() - } - return value -} - -/** - * Safely stringifies a value and parses it back into a JsonValue. - */ -export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { - try { - const json = JSON.stringify(input, queryKeyJsonReplacer) - if (json === undefined) { - return undefined - } - return JSON.parse(json) as JsonValue - } catch { - return undefined - } -} - -/** - * Detects plain objects (including objects with a null prototype). - */ -const isPlainObject = (value: unknown): value is Record => { - if (value === null || typeof value !== "object") { - return false - } - const prototype = Object.getPrototypeOf(value as object) - return prototype === Object.prototype || prototype === null -} - -/** - * Turns URLSearchParams into a sorted JSON object for deterministic keys. - */ -const serializeSearchParams = (params: URLSearchParams): JsonValue => { - const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)) - const result: Record = {} - - for (const [key, value] of entries) { - const existing = result[key] - if (existing === undefined) { - result[key] = value - continue - } - - if (Array.isArray(existing)) { - ;(existing as string[]).push(value) - } else { - result[key] = [existing, value] - } - } - - return result -} - -/** - * Normalizes any accepted value into a JSON-friendly shape for query keys. - */ -export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { - if (value === null) { - return null - } - - if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { - return value - } - - if (value === undefined || typeof value === "function" || typeof value === "symbol") { - return undefined - } - - if (typeof value === "bigint") { - return value.toString() - } - - if (value instanceof Date) { - return value.toISOString() - } - - if (Array.isArray(value)) { - return stringifyToJsonValue(value) - } - - if (typeof URLSearchParams !== "undefined" && value instanceof URLSearchParams) { - return serializeSearchParams(value) - } - - if (isPlainObject(value)) { - return stringifyToJsonValue(value) - } - - return undefined -} diff --git a/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts b/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts deleted file mode 100644 index ffc4f16dc1f3..000000000000 --- a/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts +++ /dev/null @@ -1,210 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Config } from "./types.gen.js" - -export type ServerSentEventsOptions = Omit & - Pick & { - /** - * Callback invoked when a network or parsing error occurs during streaming. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param error The error that occurred. - */ - onSseError?: (error: unknown) => void - /** - * Callback invoked when an event is streamed from the server. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param event Event streamed from the server. - * @returns Nothing (void). - */ - onSseEvent?: (event: StreamEvent) => void - /** - * Default retry delay in milliseconds. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 3000 - */ - sseDefaultRetryDelay?: number - /** - * Maximum number of retry attempts before giving up. - */ - sseMaxRetryAttempts?: number - /** - * Maximum retry delay in milliseconds. - * - * Applies only when exponential backoff is used. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 30000 - */ - sseMaxRetryDelay?: number - /** - * Optional sleep function for retry backoff. - * - * Defaults to using `setTimeout`. - */ - sseSleepFn?: (ms: number) => Promise - url: string - } - -export interface StreamEvent { - data: TData - event?: string - id?: string - retry?: number -} - -export type ServerSentEventsResult = { - stream: AsyncGenerator ? TData[keyof TData] : TData, TReturn, TNext> -} - -export const createSseClient = ({ - onSseError, - onSseEvent, - responseTransformer, - responseValidator, - sseDefaultRetryDelay, - sseMaxRetryAttempts, - sseMaxRetryDelay, - sseSleepFn, - url, - ...options -}: ServerSentEventsOptions): ServerSentEventsResult => { - let lastEventId: string | undefined - - const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))) - - const createStream = async function* () { - let retryDelay: number = sseDefaultRetryDelay ?? 3000 - let attempt = 0 - const signal = options.signal ?? new AbortController().signal - - while (true) { - if (signal.aborted) break - - attempt++ - - const headers = - options.headers instanceof Headers - ? options.headers - : new Headers(options.headers as Record | undefined) - - if (lastEventId !== undefined) { - headers.set("Last-Event-ID", lastEventId) - } - - try { - const response = await fetch(url, { ...options, headers, signal }) - - if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`) - - if (!response.body) throw new Error("No body in SSE response") - - const reader = response.body.pipeThrough(new TextDecoderStream()).getReader() - - let buffer = "" - - const abortHandler = () => { - try { - void reader.cancel() - } catch { - // noop - } - } - - signal.addEventListener("abort", abortHandler) - - try { - while (true) { - const { done, value } = await reader.read() - if (done) break - buffer += value - - const chunks = buffer.split("\n\n") - buffer = chunks.pop() ?? "" - - for (const chunk of chunks) { - const lines = chunk.split("\n") - const dataLines: Array = [] - let eventName: string | undefined - - for (const line of lines) { - if (line.startsWith("data:")) { - dataLines.push(line.replace(/^data:\s*/, "")) - } else if (line.startsWith("event:")) { - eventName = line.replace(/^event:\s*/, "") - } else if (line.startsWith("id:")) { - lastEventId = line.replace(/^id:\s*/, "") - } else if (line.startsWith("retry:")) { - const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10) - if (!Number.isNaN(parsed)) { - retryDelay = parsed - } - } - } - - let data: unknown - let parsedJson = false - - if (dataLines.length) { - const rawData = dataLines.join("\n") - try { - data = JSON.parse(rawData) - parsedJson = true - } catch { - data = rawData - } - } - - if (parsedJson) { - if (responseValidator) { - await responseValidator(data) - } - - if (responseTransformer) { - data = await responseTransformer(data) - } - } - - onSseEvent?.({ - data, - event: eventName, - id: lastEventId, - retry: retryDelay, - }) - - if (dataLines.length) { - yield data as any - } - } - } - } finally { - signal.removeEventListener("abort", abortHandler) - reader.releaseLock() - } - - break // exit loop on normal completion - } catch (error) { - // connection failed or aborted; retry after delay - onSseError?.(error) - - if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { - break // stop after firing error - } - - // exponential backoff: double retry each attempt, cap at 30s - const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000) - await sleep(backoff) - } - } - } - - const stream = createStream() - - return { stream } -} diff --git a/packages/sdk/js/src/gen/core/types.gen.ts b/packages/sdk/js/src/gen/core/types.gen.ts deleted file mode 100644 index 16408b2d09ce..000000000000 --- a/packages/sdk/js/src/gen/core/types.gen.ts +++ /dev/null @@ -1,91 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth, AuthToken } from "./auth.gen.js" -import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.gen.js" - -export interface Client { - /** - * Returns the final request URL. - */ - buildUrl: BuildUrlFn - connect: MethodFn - delete: MethodFn - get: MethodFn - getConfig: () => Config - head: MethodFn - options: MethodFn - patch: MethodFn - post: MethodFn - put: MethodFn - request: RequestFn - setConfig: (config: Config) => Config - trace: MethodFn -} - -export interface Config { - /** - * Auth token or a function returning auth token. The resolved value will be - * added to the request payload as defined by its `security` array. - */ - auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken - /** - * A function for serializing request body parameter. By default, - * {@link JSON.stringify()} will be used. - */ - bodySerializer?: BodySerializer | null - /** - * An object containing any HTTP headers that you want to pre-populate your - * `Headers` object with. - * - * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} - */ - headers?: - | RequestInit["headers"] - | Record - /** - * The request method. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} - */ - method?: "CONNECT" | "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PUT" | "TRACE" - /** - * A function for serializing request query parameters. By default, arrays - * will be exploded in form style, objects will be exploded in deepObject - * style, and reserved characters are percent-encoded. - * - * This method will have no effect if the native `paramsSerializer()` Axios - * API function is used. - * - * {@link https://swagger.io/docs/specification/serialization/#query View examples} - */ - querySerializer?: QuerySerializer | QuerySerializerOptions - /** - * A function validating request data. This is useful if you want to ensure - * the request conforms to the desired shape, so it can be safely sent to - * the server. - */ - requestValidator?: (data: unknown) => Promise - /** - * A function transforming response data before it's returned. This is useful - * for post-processing data, e.g. converting ISO strings into Date objects. - */ - responseTransformer?: (data: unknown) => Promise - /** - * A function validating response data. This is useful if you want to ensure - * the response conforms to the desired shape, so it can be safely passed to - * the transformers and returned to the user. - */ - responseValidator?: (data: unknown) => Promise -} - -type IsExactlyNeverOrNeverUndefined = [T] extends [never] - ? true - : [T] extends [never | undefined] - ? [undefined] extends [T] - ? false - : true - : false - -export type OmitNever> = { - [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K] -} diff --git a/packages/sdk/js/src/gen/core/utils.gen.ts b/packages/sdk/js/src/gen/core/utils.gen.ts deleted file mode 100644 index be18c608a5df..000000000000 --- a/packages/sdk/js/src/gen/core/utils.gen.ts +++ /dev/null @@ -1,109 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { QuerySerializer } from "./bodySerializer.gen.js" -import { - type ArraySeparatorStyle, - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from "./pathSerializer.gen.js" - -export interface PathSerializer { - path: Record - url: string -} - -export const PATH_PARAM_RE = /\{[^{}]+\}/g - -export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url - const matches = _url.match(PATH_PARAM_RE) - if (matches) { - for (const match of matches) { - let explode = false - let name = match.substring(1, match.length - 1) - let style: ArraySeparatorStyle = "simple" - - if (name.endsWith("*")) { - explode = true - name = name.substring(0, name.length - 1) - } - - if (name.startsWith(".")) { - name = name.substring(1) - style = "label" - } else if (name.startsWith(";")) { - name = name.substring(1) - style = "matrix" - } - - const value = path[name] - - if (value === undefined || value === null) { - continue - } - - if (Array.isArray(value)) { - url = url.replace(match, serializeArrayParam({ explode, name, style, value })) - continue - } - - if (typeof value === "object") { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ) - continue - } - - if (style === "matrix") { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ) - continue - } - - const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string)) - url = url.replace(match, replaceValue) - } - } - return url -} - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string - path?: Record - query?: Record - querySerializer: QuerySerializer - url: string -}) => { - const pathUrl = _url.startsWith("/") ? _url : `/${_url}` - let url = (baseUrl ?? "") + pathUrl - if (path) { - url = defaultPathSerializer({ path, url }) - } - let search = query ? querySerializer(query) : "" - if (search.startsWith("?")) { - search = search.substring(1) - } - if (search) { - url += `?${search}` - } - return url -} diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts deleted file mode 100644 index 4b5d9284c675..000000000000 --- a/packages/sdk/js/src/gen/sdk.gen.ts +++ /dev/null @@ -1,1184 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Options as ClientOptions, TDataShape, Client } from "./client/index.js" -import type { - GlobalEventData, - GlobalEventResponses, - ProjectListData, - ProjectListResponses, - ProjectCurrentData, - ProjectCurrentResponses, - PtyListData, - PtyListResponses, - PtyCreateData, - PtyCreateResponses, - PtyCreateErrors, - PtyRemoveData, - PtyRemoveResponses, - PtyRemoveErrors, - PtyGetData, - PtyGetResponses, - PtyGetErrors, - PtyUpdateData, - PtyUpdateResponses, - PtyUpdateErrors, - PtyConnectData, - PtyConnectResponses, - PtyConnectErrors, - ConfigGetData, - ConfigGetResponses, - ConfigUpdateData, - ConfigUpdateResponses, - ConfigUpdateErrors, - ToolIdsData, - ToolIdsResponses, - ToolIdsErrors, - ToolListData, - ToolListResponses, - ToolListErrors, - InstanceDisposeData, - InstanceDisposeResponses, - PathGetData, - PathGetResponses, - VcsGetData, - VcsGetResponses, - SessionListData, - SessionListResponses, - SessionCreateData, - SessionCreateResponses, - SessionCreateErrors, - SessionStatusData, - SessionStatusResponses, - SessionStatusErrors, - SessionDeleteData, - SessionDeleteResponses, - SessionDeleteErrors, - SessionGetData, - SessionGetResponses, - SessionGetErrors, - SessionUpdateData, - SessionUpdateResponses, - SessionUpdateErrors, - SessionChildrenData, - SessionChildrenResponses, - SessionChildrenErrors, - SessionInitData, - SessionInitResponses, - SessionInitErrors, - SessionForkData, - SessionForkResponses, - SessionAbortData, - SessionAbortResponses, - SessionAbortErrors, - SessionUnshareData, - SessionUnshareResponses, - SessionUnshareErrors, - SessionShareData, - SessionShareResponses, - SessionShareErrors, - SessionDiffData, - SessionDiffResponses, - SessionDiffErrors, - SessionSummarizeData, - SessionSummarizeResponses, - SessionSummarizeErrors, - SessionMessagesData, - SessionMessagesResponses, - SessionMessagesErrors, - SessionPromptData, - SessionPromptResponses, - SessionPromptErrors, - SessionMessageData, - SessionMessageResponses, - SessionMessageErrors, - SessionPromptAsyncData, - SessionPromptAsyncResponses, - SessionPromptAsyncErrors, - SessionCommandData, - SessionCommandResponses, - SessionCommandErrors, - SessionShellData, - SessionShellResponses, - SessionShellErrors, - SessionRevertData, - SessionRevertResponses, - SessionRevertErrors, - SessionUnrevertData, - SessionUnrevertResponses, - SessionUnrevertErrors, - PostSessionIdPermissionsPermissionIdData, - PostSessionIdPermissionsPermissionIdResponses, - PostSessionIdPermissionsPermissionIdErrors, - CommandListData, - CommandListResponses, - ConfigProvidersData, - ConfigProvidersResponses, - ProviderListData, - ProviderListResponses, - ProviderAuthData, - ProviderAuthResponses, - ProviderOauthAuthorizeData, - ProviderOauthAuthorizeResponses, - ProviderOauthAuthorizeErrors, - ProviderOauthCallbackData, - ProviderOauthCallbackResponses, - ProviderOauthCallbackErrors, - FindTextData, - FindTextResponses, - FindFilesData, - FindFilesResponses, - FindSymbolsData, - FindSymbolsResponses, - FileListData, - FileListResponses, - FileReadData, - FileReadResponses, - FileStatusData, - FileStatusResponses, - AppLogData, - AppLogResponses, - AppLogErrors, - AppAgentsData, - AppAgentsResponses, - McpStatusData, - McpStatusResponses, - McpAddData, - McpAddResponses, - McpAddErrors, - McpAuthRemoveData, - McpAuthRemoveResponses, - McpAuthRemoveErrors, - McpAuthStartData, - McpAuthStartResponses, - McpAuthStartErrors, - McpAuthCallbackData, - McpAuthCallbackResponses, - McpAuthCallbackErrors, - McpAuthAuthenticateData, - McpAuthAuthenticateResponses, - McpAuthAuthenticateErrors, - McpConnectData, - McpConnectResponses, - McpDisconnectData, - McpDisconnectResponses, - LspStatusData, - LspStatusResponses, - FormatterStatusData, - FormatterStatusResponses, - TuiAppendPromptData, - TuiAppendPromptResponses, - TuiAppendPromptErrors, - TuiOpenHelpData, - TuiOpenHelpResponses, - TuiOpenSessionsData, - TuiOpenSessionsResponses, - TuiOpenThemesData, - TuiOpenThemesResponses, - TuiOpenModelsData, - TuiOpenModelsResponses, - TuiSubmitPromptData, - TuiSubmitPromptResponses, - TuiClearPromptData, - TuiClearPromptResponses, - TuiExecuteCommandData, - TuiExecuteCommandResponses, - TuiExecuteCommandErrors, - TuiShowToastData, - TuiShowToastResponses, - TuiPublishData, - TuiPublishResponses, - TuiPublishErrors, - TuiControlNextData, - TuiControlNextResponses, - TuiControlResponseData, - TuiControlResponseResponses, - AuthSetData, - AuthSetResponses, - AuthSetErrors, - EventSubscribeData, - EventSubscribeResponses, -} from "./types.gen.js" -import { client as _heyApiClient } from "./client.gen.js" - -export type Options = ClientOptions< - TData, - ThrowOnError -> & { - /** - * You can provide a client instance returned by `createClient()` instead of - * individual options. This might be also useful if you want to implement a - * custom client. - */ - client?: Client - /** - * You can pass arbitrary values through the `meta` object. This can be - * used to access values that aren't defined as part of the SDK function. - */ - meta?: Record -} - -class _HeyApiClient { - protected _client: Client = _heyApiClient - - constructor(args?: { client?: Client }) { - if (args?.client) { - this._client = args.client - } - } -} - -class Global extends _HeyApiClient { - /** - * Get events - */ - public event(options?: Options) { - return (options?.client ?? this._client).get.sse({ - url: "/global/event", - ...options, - }) - } -} - -class Project extends _HeyApiClient { - /** - * List all projects - */ - public list(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/project", - ...options, - }) - } - - /** - * Get the current project - */ - public current(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/project/current", - ...options, - }) - } -} - -class Pty extends _HeyApiClient { - /** - * List all PTY sessions - */ - public list(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/pty", - ...options, - }) - } - - /** - * Create a new PTY session - */ - public create(options?: Options) { - return (options?.client ?? this._client).post({ - url: "/pty", - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }) - } - - /** - * Remove a PTY session - */ - public remove(options: Options) { - return (options.client ?? this._client).delete({ - url: "/pty/{id}", - ...options, - }) - } - - /** - * Get PTY session info - */ - public get(options: Options) { - return (options.client ?? this._client).get({ - url: "/pty/{id}", - ...options, - }) - } - - /** - * Update PTY session - */ - public update(options: Options) { - return (options.client ?? this._client).put({ - url: "/pty/{id}", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }) - } - - /** - * Connect to a PTY session - */ - public connect(options: Options) { - return (options.client ?? this._client).get({ - url: "/pty/{id}/connect", - ...options, - }) - } -} - -class Config extends _HeyApiClient { - /** - * Get config info - */ - public get(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/config", - ...options, - }) - } - - /** - * Update config - */ - public update(options?: Options) { - return (options?.client ?? this._client).patch({ - url: "/config", - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }) - } - - /** - * List all providers - */ - public providers(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/config/providers", - ...options, - }) - } -} - -class Tool extends _HeyApiClient { - /** - * List all tool IDs (including built-in and dynamically registered) - */ - public ids(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/experimental/tool/ids", - ...options, - }) - } - - /** - * List tools with JSON schema parameters for a provider/model - */ - public list(options: Options) { - return (options.client ?? this._client).get({ - url: "/experimental/tool", - ...options, - }) - } -} - -class Instance extends _HeyApiClient { - /** - * Dispose the current instance - */ - public dispose(options?: Options) { - return (options?.client ?? this._client).post({ - url: "/instance/dispose", - ...options, - }) - } -} - -class Path extends _HeyApiClient { - /** - * Get the current path - */ - public get(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/path", - ...options, - }) - } -} - -class Vcs extends _HeyApiClient { - /** - * Get VCS info for the current instance - */ - public get(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/vcs", - ...options, - }) - } -} - -class Session extends _HeyApiClient { - /** - * List all sessions - */ - public list(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/session", - ...options, - }) - } - - /** - * Create a new session - */ - public create(options?: Options) { - return (options?.client ?? this._client).post({ - url: "/session", - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }) - } - - /** - * Get session status - */ - public status(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/session/status", - ...options, - }) - } - - /** - * Delete a session and all its data - */ - public delete(options: Options) { - return (options.client ?? this._client).delete({ - url: "/session/{id}", - ...options, - }) - } - - /** - * Get session - */ - public get(options: Options) { - return (options.client ?? this._client).get({ - url: "/session/{id}", - ...options, - }) - } - - /** - * Update session properties - */ - public update(options: Options) { - return (options.client ?? this._client).patch({ - url: "/session/{id}", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }) - } - - /** - * Get a session's children - */ - public children(options: Options) { - return (options.client ?? this._client).get({ - url: "/session/{id}/children", - ...options, - }) - } - - /** - * Analyze the app and create an AGENTS.md file - */ - public init(options: Options) { - return (options.client ?? this._client).post({ - url: "/session/{id}/init", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }) - } - - /** - * Fork an existing session at a specific message - */ - public fork(options: Options) { - return (options.client ?? this._client).post({ - url: "/session/{id}/fork", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }) - } - - /** - * Abort a session - */ - public abort(options: Options) { - return (options.client ?? this._client).post({ - url: "/session/{id}/abort", - ...options, - }) - } - - /** - * Unshare the session - */ - public unshare(options: Options) { - return (options.client ?? this._client).delete({ - url: "/session/{id}/share", - ...options, - }) - } - - /** - * Share a session - */ - public share(options: Options) { - return (options.client ?? this._client).post({ - url: "/session/{id}/share", - ...options, - }) - } - - /** - * Get the diff for this session - */ - public diff(options: Options) { - return (options.client ?? this._client).get({ - url: "/session/{id}/diff", - ...options, - }) - } - - /** - * Summarize the session - */ - public summarize(options: Options) { - return (options.client ?? this._client).post({ - url: "/session/{id}/summarize", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }) - } - - /** - * List messages for a session - */ - public messages(options: Options) { - return (options.client ?? this._client).get({ - url: "/session/{id}/message", - ...options, - }) - } - - /** - * Create and send a new message to a session - */ - public prompt(options: Options) { - return (options.client ?? this._client).post({ - url: "/session/{id}/message", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }) - } - - /** - * Get a message from a session - */ - public message(options: Options) { - return (options.client ?? this._client).get({ - url: "/session/{id}/message/{messageID}", - ...options, - }) - } - - /** - * Create and send a new message to a session, start if needed and return immediately - */ - public promptAsync(options: Options) { - return (options.client ?? this._client).post({ - url: "/session/{id}/prompt_async", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }) - } - - /** - * Send a new command to a session - */ - public command(options: Options) { - return (options.client ?? this._client).post({ - url: "/session/{id}/command", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }) - } - - /** - * Run a shell command - */ - public shell(options: Options) { - return (options.client ?? this._client).post({ - url: "/session/{id}/shell", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }) - } - - /** - * Revert a message - */ - public revert(options: Options) { - return (options.client ?? this._client).post({ - url: "/session/{id}/revert", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }) - } - - /** - * Restore all reverted messages - */ - public unrevert(options: Options) { - return (options.client ?? this._client).post({ - url: "/session/{id}/unrevert", - ...options, - }) - } -} - -class Command extends _HeyApiClient { - /** - * List all commands - */ - public list(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/command", - ...options, - }) - } -} - -class Oauth extends _HeyApiClient { - /** - * Authorize a provider using OAuth - */ - public authorize(options: Options) { - return (options.client ?? this._client).post< - ProviderOauthAuthorizeResponses, - ProviderOauthAuthorizeErrors, - ThrowOnError - >({ - url: "/provider/{id}/oauth/authorize", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }) - } - - /** - * Handle OAuth callback for a provider - */ - public callback(options: Options) { - return (options.client ?? this._client).post< - ProviderOauthCallbackResponses, - ProviderOauthCallbackErrors, - ThrowOnError - >({ - url: "/provider/{id}/oauth/callback", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }) - } -} - -class Provider extends _HeyApiClient { - /** - * List all providers - */ - public list(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/provider", - ...options, - }) - } - - /** - * Get provider authentication methods - */ - public auth(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/provider/auth", - ...options, - }) - } - oauth = new Oauth({ client: this._client }) -} - -class Find extends _HeyApiClient { - /** - * Find text in files - */ - public text(options: Options) { - return (options.client ?? this._client).get({ - url: "/find", - ...options, - }) - } - - /** - * Find files - */ - public files(options: Options) { - return (options.client ?? this._client).get({ - url: "/find/file", - ...options, - }) - } - - /** - * Find workspace symbols - */ - public symbols(options: Options) { - return (options.client ?? this._client).get({ - url: "/find/symbol", - ...options, - }) - } -} - -class File extends _HeyApiClient { - /** - * List files and directories - */ - public list(options: Options) { - return (options.client ?? this._client).get({ - url: "/file", - ...options, - }) - } - - /** - * Read a file - */ - public read(options: Options) { - return (options.client ?? this._client).get({ - url: "/file/content", - ...options, - }) - } - - /** - * Get file status - */ - public status(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/file/status", - ...options, - }) - } -} - -class App extends _HeyApiClient { - /** - * Write a log entry to the server logs - */ - public log(options?: Options) { - return (options?.client ?? this._client).post({ - url: "/log", - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }) - } - - /** - * List all agents - */ - public agents(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/agent", - ...options, - }) - } -} - -class Auth extends _HeyApiClient { - /** - * Remove OAuth credentials for an MCP server - */ - public remove(options: Options) { - return (options.client ?? this._client).delete({ - url: "/mcp/{name}/auth", - ...options, - }) - } - - /** - * Start OAuth authentication flow for an MCP server - */ - public start(options: Options) { - return (options.client ?? this._client).post({ - url: "/mcp/{name}/auth", - ...options, - }) - } - - /** - * Complete OAuth authentication with authorization code - */ - public callback(options: Options) { - return (options.client ?? this._client).post({ - url: "/mcp/{name}/auth/callback", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }) - } - - /** - * Start OAuth flow and wait for callback (opens browser) - */ - public authenticate(options: Options) { - return (options.client ?? this._client).post( - { - url: "/mcp/{name}/auth/authenticate", - ...options, - }, - ) - } - - /** - * Set authentication credentials - */ - public set(options: Options) { - return (options.client ?? this._client).put({ - url: "/auth/{id}", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }) - } -} - -class Mcp extends _HeyApiClient { - /** - * Get MCP server status - */ - public status(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/mcp", - ...options, - }) - } - - /** - * Add MCP server dynamically - */ - public add(options?: Options) { - return (options?.client ?? this._client).post({ - url: "/mcp", - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }) - } - - /** - * Connect an MCP server - */ - public connect(options: Options) { - return (options.client ?? this._client).post({ - url: "/mcp/{name}/connect", - ...options, - }) - } - - /** - * Disconnect an MCP server - */ - public disconnect(options: Options) { - return (options.client ?? this._client).post({ - url: "/mcp/{name}/disconnect", - ...options, - }) - } - - auth = new Auth({ client: this._client }) -} - -class Lsp extends _HeyApiClient { - /** - * Get LSP server status - */ - public status(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/lsp", - ...options, - }) - } -} - -class Formatter extends _HeyApiClient { - /** - * Get formatter status - */ - public status(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/formatter", - ...options, - }) - } -} - -class Control extends _HeyApiClient { - /** - * Get the next TUI request from the queue - */ - public next(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/tui/control/next", - ...options, - }) - } - - /** - * Submit a response to the TUI request queue - */ - public response(options?: Options) { - return (options?.client ?? this._client).post({ - url: "/tui/control/response", - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }) - } -} - -class Tui extends _HeyApiClient { - /** - * Append prompt to the TUI - */ - public appendPrompt(options?: Options) { - return (options?.client ?? this._client).post({ - url: "/tui/append-prompt", - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }) - } - - /** - * Open the help dialog - */ - public openHelp(options?: Options) { - return (options?.client ?? this._client).post({ - url: "/tui/open-help", - ...options, - }) - } - - /** - * Open the session dialog - */ - public openSessions(options?: Options) { - return (options?.client ?? this._client).post({ - url: "/tui/open-sessions", - ...options, - }) - } - - /** - * Open the theme dialog - */ - public openThemes(options?: Options) { - return (options?.client ?? this._client).post({ - url: "/tui/open-themes", - ...options, - }) - } - - /** - * Open the model dialog - */ - public openModels(options?: Options) { - return (options?.client ?? this._client).post({ - url: "/tui/open-models", - ...options, - }) - } - - /** - * Submit the prompt - */ - public submitPrompt(options?: Options) { - return (options?.client ?? this._client).post({ - url: "/tui/submit-prompt", - ...options, - }) - } - - /** - * Clear the prompt - */ - public clearPrompt(options?: Options) { - return (options?.client ?? this._client).post({ - url: "/tui/clear-prompt", - ...options, - }) - } - - /** - * Execute a TUI command (e.g. agent_cycle) - */ - public executeCommand(options?: Options) { - return (options?.client ?? this._client).post({ - url: "/tui/execute-command", - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }) - } - - /** - * Show a toast notification in the TUI - */ - public showToast(options?: Options) { - return (options?.client ?? this._client).post({ - url: "/tui/show-toast", - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }) - } - - /** - * Publish a TUI event - */ - public publish(options?: Options) { - return (options?.client ?? this._client).post({ - url: "/tui/publish", - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }) - } - control = new Control({ client: this._client }) -} - -class Event extends _HeyApiClient { - /** - * Get events - */ - public subscribe(options?: Options) { - return (options?.client ?? this._client).get.sse({ - url: "/event", - ...options, - }) - } -} - -export class OpencodeClient extends _HeyApiClient { - /** - * Respond to a permission request - */ - public postSessionIdPermissionsPermissionId( - options: Options, - ) { - return (options.client ?? this._client).post< - PostSessionIdPermissionsPermissionIdResponses, - PostSessionIdPermissionsPermissionIdErrors, - ThrowOnError - >({ - url: "/session/{id}/permissions/{permissionID}", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }) - } - global = new Global({ client: this._client }) - project = new Project({ client: this._client }) - pty = new Pty({ client: this._client }) - config = new Config({ client: this._client }) - tool = new Tool({ client: this._client }) - instance = new Instance({ client: this._client }) - path = new Path({ client: this._client }) - vcs = new Vcs({ client: this._client }) - session = new Session({ client: this._client }) - command = new Command({ client: this._client }) - provider = new Provider({ client: this._client }) - find = new Find({ client: this._client }) - file = new File({ client: this._client }) - app = new App({ client: this._client }) - mcp = new Mcp({ client: this._client }) - lsp = new Lsp({ client: this._client }) - formatter = new Formatter({ client: this._client }) - tui = new Tui({ client: this._client }) - auth = new Auth({ client: this._client }) - event = new Event({ client: this._client }) -} diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts deleted file mode 100644 index e9b704b4681f..000000000000 --- a/packages/sdk/js/src/gen/types.gen.ts +++ /dev/null @@ -1,3843 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type EventServerInstanceDisposed = { - type: "server.instance.disposed" - properties: { - directory: string - } -} - -export type EventInstallationUpdated = { - type: "installation.updated" - properties: { - version: string - } -} - -export type EventInstallationUpdateAvailable = { - type: "installation.update-available" - properties: { - version: string - } -} - -export type EventLspClientDiagnostics = { - type: "lsp.client.diagnostics" - properties: { - serverID: string - path: string - } -} - -export type EventLspUpdated = { - type: "lsp.updated" - properties: { - [key: string]: unknown - } -} - -export type FileDiff = { - file: string - before: string - after: string - additions: number - deletions: number -} - -export type UserMessage = { - id: string - sessionID: string - role: "user" - time: { - created: number - } - summary?: { - title?: string - body?: string - diffs: Array - } - agent: string - model: { - providerID: string - modelID: string - } - system?: string - tools?: { - [key: string]: boolean - } -} - -export type ProviderAuthError = { - name: "ProviderAuthError" - data: { - providerID: string - message: string - } -} - -export type UnknownError = { - name: "UnknownError" - data: { - message: string - } -} - -export type MessageOutputLengthError = { - name: "MessageOutputLengthError" - data: { - [key: string]: unknown - } -} - -export type MessageAbortedError = { - name: "MessageAbortedError" - data: { - message: string - } -} - -export type ApiError = { - name: "APIError" - data: { - message: string - statusCode?: number - isRetryable: boolean - responseHeaders?: { - [key: string]: string - } - responseBody?: string - } -} - -export type AssistantMessage = { - id: string - sessionID: string - role: "assistant" - time: { - created: number - completed?: number - } - error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError - parentID: string - modelID: string - providerID: string - mode: string - path: { - cwd: string - root: string - } - summary?: boolean - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } - finish?: string -} - -export type Message = UserMessage | AssistantMessage - -export type EventMessageUpdated = { - type: "message.updated" - properties: { - info: Message - } -} - -export type EventMessageRemoved = { - type: "message.removed" - properties: { - sessionID: string - messageID: string - } -} - -export type TextPart = { - id: string - sessionID: string - messageID: string - type: "text" - text: string - synthetic?: boolean - ignored?: boolean - time?: { - start: number - end?: number - } - metadata?: { - [key: string]: unknown - } -} - -export type ReasoningPart = { - id: string - sessionID: string - messageID: string - type: "reasoning" - text: string - metadata?: { - [key: string]: unknown - } - time: { - start: number - end?: number - } -} - -export type FilePartSourceText = { - value: string - start: number - end: number -} - -export type FileSource = { - text: FilePartSourceText - type: "file" - path: string -} - -export type Range = { - start: { - line: number - character: number - } - end: { - line: number - character: number - } -} - -export type SymbolSource = { - text: FilePartSourceText - type: "symbol" - path: string - range: Range - name: string - kind: number -} - -export type FilePartSource = FileSource | SymbolSource - -export type FilePart = { - id: string - sessionID: string - messageID: string - type: "file" - mime: string - filename?: string - url: string - source?: FilePartSource -} - -export type ToolStatePending = { - status: "pending" - input: { - [key: string]: unknown - } - raw: string -} - -export type ToolStateRunning = { - status: "running" - input: { - [key: string]: unknown - } - title?: string - metadata?: { - [key: string]: unknown - } - time: { - start: number - } -} - -export type ToolStateCompleted = { - status: "completed" - input: { - [key: string]: unknown - } - output: string - title: string - metadata: { - [key: string]: unknown - } - time: { - start: number - end: number - compacted?: number - } - attachments?: Array -} - -export type ToolStateError = { - status: "error" - input: { - [key: string]: unknown - } - error: string - metadata?: { - [key: string]: unknown - } - time: { - start: number - end: number - } -} - -export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError - -export type ToolPart = { - id: string - sessionID: string - messageID: string - type: "tool" - callID: string - tool: string - state: ToolState - metadata?: { - [key: string]: unknown - } -} - -export type StepStartPart = { - id: string - sessionID: string - messageID: string - type: "step-start" - snapshot?: string -} - -export type StepFinishPart = { - id: string - sessionID: string - messageID: string - type: "step-finish" - reason: string - snapshot?: string - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } -} - -export type SnapshotPart = { - id: string - sessionID: string - messageID: string - type: "snapshot" - snapshot: string -} - -export type PatchPart = { - id: string - sessionID: string - messageID: string - type: "patch" - hash: string - files: Array -} - -export type AgentPart = { - id: string - sessionID: string - messageID: string - type: "agent" - name: string - source?: { - value: string - start: number - end: number - } -} - -export type RetryPart = { - id: string - sessionID: string - messageID: string - type: "retry" - attempt: number - error: ApiError - time: { - created: number - } -} - -export type CompactionPart = { - id: string - sessionID: string - messageID: string - type: "compaction" - auto: boolean -} - -export type Part = - | TextPart - | { - id: string - sessionID: string - messageID: string - type: "subtask" - prompt: string - description: string - agent: string - } - | ReasoningPart - | FilePart - | ToolPart - | StepStartPart - | StepFinishPart - | SnapshotPart - | PatchPart - | AgentPart - | RetryPart - | CompactionPart - -export type EventMessagePartUpdated = { - type: "message.part.updated" - properties: { - part: Part - delta?: string - } -} - -export type EventMessagePartRemoved = { - type: "message.part.removed" - properties: { - sessionID: string - messageID: string - partID: string - } -} - -export type Permission = { - id: string - type: string - pattern?: string | Array - sessionID: string - messageID: string - callID?: string - title: string - metadata: { - [key: string]: unknown - } - time: { - created: number - } -} - -export type EventPermissionUpdated = { - type: "permission.updated" - properties: Permission -} - -export type EventPermissionReplied = { - type: "permission.replied" - properties: { - sessionID: string - permissionID: string - response: string - } -} - -export type SessionStatus = - | { - type: "idle" - } - | { - type: "retry" - attempt: number - message: string - next: number - } - | { - type: "busy" - } - -export type EventSessionStatus = { - type: "session.status" - properties: { - sessionID: string - status: SessionStatus - } -} - -export type EventSessionIdle = { - type: "session.idle" - properties: { - sessionID: string - } -} - -export type EventSessionCompacted = { - type: "session.compacted" - properties: { - sessionID: string - } -} - -export type EventFileEdited = { - type: "file.edited" - properties: { - file: string - } -} - -export type EventCommandExecuted = { - type: "command.executed" - properties: { - name: string - sessionID: string - arguments: string - messageID: string - } -} - -export type Session = { - id: string - projectID: string - directory: string - parentID?: string - summary?: { - additions: number - deletions: number - files: number - diffs?: Array - } - share?: { - url: string - } - title: string - version: string - time: { - created: number - updated: number - compacting?: number - } - revert?: { - messageID: string - partID?: string - snapshot?: string - diff?: string - } -} - -export type EventSessionCreated = { - type: "session.created" - properties: { - info: Session - } -} - -export type EventSessionUpdated = { - type: "session.updated" - properties: { - info: Session - } -} - -export type EventSessionDeleted = { - type: "session.deleted" - properties: { - info: Session - } -} - -export type EventSessionDiff = { - type: "session.diff" - properties: { - sessionID: string - diff: Array - } -} - -export type EventSessionError = { - type: "session.error" - properties: { - sessionID?: string - error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError - } -} - -export type EventFileWatcherUpdated = { - type: "file.watcher.updated" - properties: { - file: string - event: "add" | "change" | "unlink" - } -} - -export type EventVcsBranchUpdated = { - type: "vcs.branch.updated" - properties: { - branch?: string - } -} - -export type EventTuiPromptAppend = { - type: "tui.prompt.append" - properties: { - text: string - } -} - -export type EventTuiCommandExecute = { - type: "tui.command.execute" - properties: { - command: - | ( - | "session.list" - | "session.new" - | "session.share" - | "session.interrupt" - | "session.compact" - | "session.page.up" - | "session.page.down" - | "session.half.page.up" - | "session.half.page.down" - | "session.first" - | "session.last" - | "prompt.clear" - | "prompt.submit" - | "agent.cycle" - ) - | string - } -} - -export type EventTuiToastShow = { - type: "tui.toast.show" - properties: { - title?: string - message: string - variant: "info" | "success" | "warning" | "error" - /** - * Duration in milliseconds - */ - duration?: number - } -} - -export type Pty = { - id: string - title: string - command: string - args: Array - cwd: string - status: "running" | "exited" - pid: number -} - -export type EventPtyCreated = { - type: "pty.created" - properties: { - info: Pty - } -} - -export type EventPtyUpdated = { - type: "pty.updated" - properties: { - info: Pty - } -} - -export type EventPtyExited = { - type: "pty.exited" - properties: { - id: string - exitCode: number - } -} - -export type EventPtyDeleted = { - type: "pty.deleted" - properties: { - id: string - } -} - -export type EventServerConnected = { - type: "server.connected" - properties: { - [key: string]: unknown - } -} - -export type Event = - | EventServerInstanceDisposed - | EventInstallationUpdated - | EventInstallationUpdateAvailable - | EventLspClientDiagnostics - | EventLspUpdated - | EventMessageUpdated - | EventMessageRemoved - | EventMessagePartUpdated - | EventMessagePartRemoved - | EventPermissionUpdated - | EventPermissionReplied - | EventSessionStatus - | EventSessionIdle - | EventSessionCompacted - | EventFileEdited - | EventCommandExecuted - | EventSessionCreated - | EventSessionUpdated - | EventSessionDeleted - | EventSessionDiff - | EventSessionError - | EventFileWatcherUpdated - | EventVcsBranchUpdated - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow - | EventPtyCreated - | EventPtyUpdated - | EventPtyExited - | EventPtyDeleted - | EventServerConnected - -export type GlobalEvent = { - directory: string - payload: Event -} - -export type Project = { - id: string - worktree: string - vcsDir?: string - vcs?: "git" | "hg" - time: { - created: number - initialized?: number - } -} - -export type BadRequestError = { - name: "BadRequest" - data: { - message: string - kind?: "Params" | "Headers" | "Query" | "Body" | "Payload" - } -} - -export type NotFoundError = { - name: "NotFoundError" - data: { - message: string - } -} - -/** - * Custom keybind configurations - */ -export type KeybindsConfig = { - /** - * Leader key for keybind combinations - */ - leader?: string - /** - * Exit the application - */ - app_exit?: string - /** - * Open external editor - */ - editor_open?: string - /** - * List available themes - */ - theme_list?: string - /** - * Toggle sidebar - */ - sidebar_toggle?: string - /** - * Toggle session scrollbar - */ - scrollbar_toggle?: string - /** - * Toggle username visibility - */ - username_toggle?: string - /** - * View status - */ - status_view?: string - /** - * Export session to editor - */ - session_export?: string - /** - * Create a new session - */ - session_new?: string - /** - * List all sessions - */ - session_list?: string - /** - * Show session timeline - */ - session_timeline?: string - /** - * Share current session - */ - session_share?: string - /** - * Unshare current session - */ - session_unshare?: string - /** - * Interrupt current session - */ - session_interrupt?: string - /** - * Compact the session - */ - session_compact?: string - /** - * Scroll messages up by one page - */ - messages_page_up?: string - /** - * Scroll messages down by one page - */ - messages_page_down?: string - /** - * Scroll messages up by one line - */ - messages_line_up?: string - /** - * Scroll messages down by one line - */ - messages_line_down?: string - /** - * Scroll messages up by half page - */ - messages_half_page_up?: string - /** - * Scroll messages down by half page - */ - messages_half_page_down?: string - /** - * Navigate to first message - */ - messages_first?: string - /** - * Navigate to last message - */ - messages_last?: string - /** - * Navigate to next message - */ - messages_next?: string - /** - * Navigate to previous message - */ - messages_previous?: string - /** - * Navigate to last user message - */ - messages_last_user?: string - /** - * Copy message - */ - messages_copy?: string - /** - * Undo message - */ - messages_undo?: string - /** - * Redo message - */ - messages_redo?: string - /** - * Toggle code block concealment in messages - */ - messages_toggle_conceal?: string - /** - * Toggle tool details visibility - */ - tool_details?: string - /** - * List available models - */ - model_list?: string - /** - * Next recently used model - */ - model_cycle_recent?: string - /** - * Previous recently used model - */ - model_cycle_recent_reverse?: string - /** - * List available commands - */ - command_list?: string - /** - * List agents - */ - agent_list?: string - /** - * Next agent - */ - agent_cycle?: string - /** - * Previous agent - */ - agent_cycle_reverse?: string - /** - * Clear input field - */ - input_clear?: string - /** - * Forward delete - */ - input_forward_delete?: string - /** - * Paste from clipboard - */ - input_paste?: string - /** - * Submit input - */ - input_submit?: string - /** - * Insert newline in input - */ - input_newline?: string - /** - * Previous history item - */ - history_previous?: string - /** - * Next history item - */ - history_next?: string - /** - * Next child session - */ - session_child_cycle?: string - /** - * Previous child session - */ - session_child_cycle_reverse?: string - /** - * Suspend terminal - */ - terminal_suspend?: string - /** - * Toggle terminal title - */ - terminal_title_toggle?: string -} - -export type AgentConfig = { - model?: string - temperature?: number - top_p?: number - prompt?: string - tools?: { - [key: string]: boolean - } - disable?: boolean - /** - * Description of when to use the agent - */ - description?: string - mode?: "subagent" | "primary" | "all" - /** - * Hex color code for the agent (e.g., #FF5733) - */ - color?: string - /** - * Maximum number of agentic iterations before forcing text-only response - */ - maxSteps?: number - permission?: { - edit?: "ask" | "allow" | "deny" - bash?: - | ("ask" | "allow" | "deny") - | { - [key: string]: "ask" | "allow" | "deny" - } - webfetch?: "ask" | "allow" | "deny" - doom_loop?: "ask" | "allow" | "deny" - external_directory?: "ask" | "allow" | "deny" - } - [key: string]: - | unknown - | string - | number - | { - [key: string]: boolean - } - | boolean - | ("subagent" | "primary" | "all") - | number - | { - edit?: "ask" | "allow" | "deny" - bash?: - | ("ask" | "allow" | "deny") - | { - [key: string]: "ask" | "allow" | "deny" - } - webfetch?: "ask" | "allow" | "deny" - doom_loop?: "ask" | "allow" | "deny" - external_directory?: "ask" | "allow" | "deny" - } - | undefined -} - -export type ProviderConfig = { - api?: string - name?: string - env?: Array - id?: string - npm?: string - models?: { - [key: string]: { - id?: string - name?: string - release_date?: string - attachment?: boolean - reasoning?: boolean - temperature?: boolean - tool_call?: boolean - cost?: { - input: number - output: number - cache_read?: number - cache_write?: number - context_over_200k?: { - input: number - output: number - cache_read?: number - cache_write?: number - } - } - limit?: { - context: number - output: number - } - modalities?: { - input: Array<"text" | "audio" | "image" | "video" | "pdf"> - output: Array<"text" | "audio" | "image" | "video" | "pdf"> - } - experimental?: boolean - status?: "alpha" | "beta" | "deprecated" | "active" - options?: { - [key: string]: unknown - } - headers?: { - [key: string]: string - } - provider?: { - npm: string - } - } - } - whitelist?: Array - blacklist?: Array - options?: { - apiKey?: string - baseURL?: string - /** - * GitHub Enterprise URL for copilot authentication - */ - enterpriseUrl?: string - /** - * Enable promptCacheKey for this provider (default false) - */ - setCacheKey?: boolean - /** - * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout. - */ - timeout?: number | false - [key: string]: unknown | string | boolean | (number | false) | undefined - } -} - -export type McpLocalConfig = { - /** - * Type of MCP server connection - */ - type: "local" - /** - * Command and arguments to run the MCP server - */ - command: Array - /** - * Environment variables to set when running the MCP server - */ - environment?: { - [key: string]: string - } - /** - * Enable or disable the MCP server on startup - */ - enabled?: boolean - /** - * Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified. - */ - timeout?: number -} - -export type McpOAuthConfig = { - /** - * OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted. - */ - clientId?: string - /** - * OAuth client secret (if required by the authorization server) - */ - clientSecret?: string - /** - * OAuth scopes to request during authorization - */ - scope?: string -} - -export type McpRemoteConfig = { - /** - * Type of MCP server connection - */ - type: "remote" - /** - * URL of the remote MCP server - */ - url: string - /** - * Enable or disable the MCP server on startup - */ - enabled?: boolean - /** - * Headers to send with the request - */ - headers?: { - [key: string]: string - } - /** - * OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection. - */ - oauth?: McpOAuthConfig | false - /** - * Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified. - */ - timeout?: number -} - -/** - * @deprecated Always uses stretch layout. - */ -export type LayoutConfig = "auto" | "stretch" - -export type Config = { - /** - * JSON schema reference for configuration validation - */ - $schema?: string - /** - * Theme name to use for the interface - */ - theme?: string - keybinds?: KeybindsConfig - /** - * Log level - */ - logLevel?: "DEBUG" | "INFO" | "WARN" | "ERROR" - /** - * TUI specific settings - */ - tui?: { - /** - * TUI scroll speed - */ - scroll_speed?: number - /** - * Scroll acceleration settings - */ - scroll_acceleration?: { - /** - * Enable scroll acceleration - */ - enabled: boolean - } - /** - * Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column - */ - diff_style?: "auto" | "stacked" - } - /** - * Command configuration, see https://opencode.ai/docs/commands - */ - command?: { - [key: string]: { - template: string - description?: string - agent?: string - model?: string - subtask?: boolean - } - } - watcher?: { - ignore?: Array - } - plugin?: Array - snapshot?: boolean - /** - * Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing - */ - share?: "manual" | "auto" | "disabled" - /** - * @deprecated Use 'share' field instead. Share newly created sessions automatically - */ - autoshare?: boolean - /** - * Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications - */ - autoupdate?: boolean | "notify" - /** - * Disable providers that are loaded automatically - */ - disabled_providers?: Array - /** - * When set, ONLY these providers will be enabled. All other providers will be ignored - */ - enabled_providers?: Array - /** - * Model to use in the format of provider/model, eg anthropic/claude-2 - */ - model?: string - /** - * Small model to use for tasks like title generation in the format of provider/model - */ - small_model?: string - /** - * Custom username to display in conversations instead of system username - */ - username?: string - /** - * @deprecated Use `agent` field instead. - */ - mode?: { - build?: AgentConfig - plan?: AgentConfig - [key: string]: AgentConfig | undefined - } - /** - * Agent configuration, see https://opencode.ai/docs/agent - */ - agent?: { - plan?: AgentConfig - build?: AgentConfig - general?: AgentConfig - explore?: AgentConfig - [key: string]: AgentConfig | undefined - } - /** - * Custom provider configurations and model overrides - */ - provider?: { - [key: string]: ProviderConfig - } - /** - * MCP (Model Context Protocol) server configurations - */ - mcp?: { - [key: string]: McpLocalConfig | McpRemoteConfig - } - formatter?: - | false - | { - [key: string]: { - disabled?: boolean - command?: Array - environment?: { - [key: string]: string - } - extensions?: Array - } - } - lsp?: - | false - | { - [key: string]: - | { - disabled: true - } - | { - command: Array - extensions?: Array - disabled?: boolean - env?: { - [key: string]: string - } - initialization?: { - [key: string]: unknown - } - } - } - /** - * Additional instruction files or patterns to include - */ - instructions?: Array - layout?: LayoutConfig - permission?: { - edit?: "ask" | "allow" | "deny" - bash?: - | ("ask" | "allow" | "deny") - | { - [key: string]: "ask" | "allow" | "deny" - } - webfetch?: "ask" | "allow" | "deny" - doom_loop?: "ask" | "allow" | "deny" - external_directory?: "ask" | "allow" | "deny" - } - tools?: { - [key: string]: boolean - } - enterprise?: { - /** - * Enterprise URL - */ - url?: string - } - experimental?: { - hook?: { - file_edited?: { - [key: string]: Array<{ - command: Array - environment?: { - [key: string]: string - } - }> - } - session_completed?: Array<{ - command: Array - environment?: { - [key: string]: string - } - }> - } - /** - * Number of retries for chat completions on failure - */ - chatMaxRetries?: number - disable_paste_summary?: boolean - /** - * Enable the batch tool - */ - batch_tool?: boolean - /** - * Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag) - */ - openTelemetry?: boolean - /** - * Tools that should only be available to primary agents. - */ - primary_tools?: Array - } -} - -export type ToolIds = Array - -export type ToolListItem = { - id: string - description: string - parameters: unknown -} - -export type ToolList = Array - -export type Path = { - state: string - config: string - worktree: string - directory: string -} - -export type VcsInfo = { - branch: string -} - -export type TextPartInput = { - id?: string - type: "text" - text: string - synthetic?: boolean - ignored?: boolean - time?: { - start: number - end?: number - } - metadata?: { - [key: string]: unknown - } -} - -export type FilePartInput = { - id?: string - type: "file" - mime: string - filename?: string - url: string - source?: FilePartSource -} - -export type AgentPartInput = { - id?: string - type: "agent" - name: string - source?: { - value: string - start: number - end: number - } -} - -export type SubtaskPartInput = { - id?: string - type: "subtask" - prompt: string - description: string - agent: string -} - -export type Command = { - name: string - description?: string - agent?: string - model?: string - template: string - subtask?: boolean -} - -export type Model = { - id: string - providerID: string - api: { - id: string - url: string - npm: string - } - name: string - capabilities: { - temperature: boolean - reasoning: boolean - attachment: boolean - toolcall: boolean - input: { - text: boolean - audio: boolean - image: boolean - video: boolean - pdf: boolean - } - output: { - text: boolean - audio: boolean - image: boolean - video: boolean - pdf: boolean - } - } - cost: { - input: number - output: number - cache: { - read: number - write: number - } - experimentalOver200K?: { - input: number - output: number - cache: { - read: number - write: number - } - } - } - limit: { - context: number - output: number - } - status: "alpha" | "beta" | "deprecated" | "active" - options: { - [key: string]: unknown - } - headers: { - [key: string]: string - } -} - -export type Provider = { - id: string - name: string - source: "env" | "config" | "custom" | "api" - env: Array - key?: string - options: { - [key: string]: unknown - } - models: { - [key: string]: Model - } -} - -export type ProviderAuthMethod = { - type: "oauth" | "api" - label: string -} - -export type ProviderAuthAuthorization = { - url: string - method: "auto" | "code" - instructions: string -} - -export type Symbol = { - name: string - kind: number - location: { - uri: string - range: Range - } -} - -export type FileNode = { - name: string - path: string - absolute: string - type: "file" | "directory" - ignored: boolean -} - -export type FileContent = { - type: "text" | "binary" - content: string - diff?: string - patch?: { - oldFileName: string - newFileName: string - oldHeader?: string - newHeader?: string - hunks: Array<{ - oldStart: number - oldLines: number - newStart: number - newLines: number - lines: Array - }> - index?: string - } - encoding?: "base64" - mimeType?: string -} - -export type File = { - path: string - added: number - removed: number - status: "added" | "deleted" | "modified" -} - -export type Agent = { - name: string - description?: string - mode: "subagent" | "primary" | "all" - builtIn: boolean - topP?: number - temperature?: number - color?: string - permission: { - edit: "ask" | "allow" | "deny" - bash: { - [key: string]: "ask" | "allow" | "deny" - } - webfetch?: "ask" | "allow" | "deny" - doom_loop?: "ask" | "allow" | "deny" - external_directory?: "ask" | "allow" | "deny" - } - model?: { - modelID: string - providerID: string - } - prompt?: string - tools: { - [key: string]: boolean - } - options: { - [key: string]: unknown - } - maxSteps?: number -} - -export type McpStatusConnected = { - status: "connected" -} - -export type McpStatusDisabled = { - status: "disabled" -} - -export type McpStatusFailed = { - status: "failed" - error: string -} - -export type McpStatusNeedsAuth = { - status: "needs_auth" -} - -export type McpStatusNeedsClientRegistration = { - status: "needs_client_registration" - error: string -} - -export type McpStatus = - | McpStatusConnected - | McpStatusDisabled - | McpStatusFailed - | McpStatusNeedsAuth - | McpStatusNeedsClientRegistration - -export type LspStatus = { - id: string - name: string - root: string - status: "connected" | "error" -} - -export type FormatterStatus = { - name: string - extensions: Array - enabled: boolean -} - -export type OAuth = { - type: "oauth" - refresh: string - access: string - expires: number - enterpriseUrl?: string -} - -export type ApiAuth = { - type: "api" - key: string - metadata?: { - [key: string]: string - } -} - -export type WellKnownAuth = { - type: "wellknown" - key: string - token: string -} - -export type Auth = OAuth | ApiAuth | WellKnownAuth - -export type GlobalEventData = { - body?: never - path?: never - query?: never - url: "/global/event" -} - -export type GlobalEventResponses = { - /** - * Event stream - */ - 200: GlobalEvent -} - -export type GlobalEventResponse = GlobalEventResponses[keyof GlobalEventResponses] - -export type ProjectListData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/project" -} - -export type ProjectListResponses = { - /** - * List of projects - */ - 200: Array -} - -export type ProjectListResponse = ProjectListResponses[keyof ProjectListResponses] - -export type ProjectCurrentData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/project/current" -} - -export type ProjectCurrentResponses = { - /** - * Current project - */ - 200: Project -} - -export type ProjectCurrentResponse = ProjectCurrentResponses[keyof ProjectCurrentResponses] - -export type PtyListData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/pty" -} - -export type PtyListResponses = { - /** - * List of sessions - */ - 200: Array -} - -export type PtyListResponse = PtyListResponses[keyof PtyListResponses] - -export type PtyCreateData = { - body?: { - command?: string - args?: Array - cwd?: string - title?: string - env?: { - [key: string]: string - } - } - path?: never - query?: { - directory?: string - } - url: "/pty" -} - -export type PtyCreateErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type PtyCreateError = PtyCreateErrors[keyof PtyCreateErrors] - -export type PtyCreateResponses = { - /** - * Created session - */ - 200: Pty -} - -export type PtyCreateResponse = PtyCreateResponses[keyof PtyCreateResponses] - -export type PtyRemoveData = { - body?: never - path: { - id: string - } - query?: { - directory?: string - } - url: "/pty/{id}" -} - -export type PtyRemoveErrors = { - /** - * Not found - */ - 404: NotFoundError -} - -export type PtyRemoveError = PtyRemoveErrors[keyof PtyRemoveErrors] - -export type PtyRemoveResponses = { - /** - * Session removed - */ - 200: boolean -} - -export type PtyRemoveResponse = PtyRemoveResponses[keyof PtyRemoveResponses] - -export type PtyGetData = { - body?: never - path: { - id: string - } - query?: { - directory?: string - } - url: "/pty/{id}" -} - -export type PtyGetErrors = { - /** - * Not found - */ - 404: NotFoundError -} - -export type PtyGetError = PtyGetErrors[keyof PtyGetErrors] - -export type PtyGetResponses = { - /** - * Session info - */ - 200: Pty -} - -export type PtyGetResponse = PtyGetResponses[keyof PtyGetResponses] - -export type PtyUpdateData = { - body?: { - title?: string - size?: { - rows: number - cols: number - } - } - path: { - id: string - } - query?: { - directory?: string - } - url: "/pty/{id}" -} - -export type PtyUpdateErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type PtyUpdateError = PtyUpdateErrors[keyof PtyUpdateErrors] - -export type PtyUpdateResponses = { - /** - * Updated session - */ - 200: Pty -} - -export type PtyUpdateResponse = PtyUpdateResponses[keyof PtyUpdateResponses] - -export type PtyConnectData = { - body?: never - path: { - id: string - } - query?: { - directory?: string - } - url: "/pty/{id}/connect" -} - -export type PtyConnectErrors = { - /** - * Not found - */ - 404: NotFoundError -} - -export type PtyConnectError = PtyConnectErrors[keyof PtyConnectErrors] - -export type PtyConnectResponses = { - /** - * Connected session - */ - 200: boolean -} - -export type PtyConnectResponse = PtyConnectResponses[keyof PtyConnectResponses] - -export type ConfigGetData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/config" -} - -export type ConfigGetResponses = { - /** - * Get config info - */ - 200: Config -} - -export type ConfigGetResponse = ConfigGetResponses[keyof ConfigGetResponses] - -export type ConfigUpdateData = { - body?: Config - path?: never - query?: { - directory?: string - } - url: "/config" -} - -export type ConfigUpdateErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ConfigUpdateError = ConfigUpdateErrors[keyof ConfigUpdateErrors] - -export type ConfigUpdateResponses = { - /** - * Successfully updated config - */ - 200: Config -} - -export type ConfigUpdateResponse = ConfigUpdateResponses[keyof ConfigUpdateResponses] - -export type ToolIdsData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/experimental/tool/ids" -} - -export type ToolIdsErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ToolIdsError = ToolIdsErrors[keyof ToolIdsErrors] - -export type ToolIdsResponses = { - /** - * Tool IDs - */ - 200: ToolIds -} - -export type ToolIdsResponse = ToolIdsResponses[keyof ToolIdsResponses] - -export type ToolListData = { - body?: never - path?: never - query: { - directory?: string - provider: string - model: string - } - url: "/experimental/tool" -} - -export type ToolListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ToolListError = ToolListErrors[keyof ToolListErrors] - -export type ToolListResponses = { - /** - * Tools - */ - 200: ToolList -} - -export type ToolListResponse = ToolListResponses[keyof ToolListResponses] - -export type InstanceDisposeData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/instance/dispose" -} - -export type InstanceDisposeResponses = { - /** - * Instance disposed - */ - 200: boolean -} - -export type InstanceDisposeResponse = InstanceDisposeResponses[keyof InstanceDisposeResponses] - -export type PathGetData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/path" -} - -export type PathGetResponses = { - /** - * Path - */ - 200: Path -} - -export type PathGetResponse = PathGetResponses[keyof PathGetResponses] - -export type VcsGetData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/vcs" -} - -export type VcsGetResponses = { - /** - * VCS info - */ - 200: VcsInfo -} - -export type VcsGetResponse = VcsGetResponses[keyof VcsGetResponses] - -export type SessionListData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/session" -} - -export type SessionListResponses = { - /** - * List of sessions - */ - 200: Array -} - -export type SessionListResponse = SessionListResponses[keyof SessionListResponses] - -export type SessionCreateData = { - body?: { - parentID?: string - title?: string - } - path?: never - query?: { - directory?: string - } - url: "/session" -} - -export type SessionCreateErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type SessionCreateError = SessionCreateErrors[keyof SessionCreateErrors] - -export type SessionCreateResponses = { - /** - * Successfully created session - */ - 200: Session -} - -export type SessionCreateResponse = SessionCreateResponses[keyof SessionCreateResponses] - -export type SessionStatusData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/session/status" -} - -export type SessionStatusErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type SessionStatusError = SessionStatusErrors[keyof SessionStatusErrors] - -export type SessionStatusResponses = { - /** - * Get session status - */ - 200: { - [key: string]: SessionStatus - } -} - -export type SessionStatusResponse = SessionStatusResponses[keyof SessionStatusResponses] - -export type SessionDeleteData = { - body?: never - path: { - id: string - } - query?: { - directory?: string - } - url: "/session/{id}" -} - -export type SessionDeleteErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type SessionDeleteError = SessionDeleteErrors[keyof SessionDeleteErrors] - -export type SessionDeleteResponses = { - /** - * Successfully deleted session - */ - 200: boolean -} - -export type SessionDeleteResponse = SessionDeleteResponses[keyof SessionDeleteResponses] - -export type SessionGetData = { - body?: never - path: { - id: string - } - query?: { - directory?: string - } - url: "/session/{id}" -} - -export type SessionGetErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type SessionGetError = SessionGetErrors[keyof SessionGetErrors] - -export type SessionGetResponses = { - /** - * Get session - */ - 200: Session -} - -export type SessionGetResponse = SessionGetResponses[keyof SessionGetResponses] - -export type SessionUpdateData = { - body?: { - title?: string - } - path: { - id: string - } - query?: { - directory?: string - } - url: "/session/{id}" -} - -export type SessionUpdateErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type SessionUpdateError = SessionUpdateErrors[keyof SessionUpdateErrors] - -export type SessionUpdateResponses = { - /** - * Successfully updated session - */ - 200: Session -} - -export type SessionUpdateResponse = SessionUpdateResponses[keyof SessionUpdateResponses] - -export type SessionChildrenData = { - body?: never - path: { - id: string - } - query?: { - directory?: string - } - url: "/session/{id}/children" -} - -export type SessionChildrenErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type SessionChildrenError = SessionChildrenErrors[keyof SessionChildrenErrors] - -export type SessionChildrenResponses = { - /** - * List of children - */ - 200: Array -} - -export type SessionChildrenResponse = SessionChildrenResponses[keyof SessionChildrenResponses] - -export type SessionInitData = { - body?: { - modelID: string - providerID: string - messageID: string - } - path: { - /** - * Session ID - */ - id: string - } - query?: { - directory?: string - } - url: "/session/{id}/init" -} - -export type SessionInitErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type SessionInitError = SessionInitErrors[keyof SessionInitErrors] - -export type SessionInitResponses = { - /** - * 200 - */ - 200: boolean -} - -export type SessionInitResponse = SessionInitResponses[keyof SessionInitResponses] - -export type SessionForkData = { - body?: { - messageID?: string - } - path: { - id: string - } - query?: { - directory?: string - } - url: "/session/{id}/fork" -} - -export type SessionForkResponses = { - /** - * 200 - */ - 200: Session -} - -export type SessionForkResponse = SessionForkResponses[keyof SessionForkResponses] - -export type SessionAbortData = { - body?: never - path: { - id: string - } - query?: { - directory?: string - } - url: "/session/{id}/abort" -} - -export type SessionAbortErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type SessionAbortError = SessionAbortErrors[keyof SessionAbortErrors] - -export type SessionAbortResponses = { - /** - * Aborted session - */ - 200: boolean -} - -export type SessionAbortResponse = SessionAbortResponses[keyof SessionAbortResponses] - -export type SessionUnshareData = { - body?: never - path: { - id: string - } - query?: { - directory?: string - } - url: "/session/{id}/share" -} - -export type SessionUnshareErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type SessionUnshareError = SessionUnshareErrors[keyof SessionUnshareErrors] - -export type SessionUnshareResponses = { - /** - * Successfully unshared session - */ - 200: Session -} - -export type SessionUnshareResponse = SessionUnshareResponses[keyof SessionUnshareResponses] - -export type SessionShareData = { - body?: never - path: { - id: string - } - query?: { - directory?: string - } - url: "/session/{id}/share" -} - -export type SessionShareErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type SessionShareError = SessionShareErrors[keyof SessionShareErrors] - -export type SessionShareResponses = { - /** - * Successfully shared session - */ - 200: Session -} - -export type SessionShareResponse = SessionShareResponses[keyof SessionShareResponses] - -export type SessionDiffData = { - body?: never - path: { - /** - * Session ID - */ - id: string - } - query?: { - directory?: string - messageID?: string - } - url: "/session/{id}/diff" -} - -export type SessionDiffErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type SessionDiffError = SessionDiffErrors[keyof SessionDiffErrors] - -export type SessionDiffResponses = { - /** - * List of diffs - */ - 200: Array -} - -export type SessionDiffResponse = SessionDiffResponses[keyof SessionDiffResponses] - -export type SessionSummarizeData = { - body?: { - providerID: string - modelID: string - } - path: { - /** - * Session ID - */ - id: string - } - query?: { - directory?: string - } - url: "/session/{id}/summarize" -} - -export type SessionSummarizeErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type SessionSummarizeError = SessionSummarizeErrors[keyof SessionSummarizeErrors] - -export type SessionSummarizeResponses = { - /** - * Summarized session - */ - 200: boolean -} - -export type SessionSummarizeResponse = SessionSummarizeResponses[keyof SessionSummarizeResponses] - -export type SessionMessagesData = { - body?: never - path: { - /** - * Session ID - */ - id: string - } - query?: { - directory?: string - limit?: number - } - url: "/session/{id}/message" -} - -export type SessionMessagesErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type SessionMessagesError = SessionMessagesErrors[keyof SessionMessagesErrors] - -export type SessionMessagesResponses = { - /** - * List of messages - */ - 200: Array<{ - info: Message - parts: Array - }> -} - -export type SessionMessagesResponse = SessionMessagesResponses[keyof SessionMessagesResponses] - -export type SessionPromptData = { - body?: { - messageID?: string - model?: { - providerID: string - modelID: string - } - agent?: string - noReply?: boolean - system?: string - tools?: { - [key: string]: boolean - } - parts: Array - } - path: { - /** - * Session ID - */ - id: string - } - query?: { - directory?: string - } - url: "/session/{id}/message" -} - -export type SessionPromptErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type SessionPromptError = SessionPromptErrors[keyof SessionPromptErrors] - -export type SessionPromptResponses = { - /** - * Created message - */ - 200: { - info: AssistantMessage - parts: Array - } -} - -export type SessionPromptResponse = SessionPromptResponses[keyof SessionPromptResponses] - -export type SessionMessageData = { - body?: never - path: { - /** - * Session ID - */ - id: string - /** - * Message ID - */ - messageID: string - } - query?: { - directory?: string - } - url: "/session/{id}/message/{messageID}" -} - -export type SessionMessageErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type SessionMessageError = SessionMessageErrors[keyof SessionMessageErrors] - -export type SessionMessageResponses = { - /** - * Message - */ - 200: { - info: Message - parts: Array - } -} - -export type SessionMessageResponse = SessionMessageResponses[keyof SessionMessageResponses] - -export type SessionPromptAsyncData = { - body?: { - messageID?: string - model?: { - providerID: string - modelID: string - } - agent?: string - noReply?: boolean - system?: string - tools?: { - [key: string]: boolean - } - parts: Array - } - path: { - /** - * Session ID - */ - id: string - } - query?: { - directory?: string - } - url: "/session/{id}/prompt_async" -} - -export type SessionPromptAsyncErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type SessionPromptAsyncError = SessionPromptAsyncErrors[keyof SessionPromptAsyncErrors] - -export type SessionPromptAsyncResponses = { - /** - * Prompt accepted - */ - 204: void -} - -export type SessionPromptAsyncResponse = SessionPromptAsyncResponses[keyof SessionPromptAsyncResponses] - -export type SessionCommandData = { - body?: { - messageID?: string - agent?: string - model?: string - arguments: string - command: string - } - path: { - /** - * Session ID - */ - id: string - } - query?: { - directory?: string - } - url: "/session/{id}/command" -} - -export type SessionCommandErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type SessionCommandError = SessionCommandErrors[keyof SessionCommandErrors] - -export type SessionCommandResponses = { - /** - * Created message - */ - 200: { - info: AssistantMessage - parts: Array - } -} - -export type SessionCommandResponse = SessionCommandResponses[keyof SessionCommandResponses] - -export type SessionShellData = { - body?: { - agent: string - model?: { - providerID: string - modelID: string - } - command: string - } - path: { - /** - * Session ID - */ - id: string - } - query?: { - directory?: string - } - url: "/session/{id}/shell" -} - -export type SessionShellErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type SessionShellError = SessionShellErrors[keyof SessionShellErrors] - -export type SessionShellResponses = { - /** - * Created message - */ - 200: AssistantMessage -} - -export type SessionShellResponse = SessionShellResponses[keyof SessionShellResponses] - -export type SessionRevertData = { - body?: { - messageID: string - partID?: string - } - path: { - id: string - } - query?: { - directory?: string - } - url: "/session/{id}/revert" -} - -export type SessionRevertErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type SessionRevertError = SessionRevertErrors[keyof SessionRevertErrors] - -export type SessionRevertResponses = { - /** - * Updated session - */ - 200: Session -} - -export type SessionRevertResponse = SessionRevertResponses[keyof SessionRevertResponses] - -export type SessionUnrevertData = { - body?: never - path: { - id: string - } - query?: { - directory?: string - } - url: "/session/{id}/unrevert" -} - -export type SessionUnrevertErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type SessionUnrevertError = SessionUnrevertErrors[keyof SessionUnrevertErrors] - -export type SessionUnrevertResponses = { - /** - * Updated session - */ - 200: Session -} - -export type SessionUnrevertResponse = SessionUnrevertResponses[keyof SessionUnrevertResponses] - -export type PostSessionIdPermissionsPermissionIdData = { - body?: { - response: "once" | "always" | "reject" - } - path: { - id: string - permissionID: string - } - query?: { - directory?: string - } - url: "/session/{id}/permissions/{permissionID}" -} - -export type PostSessionIdPermissionsPermissionIdErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type PostSessionIdPermissionsPermissionIdError = - PostSessionIdPermissionsPermissionIdErrors[keyof PostSessionIdPermissionsPermissionIdErrors] - -export type PostSessionIdPermissionsPermissionIdResponses = { - /** - * Permission processed successfully - */ - 200: boolean -} - -export type PostSessionIdPermissionsPermissionIdResponse = - PostSessionIdPermissionsPermissionIdResponses[keyof PostSessionIdPermissionsPermissionIdResponses] - -export type CommandListData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/command" -} - -export type CommandListResponses = { - /** - * List of commands - */ - 200: Array -} - -export type CommandListResponse = CommandListResponses[keyof CommandListResponses] - -export type ConfigProvidersData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/config/providers" -} - -export type ConfigProvidersResponses = { - /** - * List of providers - */ - 200: { - providers: Array - default: { - [key: string]: string - } - } -} - -export type ConfigProvidersResponse = ConfigProvidersResponses[keyof ConfigProvidersResponses] - -export type ProviderListData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/provider" -} - -export type ProviderListResponses = { - /** - * List of providers - */ - 200: { - all: Array<{ - api?: string - name: string - env: Array - id: string - npm?: string - models: { - [key: string]: { - id: string - name: string - release_date: string - attachment: boolean - reasoning: boolean - temperature: boolean - tool_call: boolean - cost?: { - input: number - output: number - cache_read?: number - cache_write?: number - context_over_200k?: { - input: number - output: number - cache_read?: number - cache_write?: number - } - } - limit: { - context: number - output: number - } - modalities?: { - input: Array<"text" | "audio" | "image" | "video" | "pdf"> - output: Array<"text" | "audio" | "image" | "video" | "pdf"> - } - experimental?: boolean - status?: "alpha" | "beta" | "deprecated" | "active" - options: { - [key: string]: unknown - } - headers?: { - [key: string]: string - } - provider?: { - npm: string - } - } - } - }> - default: { - [key: string]: string - } - connected: Array - } -} - -export type ProviderListResponse = ProviderListResponses[keyof ProviderListResponses] - -export type ProviderAuthData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/provider/auth" -} - -export type ProviderAuthResponses = { - /** - * Provider auth methods - */ - 200: { - [key: string]: Array - } -} - -export type ProviderAuthResponse = ProviderAuthResponses[keyof ProviderAuthResponses] - -export type ProviderOauthAuthorizeData = { - body?: { - /** - * Auth method index - */ - method: number - } - path: { - /** - * Provider ID - */ - id: string - } - query?: { - directory?: string - } - url: "/provider/{id}/oauth/authorize" -} - -export type ProviderOauthAuthorizeErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ProviderOauthAuthorizeError = ProviderOauthAuthorizeErrors[keyof ProviderOauthAuthorizeErrors] - -export type ProviderOauthAuthorizeResponses = { - /** - * Authorization URL and method - */ - 200: ProviderAuthAuthorization -} - -export type ProviderOauthAuthorizeResponse = ProviderOauthAuthorizeResponses[keyof ProviderOauthAuthorizeResponses] - -export type ProviderOauthCallbackData = { - body?: { - /** - * Auth method index - */ - method: number - /** - * OAuth authorization code - */ - code?: string - } - path: { - /** - * Provider ID - */ - id: string - } - query?: { - directory?: string - } - url: "/provider/{id}/oauth/callback" -} - -export type ProviderOauthCallbackErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ProviderOauthCallbackError = ProviderOauthCallbackErrors[keyof ProviderOauthCallbackErrors] - -export type ProviderOauthCallbackResponses = { - /** - * OAuth callback processed successfully - */ - 200: boolean -} - -export type ProviderOauthCallbackResponse = ProviderOauthCallbackResponses[keyof ProviderOauthCallbackResponses] - -export type FindTextData = { - body?: never - path?: never - query: { - directory?: string - pattern: string - } - url: "/find" -} - -export type FindTextResponses = { - /** - * Matches - */ - 200: Array<{ - path: { - text: string - } - lines: { - text: string - } - line_number: number - absolute_offset: number - submatches: Array<{ - match: { - text: string - } - start: number - end: number - }> - }> -} - -export type FindTextResponse = FindTextResponses[keyof FindTextResponses] - -export type FindFilesData = { - body?: never - path?: never - query: { - directory?: string - query: string - dirs?: "true" | "false" - } - url: "/find/file" -} - -export type FindFilesResponses = { - /** - * File paths - */ - 200: Array -} - -export type FindFilesResponse = FindFilesResponses[keyof FindFilesResponses] - -export type FindSymbolsData = { - body?: never - path?: never - query: { - directory?: string - query: string - } - url: "/find/symbol" -} - -export type FindSymbolsResponses = { - /** - * Symbols - */ - 200: Array -} - -export type FindSymbolsResponse = FindSymbolsResponses[keyof FindSymbolsResponses] - -export type FileListData = { - body?: never - path?: never - query: { - directory?: string - path: string - } - url: "/file" -} - -export type FileListResponses = { - /** - * Files and directories - */ - 200: Array -} - -export type FileListResponse = FileListResponses[keyof FileListResponses] - -export type FileReadData = { - body?: never - path?: never - query: { - directory?: string - path: string - } - url: "/file/content" -} - -export type FileReadResponses = { - /** - * File content - */ - 200: FileContent -} - -export type FileReadResponse = FileReadResponses[keyof FileReadResponses] - -export type FileStatusData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/file/status" -} - -export type FileStatusResponses = { - /** - * File status - */ - 200: Array -} - -export type FileStatusResponse = FileStatusResponses[keyof FileStatusResponses] - -export type AppLogData = { - body?: { - /** - * Service name for the log entry - */ - service: string - /** - * Log level - */ - level: "debug" | "info" | "error" | "warn" - /** - * Log message - */ - message: string - /** - * Additional metadata for the log entry - */ - extra?: { - [key: string]: unknown - } - } - path?: never - query?: { - directory?: string - } - url: "/log" -} - -export type AppLogErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type AppLogError = AppLogErrors[keyof AppLogErrors] - -export type AppLogResponses = { - /** - * Log entry written successfully - */ - 200: boolean -} - -export type AppLogResponse = AppLogResponses[keyof AppLogResponses] - -export type AppAgentsData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/agent" -} - -export type AppAgentsResponses = { - /** - * List of agents - */ - 200: Array -} - -export type AppAgentsResponse = AppAgentsResponses[keyof AppAgentsResponses] - -export type McpStatusData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/mcp" -} - -export type McpStatusResponses = { - /** - * MCP server status - */ - 200: { - [key: string]: McpStatus - } -} - -export type McpStatusResponse = McpStatusResponses[keyof McpStatusResponses] - -export type McpAddData = { - body?: { - name: string - config: McpLocalConfig | McpRemoteConfig - } - path?: never - query?: { - directory?: string - } - url: "/mcp" -} - -export type McpAddErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type McpAddError = McpAddErrors[keyof McpAddErrors] - -export type McpAddResponses = { - /** - * MCP server added successfully - */ - 200: { - [key: string]: McpStatus - } -} - -export type McpAddResponse = McpAddResponses[keyof McpAddResponses] - -export type McpAuthRemoveData = { - body?: never - path: { - name: string - } - query?: { - directory?: string - } - url: "/mcp/{name}/auth" -} - -export type McpAuthRemoveErrors = { - /** - * Not found - */ - 404: NotFoundError -} - -export type McpAuthRemoveError = McpAuthRemoveErrors[keyof McpAuthRemoveErrors] - -export type McpAuthRemoveResponses = { - /** - * OAuth credentials removed - */ - 200: { - success: true - } -} - -export type McpAuthRemoveResponse = McpAuthRemoveResponses[keyof McpAuthRemoveResponses] - -export type McpAuthStartData = { - body?: never - path: { - name: string - } - query?: { - directory?: string - } - url: "/mcp/{name}/auth" -} - -export type McpAuthStartErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type McpAuthStartError = McpAuthStartErrors[keyof McpAuthStartErrors] - -export type McpAuthStartResponses = { - /** - * OAuth flow started - */ - 200: { - /** - * URL to open in browser for authorization - */ - authorizationUrl: string - } -} - -export type McpAuthStartResponse = McpAuthStartResponses[keyof McpAuthStartResponses] - -export type McpAuthCallbackData = { - body?: { - /** - * Authorization code from OAuth callback - */ - code: string - } - path: { - name: string - } - query?: { - directory?: string - } - url: "/mcp/{name}/auth/callback" -} - -export type McpAuthCallbackErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type McpAuthCallbackError = McpAuthCallbackErrors[keyof McpAuthCallbackErrors] - -export type McpAuthCallbackResponses = { - /** - * OAuth authentication completed - */ - 200: McpStatus -} - -export type McpAuthCallbackResponse = McpAuthCallbackResponses[keyof McpAuthCallbackResponses] - -export type McpAuthAuthenticateData = { - body?: never - path: { - name: string - } - query?: { - directory?: string - } - url: "/mcp/{name}/auth/authenticate" -} - -export type McpAuthAuthenticateErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type McpAuthAuthenticateError = McpAuthAuthenticateErrors[keyof McpAuthAuthenticateErrors] - -export type McpAuthAuthenticateResponses = { - /** - * OAuth authentication completed - */ - 200: McpStatus -} - -export type McpAuthAuthenticateResponse = McpAuthAuthenticateResponses[keyof McpAuthAuthenticateResponses] - -export type McpConnectData = { - body?: never - path: { - name: string - } - query?: { - directory?: string - } - url: "/mcp/{name}/connect" -} - -export type McpConnectResponses = { - /** - * MCP server connected successfully - */ - 200: boolean -} - -export type McpConnectResponse = McpConnectResponses[keyof McpConnectResponses] - -export type McpDisconnectData = { - body?: never - path: { - name: string - } - query?: { - directory?: string - } - url: "/mcp/{name}/disconnect" -} - -export type McpDisconnectResponses = { - /** - * MCP server disconnected successfully - */ - 200: boolean -} - -export type McpDisconnectResponse = McpDisconnectResponses[keyof McpDisconnectResponses] - -export type LspStatusData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/lsp" -} - -export type LspStatusResponses = { - /** - * LSP server status - */ - 200: Array -} - -export type LspStatusResponse = LspStatusResponses[keyof LspStatusResponses] - -export type FormatterStatusData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/formatter" -} - -export type FormatterStatusResponses = { - /** - * Formatter status - */ - 200: Array -} - -export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses] - -export type TuiAppendPromptData = { - body?: { - text: string - } - path?: never - query?: { - directory?: string - } - url: "/tui/append-prompt" -} - -export type TuiAppendPromptErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiAppendPromptError = TuiAppendPromptErrors[keyof TuiAppendPromptErrors] - -export type TuiAppendPromptResponses = { - /** - * Prompt processed successfully - */ - 200: boolean -} - -export type TuiAppendPromptResponse = TuiAppendPromptResponses[keyof TuiAppendPromptResponses] - -export type TuiOpenHelpData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/tui/open-help" -} - -export type TuiOpenHelpResponses = { - /** - * Help dialog opened successfully - */ - 200: boolean -} - -export type TuiOpenHelpResponse = TuiOpenHelpResponses[keyof TuiOpenHelpResponses] - -export type TuiOpenSessionsData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/tui/open-sessions" -} - -export type TuiOpenSessionsResponses = { - /** - * Session dialog opened successfully - */ - 200: boolean -} - -export type TuiOpenSessionsResponse = TuiOpenSessionsResponses[keyof TuiOpenSessionsResponses] - -export type TuiOpenThemesData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/tui/open-themes" -} - -export type TuiOpenThemesResponses = { - /** - * Theme dialog opened successfully - */ - 200: boolean -} - -export type TuiOpenThemesResponse = TuiOpenThemesResponses[keyof TuiOpenThemesResponses] - -export type TuiOpenModelsData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/tui/open-models" -} - -export type TuiOpenModelsResponses = { - /** - * Model dialog opened successfully - */ - 200: boolean -} - -export type TuiOpenModelsResponse = TuiOpenModelsResponses[keyof TuiOpenModelsResponses] - -export type TuiSubmitPromptData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/tui/submit-prompt" -} - -export type TuiSubmitPromptResponses = { - /** - * Prompt submitted successfully - */ - 200: boolean -} - -export type TuiSubmitPromptResponse = TuiSubmitPromptResponses[keyof TuiSubmitPromptResponses] - -export type TuiClearPromptData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/tui/clear-prompt" -} - -export type TuiClearPromptResponses = { - /** - * Prompt cleared successfully - */ - 200: boolean -} - -export type TuiClearPromptResponse = TuiClearPromptResponses[keyof TuiClearPromptResponses] - -export type TuiExecuteCommandData = { - body?: { - command: string - } - path?: never - query?: { - directory?: string - } - url: "/tui/execute-command" -} - -export type TuiExecuteCommandErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiExecuteCommandError = TuiExecuteCommandErrors[keyof TuiExecuteCommandErrors] - -export type TuiExecuteCommandResponses = { - /** - * Command executed successfully - */ - 200: boolean -} - -export type TuiExecuteCommandResponse = TuiExecuteCommandResponses[keyof TuiExecuteCommandResponses] - -export type TuiShowToastData = { - body?: { - title?: string - message: string - variant: "info" | "success" | "warning" | "error" - /** - * Duration in milliseconds - */ - duration?: number - } - path?: never - query?: { - directory?: string - } - url: "/tui/show-toast" -} - -export type TuiShowToastResponses = { - /** - * Toast notification shown successfully - */ - 200: boolean -} - -export type TuiShowToastResponse = TuiShowToastResponses[keyof TuiShowToastResponses] - -export type TuiPublishData = { - body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow - path?: never - query?: { - directory?: string - } - url: "/tui/publish" -} - -export type TuiPublishErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiPublishError = TuiPublishErrors[keyof TuiPublishErrors] - -export type TuiPublishResponses = { - /** - * Event published successfully - */ - 200: boolean -} - -export type TuiPublishResponse = TuiPublishResponses[keyof TuiPublishResponses] - -export type TuiControlNextData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/tui/control/next" -} - -export type TuiControlNextResponses = { - /** - * Next TUI request - */ - 200: { - path: string - body: unknown - } -} - -export type TuiControlNextResponse = TuiControlNextResponses[keyof TuiControlNextResponses] - -export type TuiControlResponseData = { - body?: unknown - path?: never - query?: { - directory?: string - } - url: "/tui/control/response" -} - -export type TuiControlResponseResponses = { - /** - * Response submitted successfully - */ - 200: boolean -} - -export type TuiControlResponseResponse = TuiControlResponseResponses[keyof TuiControlResponseResponses] - -export type AuthSetData = { - body?: Auth - path: { - id: string - } - query?: { - directory?: string - } - url: "/auth/{id}" -} - -export type AuthSetErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type AuthSetError = AuthSetErrors[keyof AuthSetErrors] - -export type AuthSetResponses = { - /** - * Successfully set authentication credentials - */ - 200: boolean -} - -export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses] - -export type EventSubscribeData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/event" -} - -export type EventSubscribeResponses = { - /** - * Event stream - */ - 200: Event -} - -export type EventSubscribeResponse = EventSubscribeResponses[keyof EventSubscribeResponses] - -export type ClientOptions = { - baseUrl: `${string}://${string}` | (string & {}) -} diff --git a/packages/sdk/js/src/index.ts b/packages/sdk/js/src/index.ts deleted file mode 100644 index d044f5ad66e4..000000000000 --- a/packages/sdk/js/src/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -export * from "./client.js" -export * from "./server.js" - -import { createOpencodeClient } from "./client.js" -import { createOpencodeServer } from "./server.js" -import type { ServerOptions } from "./server.js" - -export async function createOpencode(options?: ServerOptions) { - const server = await createOpencodeServer({ - ...options, - }) - - const client = createOpencodeClient({ - baseUrl: server.url, - }) - - return { - client, - server, - } -} diff --git a/packages/sdk/js/src/process.ts b/packages/sdk/js/src/process.ts deleted file mode 100644 index 3111b424aa18..000000000000 --- a/packages/sdk/js/src/process.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { type ChildProcess, spawnSync } from "node:child_process" - -// Duplicated from `packages/opencode/src/util/process.ts` because the SDK cannot -// import `opencode` without creating a cycle (`opencode` depends on `@opencode-ai/sdk`). -export function stop(proc: ChildProcess) { - if (proc.exitCode !== null || proc.signalCode !== null) return - if (process.platform === "win32" && proc.pid) { - const out = spawnSync("taskkill", ["/pid", String(proc.pid), "/T", "/F"], { windowsHide: true }) - if (!out.error && out.status === 0) return - } - proc.kill() -} - -export function bindAbort(proc: ChildProcess, signal?: AbortSignal, onAbort?: () => void) { - if (!signal) return () => {} - const abort = () => { - clear() - stop(proc) - onAbort?.() - } - const clear = () => { - signal.removeEventListener("abort", abort) - proc.off("exit", clear) - proc.off("error", clear) - } - signal.addEventListener("abort", abort, { once: true }) - proc.on("exit", clear) - proc.on("error", clear) - if (signal.aborted) abort() - return clear -} diff --git a/packages/sdk/js/src/server.ts b/packages/sdk/js/src/server.ts deleted file mode 100644 index 2d1ab29fc928..000000000000 --- a/packages/sdk/js/src/server.ts +++ /dev/null @@ -1,134 +0,0 @@ -import launch from "cross-spawn" -import { type Config } from "./gen/types.gen.js" -import { stop, bindAbort } from "./process.js" - -export type ServerOptions = { - hostname?: string - port?: number - signal?: AbortSignal - timeout?: number - config?: Config -} - -export type TuiOptions = { - project?: string - model?: string - session?: string - agent?: string - signal?: AbortSignal - config?: Config -} - -export async function createOpencodeServer(options?: ServerOptions) { - options = Object.assign( - { - hostname: "127.0.0.1", - port: 4096, - timeout: 5000, - }, - options ?? {}, - ) - - const args = [`serve`, `--hostname=${options.hostname}`, `--port=${options.port}`] - if (options.config?.logLevel) args.push(`--log-level=${options.config.logLevel}`) - - const proc = launch(`opencode`, args, { - env: { - ...process.env, - OPENCODE_CONFIG_CONTENT: JSON.stringify(options.config ?? {}), - }, - }) - let clear = () => {} - - const url = await new Promise((resolve, reject) => { - const id = setTimeout(() => { - clear() - stop(proc) - reject(new Error(`Timeout waiting for server to start after ${options.timeout}ms`)) - }, options.timeout) - let output = "" - let resolved = false - proc.stdout?.on("data", (chunk) => { - if (resolved) return - output += chunk.toString() - const lines = output.split("\n") - for (const line of lines) { - if (line.startsWith("opencode server listening")) { - const match = line.match(/on\s+(https?:\/\/[^\s]+)/) - if (!match) { - clear() - stop(proc) - clearTimeout(id) - reject(new Error(`Failed to parse server url from output: ${line}`)) - return - } - clearTimeout(id) - resolved = true - resolve(match[1]!) - return - } - } - }) - proc.stderr?.on("data", (chunk) => { - output += chunk.toString() - }) - proc.on("exit", (code) => { - clearTimeout(id) - let msg = `Server exited with code ${code}` - if (output.trim()) { - msg += `\nServer output: ${output}` - } - reject(new Error(msg)) - }) - proc.on("error", (error) => { - clearTimeout(id) - reject(error) - }) - clear = bindAbort(proc, options.signal, () => { - clearTimeout(id) - reject(options.signal?.reason) - }) - }) - - return { - url, - close() { - clear() - stop(proc) - }, - } -} - -export function createOpencodeTui(options?: TuiOptions) { - const args = [] - - if (options?.project) { - args.push(`--project=${options.project}`) - } - if (options?.model) { - args.push(`--model=${options.model}`) - } - if (options?.session) { - args.push(`--session=${options.session}`) - } - if (options?.agent) { - args.push(`--agent=${options.agent}`) - } - - const proc = launch(`opencode`, args, { - stdio: "inherit", - env: { - ...process.env, - OPENCODE_CONFIG_CONTENT: JSON.stringify(options?.config ?? {}), - }, - }) - - const clear = bindAbort(proc, options?.signal) - - return { - close() { - clear() - stop(proc) - }, - } -} diff --git a/packages/sdk/js/src/v2/client.ts b/packages/sdk/js/src/v2/client.ts deleted file mode 100644 index bd8f984d7fb9..000000000000 --- a/packages/sdk/js/src/v2/client.ts +++ /dev/null @@ -1,97 +0,0 @@ -export * from "./gen/types.gen.js" -export type { FileSystemEntry as LocationFileSystemEntry } from "./gen/types.gen.js" -import type { UserMessage } from "./gen/types.gen.js" - -/** @deprecated V1 snapshot compatibility. Use FileDiffInfo for current API responses. */ -export type SnapshotFileDiff = NonNullable["diffs"]>[number] - -import { createClient } from "./gen/client/client.gen.js" -import { type Config } from "./gen/client/types.gen.js" -import { OpencodeClient } from "./gen/sdk.gen.js" -import { wrapClientError } from "../error-interceptor.js" -export { type Config as OpencodeClientConfig, OpencodeClient } - -function pick(value: string | null, fallback?: string, encode?: (value: string) => string) { - if (!value) return - if (!fallback) return value - if (value === fallback) return fallback - if (encode && value === encode(fallback)) return fallback - return value -} - -function rewrite(request: Request, values: { directory?: string; workspace?: string }) { - if (request.method !== "GET" && request.method !== "HEAD") return request - - const url = new URL(request.url) - let changed = false - - for (const [name, key] of [ - ["x-opencode-directory", "directory"], - ["x-opencode-workspace", "workspace"], - ] as const) { - const value = pick( - request.headers.get(name), - key === "directory" ? values.directory : values.workspace, - key === "directory" ? encodeURIComponent : undefined, - ) - if (!value) continue - for (const query of url.pathname.startsWith("/api/") ? [key, `location[${key}]`] : [key]) { - if (!url.searchParams.has(query)) { - url.searchParams.set(query, value) - } - } - changed = true - } - - if (!changed) return request - - const next = new Request(url, request) - next.headers.delete("x-opencode-directory") - next.headers.delete("x-opencode-workspace") - return next -} - -export function createOpencodeClient(config?: Config & { directory?: string; experimental_workspaceID?: string }) { - if (!config?.fetch) { - const customFetch: any = (req: any) => { - // @ts-ignore - req.timeout = false - return fetch(req) - } - config = { - ...config, - fetch: customFetch, - } - } - - if (config?.directory) { - config.headers = { - ...config.headers, - "x-opencode-directory": encodeURIComponent(config.directory), - } - } - - if (config?.experimental_workspaceID) { - config.headers = { - ...config.headers, - "x-opencode-workspace": config.experimental_workspaceID, - } - } - - const client = createClient(config) - client.interceptors.request.use((request) => - rewrite(request, { - directory: config?.directory, - workspace: config?.experimental_workspaceID, - }), - ) - client.interceptors.response.use((response) => { - const contentType = response.headers.get("content-type") - if (contentType === "text/html") - throw new Error("Request is not supported by this version of OpenCode Server (Server responded with text/html)") - - return response - }) - client.interceptors.error.use(wrapClientError) - return new OpencodeClient({ client }) -} diff --git a/packages/sdk/js/src/v2/data.ts b/packages/sdk/js/src/v2/data.ts deleted file mode 100644 index 776b168ad9d3..000000000000 --- a/packages/sdk/js/src/v2/data.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Part, UserMessage } from "./client.js" - -export const message = { - user(input: Omit & { parts: Omit[] }): { - info: UserMessage - parts: Part[] - } { - const { parts: _parts, ...rest } = input - - const info: UserMessage = { - ...rest, - id: "asdasd", - time: { - created: Date.now(), - }, - role: "user", - } - - return { - info, - parts: input.parts.map( - (part) => - ({ - ...part, - id: "asdasd", - messageID: info.id, - sessionID: info.sessionID, - }) as Part, - ), - } - }, -} diff --git a/packages/sdk/js/src/v2/gen/client.gen.ts b/packages/sdk/js/src/v2/gen/client.gen.ts deleted file mode 100644 index 0c110eca39ba..000000000000 --- a/packages/sdk/js/src/v2/gen/client.gen.ts +++ /dev/null @@ -1,18 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { type ClientOptions, type Config, createClient, createConfig } from "./client/index.js" -import type { ClientOptions as ClientOptions2 } from "./types.gen.js" - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = ( - override?: Config, -) => Config & T> - -export const client = createClient(createConfig({ baseUrl: "http://localhost:4096" })) diff --git a/packages/sdk/js/src/v2/gen/client/client.gen.ts b/packages/sdk/js/src/v2/gen/client/client.gen.ts deleted file mode 100644 index 627e98ec4206..000000000000 --- a/packages/sdk/js/src/v2/gen/client/client.gen.ts +++ /dev/null @@ -1,285 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { createSseClient } from "../core/serverSentEvents.gen.js" -import type { HttpMethod } from "../core/types.gen.js" -import { getValidRequestBody } from "../core/utils.gen.js" -import type { Client, Config, RequestOptions, ResolvedRequestOptions } from "./types.gen.js" -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from "./utils.gen.js" - -type ReqInit = Omit & { - body?: any - headers: ReturnType -} - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config) - - const getConfig = (): Config => ({ ..._config }) - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config) - return getConfig() - } - - const interceptors = createInterceptors() - - const beforeRequest = async (options: RequestOptions) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - serializedBody: undefined, - } - - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }) - } - - if (opts.requestValidator) { - await opts.requestValidator(opts) - } - - if (opts.body !== undefined && opts.bodySerializer) { - opts.serializedBody = opts.bodySerializer(opts.body) - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.serializedBody === "") { - opts.headers.delete("Content-Type") - } - - const url = buildUrl(opts) - - return { opts, url } - } - - const request: Client["request"] = async (options) => { - // @ts-expect-error - const { opts, url } = await beforeRequest(options) - const requestInit: ReqInit = { - redirect: "follow", - ...opts, - body: getValidRequestBody(opts), - } - - let request = new Request(url, requestInit) - - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts) - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch! - let response: Response - - try { - response = await _fetch(request) - } catch (error) { - // Handle fetch exceptions (AbortError, network errors, etc.) - let finalError = error - - for (const fn of interceptors.error.fns) { - if (fn) { - finalError = (await fn(error, undefined as any, request, opts)) as unknown - } - } - - finalError = finalError || ({} as unknown) - - if (opts.throwOnError) { - throw finalError - } - - // Return error response - return opts.responseStyle === "data" - ? undefined - : { - error: finalError, - request, - response: undefined as any, - } - } - - for (const fn of interceptors.response.fns) { - if (fn) { - response = await fn(response, request, opts) - } - } - - const result = { - request, - response, - } - - if (response.ok) { - const parseAs = - (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json" - - if (response.status === 204 || response.headers.get("Content-Length") === "0") { - let emptyData: any - switch (parseAs) { - case "arrayBuffer": - case "blob": - case "text": - emptyData = await response[parseAs]() - break - case "formData": - emptyData = new FormData() - break - case "stream": - emptyData = response.body - break - case "json": - default: - emptyData = {} - break - } - return opts.responseStyle === "data" - ? emptyData - : { - data: emptyData, - ...result, - } - } - - let data: any - switch (parseAs) { - case "arrayBuffer": - case "blob": - case "formData": - case "text": - data = await response[parseAs]() - break - case "json": { - // Some servers return 200 with no Content-Length and empty body. - // response.json() would throw; read as text and parse if non-empty. - const text = await response.text() - data = text ? JSON.parse(text) : {} - break - } - case "stream": - return opts.responseStyle === "data" - ? response.body - : { - data: response.body, - ...result, - } - } - - if (parseAs === "json") { - if (opts.responseValidator) { - await opts.responseValidator(data) - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data) - } - } - - return opts.responseStyle === "data" - ? data - : { - data, - ...result, - } - } - - const textError = await response.text() - let jsonError: unknown - - try { - jsonError = JSON.parse(textError) - } catch { - // noop - } - - const error = jsonError ?? textError - let finalError = error - - for (const fn of interceptors.error.fns) { - if (fn) { - finalError = (await fn(error, response, request, opts)) as string - } - } - - finalError = finalError || ({} as string) - - if (opts.throwOnError) { - throw finalError - } - - // TODO: we probably want to return error and improve types - return opts.responseStyle === "data" - ? undefined - : { - error: finalError, - ...result, - } - } - - const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => request({ ...options, method }) - - const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { - const { opts, url } = await beforeRequest(options) - return createSseClient({ - ...opts, - body: opts.body as BodyInit | null | undefined, - headers: opts.headers as unknown as Record, - method, - onRequest: async (url, init) => { - let request = new Request(url, init) - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts) - } - } - return request - }, - serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, - url, - }) - } - - return { - buildUrl, - connect: makeMethodFn("CONNECT"), - delete: makeMethodFn("DELETE"), - get: makeMethodFn("GET"), - getConfig, - head: makeMethodFn("HEAD"), - interceptors, - options: makeMethodFn("OPTIONS"), - patch: makeMethodFn("PATCH"), - post: makeMethodFn("POST"), - put: makeMethodFn("PUT"), - request, - setConfig, - sse: { - connect: makeSseFn("CONNECT"), - delete: makeSseFn("DELETE"), - get: makeSseFn("GET"), - head: makeSseFn("HEAD"), - options: makeSseFn("OPTIONS"), - patch: makeSseFn("PATCH"), - post: makeSseFn("POST"), - put: makeSseFn("PUT"), - trace: makeSseFn("TRACE"), - }, - trace: makeMethodFn("TRACE"), - } as Client -} diff --git a/packages/sdk/js/src/v2/gen/client/index.ts b/packages/sdk/js/src/v2/gen/client/index.ts deleted file mode 100644 index 0af63f3300eb..000000000000 --- a/packages/sdk/js/src/v2/gen/client/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type { Auth } from "../core/auth.gen.js" -export type { QuerySerializerOptions } from "../core/bodySerializer.gen.js" -export { - formDataBodySerializer, - jsonBodySerializer, - urlSearchParamsBodySerializer, -} from "../core/bodySerializer.gen.js" -export { buildClientParams } from "../core/params.gen.js" -export { serializeQueryKeyValue } from "../core/queryKeySerializer.gen.js" -export { createClient } from "./client.gen.js" -export type { - Client, - ClientOptions, - Config, - CreateClientConfig, - Options, - RequestOptions, - RequestResult, - ResolvedRequestOptions, - ResponseStyle, - TDataShape, -} from "./types.gen.js" -export { createConfig, mergeHeaders } from "./utils.gen.js" diff --git a/packages/sdk/js/src/v2/gen/client/types.gen.ts b/packages/sdk/js/src/v2/gen/client/types.gen.ts deleted file mode 100644 index 99d7e7f8f2e8..000000000000 --- a/packages/sdk/js/src/v2/gen/client/types.gen.ts +++ /dev/null @@ -1,202 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth } from "../core/auth.gen.js" -import type { ServerSentEventsOptions, ServerSentEventsResult } from "../core/serverSentEvents.gen.js" -import type { Client as CoreClient, Config as CoreConfig } from "../core/types.gen.js" -import type { Middleware } from "./utils.gen.js" - -export type ResponseStyle = "data" | "fields" - -export interface Config - extends Omit, - CoreConfig { - /** - * Base URL for all requests made by this client. - */ - baseUrl?: T["baseUrl"] - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch - /** - * Please don't use the Fetch client for Next.js applications. The `next` - * options won't have any effect. - * - * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. - */ - next?: never - /** - * Return the response data parsed in a specified format. By default, `auto` - * will infer the appropriate method from the `Content-Type` response header. - * You can override this behavior with any of the {@link Body} methods. - * Select `stream` if you don't want to parse response data at all. - * - * @default 'auto' - */ - parseAs?: "arrayBuffer" | "auto" | "blob" | "formData" | "json" | "stream" | "text" - /** - * Should we return only data or multiple fields (data, error, response, etc.)? - * - * @default 'fields' - */ - responseStyle?: ResponseStyle - /** - * Throw an error instead of returning it in the response? - * - * @default false - */ - throwOnError?: T["throwOnError"] -} - -export interface RequestOptions< - TData = unknown, - TResponseStyle extends ResponseStyle = "fields", - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends Config<{ - responseStyle: TResponseStyle - throwOnError: ThrowOnError - }>, - Pick< - ServerSentEventsOptions, - "onSseError" | "onSseEvent" | "sseDefaultRetryDelay" | "sseMaxRetryAttempts" | "sseMaxRetryDelay" - > { - /** - * Any body that you want to add to your request. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} - */ - body?: unknown - path?: Record - query?: Record - /** - * Security mechanism(s) to use for the request. - */ - security?: ReadonlyArray - url: Url -} - -export interface ResolvedRequestOptions< - TResponseStyle extends ResponseStyle = "fields", - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends RequestOptions { - serializedBody?: string -} - -export type RequestResult< - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = "fields", -> = ThrowOnError extends true - ? Promise< - TResponseStyle extends "data" - ? TData extends Record - ? TData[keyof TData] - : TData - : { - data: TData extends Record ? TData[keyof TData] : TData - request: Request - response: Response - } - > - : Promise< - TResponseStyle extends "data" - ? (TData extends Record ? TData[keyof TData] : TData) | undefined - : ( - | { - data: TData extends Record ? TData[keyof TData] : TData - error: undefined - } - | { - data: undefined - error: TError extends Record ? TError[keyof TError] : TError - } - ) & { - request: Request - response: Response - } - > - -export interface ClientOptions { - baseUrl?: string - responseStyle?: ResponseStyle - throwOnError?: boolean -} - -type MethodFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = "fields", ->( - options: Omit, "method">, -) => RequestResult - -type SseFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = "fields", ->( - options: Omit, "method">, -) => Promise> - -type RequestFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = "fields", ->( - options: Omit, "method"> & - Pick>, "method">, -) => RequestResult - -type BuildUrlFn = < - TData extends { - body?: unknown - path?: Record - query?: Record - url: string - }, ->( - options: TData & Options, -) => string - -export type Client = CoreClient & { - interceptors: Middleware -} - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = ( - override?: Config, -) => Config & T> - -export interface TDataShape { - body?: unknown - headers?: unknown - path?: unknown - query?: unknown - url: string -} - -type OmitKeys = Pick> - -export type Options< - TData extends TDataShape = TDataShape, - ThrowOnError extends boolean = boolean, - TResponse = unknown, - TResponseStyle extends ResponseStyle = "fields", -> = OmitKeys, "body" | "path" | "query" | "url"> & - ([TData] extends [never] ? unknown : Omit) diff --git a/packages/sdk/js/src/v2/gen/client/utils.gen.ts b/packages/sdk/js/src/v2/gen/client/utils.gen.ts deleted file mode 100644 index 49c07010baa0..000000000000 --- a/packages/sdk/js/src/v2/gen/client/utils.gen.ts +++ /dev/null @@ -1,294 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { getAuthToken } from "../core/auth.gen.js" -import type { QuerySerializerOptions } from "../core/bodySerializer.gen.js" -import { jsonBodySerializer } from "../core/bodySerializer.gen.js" -import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.gen.js" -import { getUrl } from "../core/utils.gen.js" -import type { Client, ClientOptions, Config, RequestOptions } from "./types.gen.js" - -export const createQuerySerializer = ({ parameters = {}, ...args }: QuerySerializerOptions = {}) => { - const querySerializer = (queryParams: T) => { - const search: string[] = [] - if (queryParams && typeof queryParams === "object") { - for (const name in queryParams) { - const value = queryParams[name] - - if (value === undefined) { - continue - } - - if (value === null) { - search.push(`${name}=null`) - continue - } - - const options = parameters[name] || args - - if (Array.isArray(value)) { - const serializedArray = serializeArrayParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: "form", - value, - ...options.array, - }) - if (serializedArray) search.push(serializedArray) - } else if (typeof value === "object") { - const serializedObject = serializeObjectParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: "deepObject", - value: value as Record, - ...options.object, - }) - if (serializedObject) search.push(serializedObject) - } else { - const serializedPrimitive = serializePrimitiveParam({ - allowReserved: options.allowReserved, - name, - value: value as string, - }) - if (serializedPrimitive) search.push(serializedPrimitive) - } - } - } - return search.join("&") - } - return querySerializer -} - -/** - * Infers parseAs value from provided Content-Type header. - */ -export const getParseAs = (contentType: string | null): Exclude => { - if (!contentType) { - // If no Content-Type header is provided, the best we can do is return the raw response body, - // which is effectively the same as the 'stream' option. - return "stream" - } - - const cleanContent = contentType.split(";")[0]?.trim() - - if (!cleanContent) { - return - } - - if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) { - return "json" - } - - if (cleanContent === "multipart/form-data") { - return "formData" - } - - if (["application/", "audio/", "image/", "video/"].some((type) => cleanContent.startsWith(type))) { - return "blob" - } - - if (cleanContent.startsWith("text/")) { - return "text" - } - - return -} - -const checkForExistence = ( - options: Pick & { - headers: Headers - }, - name?: string, -): boolean => { - if (!name) { - return false - } - if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) { - return true - } - return false -} - -export const setAuthParams = async ({ - security, - ...options -}: Pick, "security"> & - Pick & { - headers: Headers - }) => { - for (const auth of security) { - if (checkForExistence(options, auth.name)) { - continue - } - - const token = await getAuthToken(auth, options.auth) - - if (!token) { - continue - } - - const name = auth.name ?? "Authorization" - - switch (auth.in) { - case "query": - if (!options.query) { - options.query = {} - } - options.query[name] = token - break - case "cookie": - options.headers.append("Cookie", `${name}=${token}`) - break - case "header": - default: - options.headers.set(name, token) - break - } - } -} - -export const buildUrl: Client["buildUrl"] = (options) => - getUrl({ - baseUrl: options.baseUrl as string, - path: options.path, - query: options.query, - querySerializer: - typeof options.querySerializer === "function" - ? options.querySerializer - : createQuerySerializer(options.querySerializer), - url: options.url, - }) - -export const mergeConfigs = (a: Config, b: Config): Config => { - const config = { ...a, ...b } - if (config.baseUrl?.endsWith("/")) { - config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1) - } - config.headers = mergeHeaders(a.headers, b.headers) - return config -} - -const headersEntries = (headers: Headers): Array<[string, string]> => { - const entries: Array<[string, string]> = [] - headers.forEach((value, key) => { - entries.push([key, value]) - }) - return entries -} - -export const mergeHeaders = (...headers: Array["headers"] | undefined>): Headers => { - const mergedHeaders = new Headers() - for (const header of headers) { - if (!header) { - continue - } - - const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header) - - for (const [key, value] of iterator) { - if (value === null) { - mergedHeaders.delete(key) - } else if (Array.isArray(value)) { - for (const v of value) { - mergedHeaders.append(key, v as string) - } - } else if (value !== undefined) { - // assume object headers are meant to be JSON stringified, i.e. their - // content value in OpenAPI specification is 'application/json' - mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : (value as string)) - } - } - } - return mergedHeaders -} - -type ErrInterceptor = ( - error: Err, - response: Res, - request: Req, - options: Options, -) => Err | Promise - -type ReqInterceptor = (request: Req, options: Options) => Req | Promise - -type ResInterceptor = (response: Res, request: Req, options: Options) => Res | Promise - -class Interceptors { - fns: Array = [] - - clear(): void { - this.fns = [] - } - - eject(id: number | Interceptor): void { - const index = this.getInterceptorIndex(id) - if (this.fns[index]) { - this.fns[index] = null - } - } - - exists(id: number | Interceptor): boolean { - const index = this.getInterceptorIndex(id) - return Boolean(this.fns[index]) - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === "number") { - return this.fns[id] ? id : -1 - } - return this.fns.indexOf(id) - } - - update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { - const index = this.getInterceptorIndex(id) - if (this.fns[index]) { - this.fns[index] = fn - return id - } - return false - } - - use(fn: Interceptor): number { - this.fns.push(fn) - return this.fns.length - 1 - } -} - -export interface Middleware { - error: Interceptors> - request: Interceptors> - response: Interceptors> -} - -export const createInterceptors = (): Middleware => ({ - error: new Interceptors>(), - request: new Interceptors>(), - response: new Interceptors>(), -}) - -const defaultQuerySerializer = createQuerySerializer({ - allowReserved: false, - array: { - explode: true, - style: "form", - }, - object: { - explode: true, - style: "deepObject", - }, -}) - -const defaultHeaders = { - "Content-Type": "application/json", -} - -export const createConfig = ( - override: Config & T> = {}, -): Config & T> => ({ - ...jsonBodySerializer, - headers: defaultHeaders, - parseAs: "auto", - querySerializer: defaultQuerySerializer, - ...override, -}) diff --git a/packages/sdk/js/src/v2/gen/core/auth.gen.ts b/packages/sdk/js/src/v2/gen/core/auth.gen.ts deleted file mode 100644 index bc7b230f4475..000000000000 --- a/packages/sdk/js/src/v2/gen/core/auth.gen.ts +++ /dev/null @@ -1,41 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type AuthToken = string | undefined - -export interface Auth { - /** - * Which part of the request do we use to send the auth? - * - * @default 'header' - */ - in?: "header" | "query" | "cookie" - /** - * Header or query parameter name. - * - * @default 'Authorization' - */ - name?: string - scheme?: "basic" | "bearer" - type: "apiKey" | "http" -} - -export const getAuthToken = async ( - auth: Auth, - callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, -): Promise => { - const token = typeof callback === "function" ? await callback(auth) : callback - - if (!token) { - return - } - - if (auth.scheme === "bearer") { - return `Bearer ${token}` - } - - if (auth.scheme === "basic") { - return `Basic ${btoa(token)}` - } - - return token -} diff --git a/packages/sdk/js/src/v2/gen/core/bodySerializer.gen.ts b/packages/sdk/js/src/v2/gen/core/bodySerializer.gen.ts deleted file mode 100644 index 9678fb08ec6f..000000000000 --- a/packages/sdk/js/src/v2/gen/core/bodySerializer.gen.ts +++ /dev/null @@ -1,82 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { ArrayStyle, ObjectStyle, SerializerOptions } from "./pathSerializer.gen.js" - -export type QuerySerializer = (query: Record) => string - -export type BodySerializer = (body: any) => any - -type QuerySerializerOptionsObject = { - allowReserved?: boolean - array?: Partial> - object?: Partial> -} - -export type QuerySerializerOptions = QuerySerializerOptionsObject & { - /** - * Per-parameter serialization overrides. When provided, these settings - * override the global array/object settings for specific parameter names. - */ - parameters?: Record -} - -const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { - if (typeof value === "string" || value instanceof Blob) { - data.append(key, value) - } else if (value instanceof Date) { - data.append(key, value.toISOString()) - } else { - data.append(key, JSON.stringify(value)) - } -} - -const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { - if (typeof value === "string") { - data.append(key, value) - } else { - data.append(key, JSON.stringify(value)) - } -} - -export const formDataBodySerializer = { - bodySerializer: | Array>>(body: T): FormData => { - const data = new FormData() - - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return - } - if (Array.isArray(value)) { - value.forEach((v) => serializeFormDataPair(data, key, v)) - } else { - serializeFormDataPair(data, key, value) - } - }) - - return data - }, -} - -export const jsonBodySerializer = { - bodySerializer: (body: T): string => - JSON.stringify(body, (_key, value) => (typeof value === "bigint" ? value.toString() : value)), -} - -export const urlSearchParamsBodySerializer = { - bodySerializer: | Array>>(body: T): string => { - const data = new URLSearchParams() - - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return - } - if (Array.isArray(value)) { - value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)) - } else { - serializeUrlSearchParamsPair(data, key, value) - } - }) - - return data.toString() - }, -} diff --git a/packages/sdk/js/src/v2/gen/core/params.gen.ts b/packages/sdk/js/src/v2/gen/core/params.gen.ts deleted file mode 100644 index 6e9d0b9add42..000000000000 --- a/packages/sdk/js/src/v2/gen/core/params.gen.ts +++ /dev/null @@ -1,169 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -type Slot = "body" | "headers" | "path" | "query" - -export type Field = - | { - in: Exclude - /** - * Field name. This is the name we want the user to see and use. - */ - key: string - /** - * Field mapped name. This is the name we want to use in the request. - * If omitted, we use the same value as `key`. - */ - map?: string - } - | { - in: Extract - /** - * Key isn't required for bodies. - */ - key?: string - map?: string - } - | { - /** - * Field name. This is the name we want the user to see and use. - */ - key: string - /** - * Field mapped name. This is the name we want to use in the request. - * If `in` is omitted, `map` aliases `key` to the transport layer. - */ - map: Slot - } - -export interface Fields { - allowExtra?: Partial> - args?: ReadonlyArray -} - -export type FieldsConfig = ReadonlyArray - -const extraPrefixesMap: Record = { - $body_: "body", - $headers_: "headers", - $path_: "path", - $query_: "query", -} -const extraPrefixes = Object.entries(extraPrefixesMap) - -type KeyMap = Map< - string, - | { - in: Slot - map?: string - } - | { - in?: never - map: Slot - } -> - -const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { - if (!map) { - map = new Map() - } - - for (const config of fields) { - if ("in" in config) { - if (config.key) { - map.set(config.key, { - in: config.in, - map: config.map, - }) - } - } else if ("key" in config) { - map.set(config.key, { - map: config.map, - }) - } else if (config.args) { - buildKeyMap(config.args, map) - } - } - - return map -} - -interface Params { - body: unknown - headers: Record - path: Record - query: Record -} - -const stripEmptySlots = (params: Params) => { - for (const [slot, value] of Object.entries(params)) { - if (value && typeof value === "object" && !Object.keys(value).length) { - delete params[slot as Slot] - } - } -} - -export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { - const params: Params = { - body: {}, - headers: {}, - path: {}, - query: {}, - } - - const map = buildKeyMap(fields) - - let config: FieldsConfig[number] | undefined - - for (const [index, arg] of args.entries()) { - if (fields[index]) { - config = fields[index] - } - - if (!config) { - continue - } - - if ("in" in config) { - if (config.key) { - const field = map.get(config.key)! - const name = field.map || config.key - if (field.in) { - ;(params[field.in] as Record)[name] = arg - } - } else { - params.body = arg - } - } else { - for (const [key, value] of Object.entries(arg ?? {})) { - const field = map.get(key) - - if (field) { - if (field.in) { - const name = field.map || key - ;(params[field.in] as Record)[name] = value - } else { - params[field.map] = value - } - } else { - const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)) - - if (extra) { - const [prefix, slot] = extra - ;(params[slot] as Record)[key.slice(prefix.length)] = value - } else if ("allowExtra" in config && config.allowExtra) { - for (const [slot, allowed] of Object.entries(config.allowExtra)) { - if (allowed) { - ;(params[slot as Slot] as Record)[key] = value - break - } - } - } - } - } - } - } - - stripEmptySlots(params) - - return params -} diff --git a/packages/sdk/js/src/v2/gen/core/pathSerializer.gen.ts b/packages/sdk/js/src/v2/gen/core/pathSerializer.gen.ts deleted file mode 100644 index 96be3bc5a397..000000000000 --- a/packages/sdk/js/src/v2/gen/core/pathSerializer.gen.ts +++ /dev/null @@ -1,167 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} - -interface SerializePrimitiveOptions { - allowReserved?: boolean - name: string -} - -export interface SerializerOptions { - /** - * @default true - */ - explode: boolean - style: T -} - -export type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited" -export type ArraySeparatorStyle = ArrayStyle | MatrixStyle -type MatrixStyle = "label" | "matrix" | "simple" -export type ObjectStyle = "form" | "deepObject" -type ObjectSeparatorStyle = ObjectStyle | MatrixStyle - -interface SerializePrimitiveParam extends SerializePrimitiveOptions { - value: string -} - -export const separatorArrayExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case "label": - return "." - case "matrix": - return ";" - case "simple": - return "," - default: - return "&" - } -} - -export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case "form": - return "," - case "pipeDelimited": - return "|" - case "spaceDelimited": - return "%20" - default: - return "," - } -} - -export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { - switch (style) { - case "label": - return "." - case "matrix": - return ";" - case "simple": - return "," - default: - return "&" - } -} - -export const serializeArrayParam = ({ - allowReserved, - explode, - name, - style, - value, -}: SerializeOptions & { - value: unknown[] -}) => { - if (!explode) { - const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v as string))).join( - separatorArrayNoExplode(style), - ) - switch (style) { - case "label": - return `.${joinedValues}` - case "matrix": - return `;${name}=${joinedValues}` - case "simple": - return joinedValues - default: - return `${name}=${joinedValues}` - } - } - - const separator = separatorArrayExplode(style) - const joinedValues = value - .map((v) => { - if (style === "label" || style === "simple") { - return allowReserved ? v : encodeURIComponent(v as string) - } - - return serializePrimitiveParam({ - allowReserved, - name, - value: v as string, - }) - }) - .join(separator) - return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues -} - -export const serializePrimitiveParam = ({ allowReserved, name, value }: SerializePrimitiveParam) => { - if (value === undefined || value === null) { - return "" - } - - if (typeof value === "object") { - throw new Error( - "Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.", - ) - } - - return `${name}=${allowReserved ? value : encodeURIComponent(value)}` -} - -export const serializeObjectParam = ({ - allowReserved, - explode, - name, - style, - value, - valueOnly, -}: SerializeOptions & { - value: Record | Date - valueOnly?: boolean -}) => { - if (value instanceof Date) { - return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}` - } - - if (style !== "deepObject" && !explode) { - let values: string[] = [] - Object.entries(value).forEach(([key, v]) => { - values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)] - }) - const joinedValues = values.join(",") - switch (style) { - case "form": - return `${name}=${joinedValues}` - case "label": - return `.${joinedValues}` - case "matrix": - return `;${name}=${joinedValues}` - default: - return joinedValues - } - } - - const separator = separatorObjectExplode(style) - const joinedValues = Object.entries(value) - .map(([key, v]) => - serializePrimitiveParam({ - allowReserved, - name: style === "deepObject" ? `${name}[${key}]` : key, - value: v as string, - }), - ) - .join(separator) - return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues -} diff --git a/packages/sdk/js/src/v2/gen/core/queryKeySerializer.gen.ts b/packages/sdk/js/src/v2/gen/core/queryKeySerializer.gen.ts deleted file mode 100644 index 320204aef108..000000000000 --- a/packages/sdk/js/src/v2/gen/core/queryKeySerializer.gen.ts +++ /dev/null @@ -1,111 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -/** - * JSON-friendly union that mirrors what Pinia Colada can hash. - */ -export type JsonValue = null | string | number | boolean | JsonValue[] | { [key: string]: JsonValue } - -/** - * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. - */ -export const queryKeyJsonReplacer = (_key: string, value: unknown) => { - if (value === undefined || typeof value === "function" || typeof value === "symbol") { - return undefined - } - if (typeof value === "bigint") { - return value.toString() - } - if (value instanceof Date) { - return value.toISOString() - } - return value -} - -/** - * Safely stringifies a value and parses it back into a JsonValue. - */ -export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { - try { - const json = JSON.stringify(input, queryKeyJsonReplacer) - if (json === undefined) { - return undefined - } - return JSON.parse(json) as JsonValue - } catch { - return undefined - } -} - -/** - * Detects plain objects (including objects with a null prototype). - */ -const isPlainObject = (value: unknown): value is Record => { - if (value === null || typeof value !== "object") { - return false - } - const prototype = Object.getPrototypeOf(value as object) - return prototype === Object.prototype || prototype === null -} - -/** - * Turns URLSearchParams into a sorted JSON object for deterministic keys. - */ -const serializeSearchParams = (params: URLSearchParams): JsonValue => { - const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)) - const result: Record = {} - - for (const [key, value] of entries) { - const existing = result[key] - if (existing === undefined) { - result[key] = value - continue - } - - if (Array.isArray(existing)) { - ;(existing as string[]).push(value) - } else { - result[key] = [existing, value] - } - } - - return result -} - -/** - * Normalizes any accepted value into a JSON-friendly shape for query keys. - */ -export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { - if (value === null) { - return null - } - - if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { - return value - } - - if (value === undefined || typeof value === "function" || typeof value === "symbol") { - return undefined - } - - if (typeof value === "bigint") { - return value.toString() - } - - if (value instanceof Date) { - return value.toISOString() - } - - if (Array.isArray(value)) { - return stringifyToJsonValue(value) - } - - if (typeof URLSearchParams !== "undefined" && value instanceof URLSearchParams) { - return serializeSearchParams(value) - } - - if (isPlainObject(value)) { - return stringifyToJsonValue(value) - } - - return undefined -} diff --git a/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts b/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts deleted file mode 100644 index 056a81259322..000000000000 --- a/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts +++ /dev/null @@ -1,239 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Config } from "./types.gen.js" - -export type ServerSentEventsOptions = Omit & - Pick & { - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch - /** - * Implementing clients can call request interceptors inside this hook. - */ - onRequest?: (url: string, init: RequestInit) => Promise - /** - * Callback invoked when a network or parsing error occurs during streaming. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param error The error that occurred. - */ - onSseError?: (error: unknown) => void - /** - * Callback invoked when an event is streamed from the server. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param event Event streamed from the server. - * @returns Nothing (void). - */ - onSseEvent?: (event: StreamEvent) => void - serializedBody?: RequestInit["body"] - /** - * Default retry delay in milliseconds. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 3000 - */ - sseDefaultRetryDelay?: number - /** - * Maximum number of retry attempts before giving up. - */ - sseMaxRetryAttempts?: number - /** - * Maximum retry delay in milliseconds. - * - * Applies only when exponential backoff is used. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 30000 - */ - sseMaxRetryDelay?: number - /** - * Optional sleep function for retry backoff. - * - * Defaults to using `setTimeout`. - */ - sseSleepFn?: (ms: number) => Promise - url: string - } - -export interface StreamEvent { - data: TData - event?: string - id?: string - retry?: number -} - -export type ServerSentEventsResult = { - stream: AsyncGenerator ? TData[keyof TData] : TData, TReturn, TNext> -} - -export const createSseClient = ({ - onRequest, - onSseError, - onSseEvent, - responseTransformer, - responseValidator, - sseDefaultRetryDelay, - sseMaxRetryAttempts, - sseMaxRetryDelay, - sseSleepFn, - url, - ...options -}: ServerSentEventsOptions): ServerSentEventsResult => { - let lastEventId: string | undefined - - const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))) - - const createStream = async function* () { - let retryDelay: number = sseDefaultRetryDelay ?? 3000 - let attempt = 0 - const signal = options.signal ?? new AbortController().signal - - while (true) { - if (signal.aborted) break - - attempt++ - - const headers = - options.headers instanceof Headers - ? options.headers - : new Headers(options.headers as Record | undefined) - - if (lastEventId !== undefined) { - headers.set("Last-Event-ID", lastEventId) - } - - try { - const requestInit: RequestInit = { - redirect: "follow", - ...options, - body: options.serializedBody, - headers, - signal, - } - let request = new Request(url, requestInit) - if (onRequest) { - request = await onRequest(url, requestInit) - } - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = options.fetch ?? globalThis.fetch - const response = await _fetch(request) - - if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`) - - if (!response.body) throw new Error("No body in SSE response") - - const reader = response.body.pipeThrough(new TextDecoderStream()).getReader() - - let buffer = "" - - const abortHandler = () => { - try { - reader.cancel() - } catch { - // noop - } - } - - signal.addEventListener("abort", abortHandler) - - try { - while (true) { - const { done, value } = await reader.read() - if (done) break - buffer += value - // Normalize line endings: CRLF -> LF, then CR -> LF - buffer = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n") - - const chunks = buffer.split("\n\n") - buffer = chunks.pop() ?? "" - - for (const chunk of chunks) { - const lines = chunk.split("\n") - const dataLines: Array = [] - let eventName: string | undefined - - for (const line of lines) { - if (line.startsWith("data:")) { - dataLines.push(line.replace(/^data:\s*/, "")) - } else if (line.startsWith("event:")) { - eventName = line.replace(/^event:\s*/, "") - } else if (line.startsWith("id:")) { - lastEventId = line.replace(/^id:\s*/, "") - } else if (line.startsWith("retry:")) { - const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10) - if (!Number.isNaN(parsed)) { - retryDelay = parsed - } - } - } - - let data: unknown - let parsedJson = false - - if (dataLines.length) { - const rawData = dataLines.join("\n") - try { - data = JSON.parse(rawData) - parsedJson = true - } catch { - data = rawData - } - } - - if (parsedJson) { - if (responseValidator) { - await responseValidator(data) - } - - if (responseTransformer) { - data = await responseTransformer(data) - } - } - - onSseEvent?.({ - data, - event: eventName, - id: lastEventId, - retry: retryDelay, - }) - - if (dataLines.length) { - yield data as any - } - } - } - } finally { - signal.removeEventListener("abort", abortHandler) - reader.releaseLock() - } - - break // exit loop on normal completion - } catch (error) { - // connection failed or aborted; retry after delay - onSseError?.(error) - - if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { - break // stop after firing error - } - - // exponential backoff: double retry each attempt, cap at 30s - const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000) - await sleep(backoff) - } - } - } - - const stream = createStream() - - return { stream } -} diff --git a/packages/sdk/js/src/v2/gen/core/types.gen.ts b/packages/sdk/js/src/v2/gen/core/types.gen.ts deleted file mode 100644 index bfa77b8acd2b..000000000000 --- a/packages/sdk/js/src/v2/gen/core/types.gen.ts +++ /dev/null @@ -1,86 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth, AuthToken } from "./auth.gen.js" -import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.gen.js" - -export type HttpMethod = "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace" - -export type Client = { - /** - * Returns the final request URL. - */ - buildUrl: BuildUrlFn - getConfig: () => Config - request: RequestFn - setConfig: (config: Config) => Config -} & { - [K in HttpMethod]: MethodFn -} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }) - -export interface Config { - /** - * Auth token or a function returning auth token. The resolved value will be - * added to the request payload as defined by its `security` array. - */ - auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken - /** - * A function for serializing request body parameter. By default, - * {@link JSON.stringify()} will be used. - */ - bodySerializer?: BodySerializer | null - /** - * An object containing any HTTP headers that you want to pre-populate your - * `Headers` object with. - * - * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} - */ - headers?: - | RequestInit["headers"] - | Record - /** - * The request method. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} - */ - method?: Uppercase - /** - * A function for serializing request query parameters. By default, arrays - * will be exploded in form style, objects will be exploded in deepObject - * style, and reserved characters are percent-encoded. - * - * This method will have no effect if the native `paramsSerializer()` Axios - * API function is used. - * - * {@link https://swagger.io/docs/specification/serialization/#query View examples} - */ - querySerializer?: QuerySerializer | QuerySerializerOptions - /** - * A function validating request data. This is useful if you want to ensure - * the request conforms to the desired shape, so it can be safely sent to - * the server. - */ - requestValidator?: (data: unknown) => Promise - /** - * A function transforming response data before it's returned. This is useful - * for post-processing data, e.g. converting ISO strings into Date objects. - */ - responseTransformer?: (data: unknown) => Promise - /** - * A function validating response data. This is useful if you want to ensure - * the response conforms to the desired shape, so it can be safely passed to - * the transformers and returned to the user. - */ - responseValidator?: (data: unknown) => Promise -} - -type IsExactlyNeverOrNeverUndefined = [T] extends [never] - ? true - : [T] extends [never | undefined] - ? [undefined] extends [T] - ? false - : true - : false - -export type OmitNever> = { - [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K] -} diff --git a/packages/sdk/js/src/v2/gen/core/utils.gen.ts b/packages/sdk/js/src/v2/gen/core/utils.gen.ts deleted file mode 100644 index 8a45f72698ae..000000000000 --- a/packages/sdk/js/src/v2/gen/core/utils.gen.ts +++ /dev/null @@ -1,137 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { BodySerializer, QuerySerializer } from "./bodySerializer.gen.js" -import { - type ArraySeparatorStyle, - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from "./pathSerializer.gen.js" - -export interface PathSerializer { - path: Record - url: string -} - -export const PATH_PARAM_RE = /\{[^{}]+\}/g - -export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url - const matches = _url.match(PATH_PARAM_RE) - if (matches) { - for (const match of matches) { - let explode = false - let name = match.substring(1, match.length - 1) - let style: ArraySeparatorStyle = "simple" - - if (name.endsWith("*")) { - explode = true - name = name.substring(0, name.length - 1) - } - - if (name.startsWith(".")) { - name = name.substring(1) - style = "label" - } else if (name.startsWith(";")) { - name = name.substring(1) - style = "matrix" - } - - const value = path[name] - - if (value === undefined || value === null) { - continue - } - - if (Array.isArray(value)) { - url = url.replace(match, serializeArrayParam({ explode, name, style, value })) - continue - } - - if (typeof value === "object") { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ) - continue - } - - if (style === "matrix") { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ) - continue - } - - const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string)) - url = url.replace(match, replaceValue) - } - } - return url -} - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string - path?: Record - query?: Record - querySerializer: QuerySerializer - url: string -}) => { - const pathUrl = _url.startsWith("/") ? _url : `/${_url}` - let url = (baseUrl ?? "") + pathUrl - if (path) { - url = defaultPathSerializer({ path, url }) - } - let search = query ? querySerializer(query) : "" - if (search.startsWith("?")) { - search = search.substring(1) - } - if (search) { - url += `?${search}` - } - return url -} - -export function getValidRequestBody(options: { - body?: unknown - bodySerializer?: BodySerializer | null - serializedBody?: unknown -}) { - const hasBody = options.body !== undefined - const isSerializedBody = hasBody && options.bodySerializer - - if (isSerializedBody) { - if ("serializedBody" in options) { - const hasSerializedBody = options.serializedBody !== undefined && options.serializedBody !== "" - - return hasSerializedBody ? options.serializedBody : null - } - - // not all clients implement a serializedBody property (i.e. client-axios) - return options.body !== "" ? options.body : null - } - - // plain/text body - if (hasBody) { - return options.body - } - - // no body was provided - return undefined -} diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts deleted file mode 100644 index 35dc600e5df5..000000000000 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ /dev/null @@ -1,8828 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { client } from "./client.gen.js" -import { buildClientParams, type Client, type Options as Options2, type TDataShape } from "./client/index.js" -import type { - AgentPartInput, - AppAgentsErrors, - AppAgentsResponses, - AppLogErrors, - AppLogResponses, - AppSkillsErrors, - AppSkillsResponses, - Auth as Auth3, - AuthRemoveErrors, - AuthRemoveResponses, - AuthSetErrors, - AuthSetResponses, - CommandListErrors, - CommandListResponses, - Config as Config3, - ConfigGetErrors, - ConfigGetResponses, - ConfigProvidersErrors, - ConfigProvidersResponses, - ConfigUpdateErrors, - ConfigUpdateResponses, - EventSubscribeResponses, - EventTuiCommandExecute, - EventTuiPromptAppend, - EventTuiSessionSelect, - EventTuiToastShow, - ExperimentalCapabilitiesGetErrors, - ExperimentalCapabilitiesGetResponses, - ExperimentalConsoleGetErrors, - ExperimentalConsoleGetResponses, - ExperimentalConsoleListOrgsErrors, - ExperimentalConsoleListOrgsResponses, - ExperimentalConsoleSwitchOrgResponses, - ExperimentalControlPlaneMoveSessionErrors, - ExperimentalControlPlaneMoveSessionResponses, - ExperimentalProjectCopyGenerateNameErrors, - ExperimentalProjectCopyGenerateNameResponses, - ExperimentalResourceListErrors, - ExperimentalResourceListResponses, - ExperimentalSessionBackgroundErrors, - ExperimentalSessionBackgroundResponses, - ExperimentalSessionListErrors, - ExperimentalSessionListResponses, - ExperimentalWorkspaceAdapterListErrors, - ExperimentalWorkspaceAdapterListResponses, - ExperimentalWorkspaceCreateErrors, - ExperimentalWorkspaceCreateResponses, - ExperimentalWorkspaceListErrors, - ExperimentalWorkspaceListResponses, - ExperimentalWorkspaceRemoveErrors, - ExperimentalWorkspaceRemoveResponses, - ExperimentalWorkspaceStatusErrors, - ExperimentalWorkspaceStatusResponses, - ExperimentalWorkspaceSyncListErrors, - ExperimentalWorkspaceSyncListResponses, - ExperimentalWorkspaceWarpErrors, - ExperimentalWorkspaceWarpResponses, - FileListErrors, - FileListResponses, - FilePartInput, - FilePartSource, - FileReadErrors, - FileReadResponses, - FileStatusErrors, - FileStatusResponses, - FindFilesErrors, - FindFilesResponses, - FindSymbolsErrors, - FindSymbolsResponses, - FindTextErrors, - FindTextResponses, - FormatterStatusErrors, - FormatterStatusResponses, - FormCreatePayloadV2, - FormReply, - GlobalConfigGetErrors, - GlobalConfigGetResponses, - GlobalConfigUpdateErrors, - GlobalConfigUpdateResponses, - GlobalDisposeErrors, - GlobalDisposeResponses, - GlobalEventErrors, - GlobalEventResponses, - GlobalHealthErrors, - GlobalHealthResponses, - GlobalUpgradeErrors, - GlobalUpgradeResponses, - InstanceDisposeErrors, - InstanceDisposeResponses, - InstructionEntryKeyV2, - LocationRefV2, - LspStatusErrors, - LspStatusResponses, - McpAddErrors, - McpAddResponses, - McpAuthAuthenticateErrors, - McpAuthAuthenticateResponses, - McpAuthCallbackErrors, - McpAuthCallbackResponses, - McpAuthRemoveErrors, - McpAuthRemoveResponses, - McpAuthStartErrors, - McpAuthStartResponses, - McpConnectErrors, - McpConnectResponses, - McpDisconnectErrors, - McpDisconnectResponses, - McpLocalConfig, - McpRemoteConfig, - McpStatusErrors, - McpStatusResponses, - ModelRef, - MoveSessionDestination, - OutputFormat, - Part as Part2, - PartDeleteErrors, - PartDeleteResponses, - PartUpdateErrors, - PartUpdateResponses, - PathGetErrors, - PathGetResponses, - PermissionListErrors, - PermissionListResponses, - PermissionReplyErrors, - PermissionReplyResponses, - PermissionRespondErrors, - PermissionRespondResponses, - PermissionRuleset, - PermissionV2Reply, - PermissionV2SourceV2, - ProjectCommands, - ProjectCurrentErrors, - ProjectCurrentResponses, - ProjectDirectoriesErrors, - ProjectDirectoriesResponses, - ProjectIcon, - ProjectInitGitErrors, - ProjectInitGitResponses, - ProjectListErrors, - ProjectListResponses, - ProjectUpdateErrors, - ProjectUpdateResponses, - PromptAgentAttachment, - PromptInputFileAttachment, - ProviderAuthErrors, - ProviderAuthResponses, - ProviderListErrors, - ProviderListResponses, - ProviderOauthAuthorizeErrors, - ProviderOauthAuthorizeResponses, - ProviderOauthCallbackErrors, - ProviderOauthCallbackResponses, - PtyConnectErrors, - PtyConnectResponses, - PtyConnectTokenErrors, - PtyConnectTokenResponses, - PtyCreateErrors, - PtyCreateResponses, - PtyGetErrors, - PtyGetResponses, - PtyListErrors, - PtyListResponses, - PtyRemoveErrors, - PtyRemoveResponses, - PtyShellsErrors, - PtyShellsResponses, - PtyUpdateErrors, - PtyUpdateResponses, - QuestionAnswer, - QuestionListErrors, - QuestionListResponses, - QuestionRejectErrors, - QuestionRejectResponses, - QuestionReplyErrors, - QuestionReplyResponses, - QuestionV2Reply, - ServiceStopRequest, - SessionAbortErrors, - SessionAbortResponses, - SessionChildrenErrors, - SessionChildrenResponses, - SessionCommandErrors, - SessionCommandResponses, - SessionCreateErrors, - SessionCreateResponses, - SessionDeleteErrors, - SessionDeleteMessageErrors, - SessionDeleteMessageResponses, - SessionDeleteResponses, - SessionDiffErrors, - SessionDiffResponses, - SessionForkErrors, - SessionForkResponses, - SessionGetErrors, - SessionGetResponses, - SessionInitErrors, - SessionInitResponses, - SessionListErrors, - SessionListResponses, - SessionMessageErrors, - SessionMessageResponses, - SessionMessagesErrors, - SessionMessagesResponses, - SessionPromptAsyncErrors, - SessionPromptAsyncResponses, - SessionPromptErrors, - SessionPromptResponses, - SessionRevertErrors, - SessionRevertResponses, - SessionShareErrors, - SessionShareResponses, - SessionShellErrors, - SessionShellResponses, - SessionStatusErrors, - SessionStatusResponses, - SessionSummarizeErrors, - SessionSummarizeResponses, - SessionUnrevertErrors, - SessionUnrevertResponses, - SessionUnshareErrors, - SessionUnshareResponses, - SessionUpdateErrors, - SessionUpdateResponses, - SubtaskPartInput, - SyncHistoryListErrors, - SyncHistoryListResponses, - SyncReplayErrors, - SyncReplayResponses, - SyncStartErrors, - SyncStartResponses, - SyncStealErrors, - SyncStealResponses, - TextPartInput, - ToolIdsErrors, - ToolIdsResponses, - ToolListErrors, - ToolListResponses, - TuiAppendPromptErrors, - TuiAppendPromptResponses, - TuiClearPromptErrors, - TuiClearPromptResponses, - TuiControlNextErrors, - TuiControlNextResponses, - TuiControlResponseErrors, - TuiControlResponseResponses, - TuiExecuteCommandErrors, - TuiExecuteCommandResponses, - TuiOpenHelpErrors, - TuiOpenHelpResponses, - TuiOpenModelsErrors, - TuiOpenModelsResponses, - TuiOpenSessionsErrors, - TuiOpenSessionsResponses, - TuiOpenThemesErrors, - TuiOpenThemesResponses, - TuiPublishErrors, - TuiPublishResponses, - TuiSelectSessionErrors, - TuiSelectSessionResponses, - TuiShowToastErrors, - TuiShowToastResponses, - TuiSubmitPromptErrors, - TuiSubmitPromptResponses, - V2AgentListErrors, - V2AgentListResponses, - V2CommandListErrors, - V2CommandListResponses, - V2CredentialRemoveErrors, - V2CredentialRemoveResponses, - V2CredentialUpdateErrors, - V2CredentialUpdateResponses, - V2DebugLocationEvictErrors, - V2DebugLocationEvictResponses, - V2DebugLocationListErrors, - V2DebugLocationListResponses, - V2EventSubscribeErrors, - V2EventSubscribeResponses, - V2ExperimentalIntegrationWellknownAddErrors, - V2ExperimentalIntegrationWellknownAddResponses, - V2FormRequestListErrors, - V2FormRequestListResponses, - V2FsFindErrors, - V2FsFindResponses, - V2FsListErrors, - V2FsListResponses, - V2FsReadErrors, - V2FsReadResponses, - V2GenerateTextErrors, - V2GenerateTextResponses, - V2HealthGetErrors, - V2HealthGetResponses, - V2HealthStopErrors, - V2HealthStopResponses, - V2IntegrationCommandCancelErrors, - V2IntegrationCommandCancelResponses, - V2IntegrationCommandConnectErrors, - V2IntegrationCommandConnectResponses, - V2IntegrationCommandStatusErrors, - V2IntegrationCommandStatusResponses, - V2IntegrationConnectKeyErrors, - V2IntegrationConnectKeyResponses, - V2IntegrationGetErrors, - V2IntegrationGetResponses, - V2IntegrationListErrors, - V2IntegrationListResponses, - V2IntegrationOauthCancelErrors, - V2IntegrationOauthCancelResponses, - V2IntegrationOauthCompleteErrors, - V2IntegrationOauthCompleteResponses, - V2IntegrationOauthConnectErrors, - V2IntegrationOauthConnectResponses, - V2IntegrationOauthStatusErrors, - V2IntegrationOauthStatusResponses, - V2LocationGetErrors, - V2LocationGetResponses, - V2McpListErrors, - V2McpListResponses, - V2McpResourceCatalogErrors, - V2McpResourceCatalogResponses, - V2MessageListErrors, - V2MessageListResponses, - V2ModelDefaultErrors, - V2ModelDefaultResponses, - V2ModelListErrors, - V2ModelListResponses, - V2PermissionRequestListErrors, - V2PermissionRequestListResponses, - V2PermissionSavedListErrors, - V2PermissionSavedListResponses, - V2PermissionSavedRemoveErrors, - V2PermissionSavedRemoveResponses, - V2PluginListErrors, - V2PluginListResponses, - V2ProjectCopyCreateErrors, - V2ProjectCopyCreateResponses, - V2ProjectCopyRefreshErrors, - V2ProjectCopyRefreshResponses, - V2ProjectCopyRemoveErrors, - V2ProjectCopyRemoveResponses, - V2ProjectCurrentErrors, - V2ProjectCurrentResponses, - V2ProjectDirectoriesErrors, - V2ProjectDirectoriesResponses, - V2ProjectListErrors, - V2ProjectListResponses, - V2ProviderGetErrors, - V2ProviderGetResponses, - V2ProviderListErrors, - V2ProviderListResponses, - V2PtyConnectErrors, - V2PtyConnectResponses, - V2PtyConnectTokenErrors, - V2PtyConnectTokenResponses, - V2PtyCreateErrors, - V2PtyCreateResponses, - V2PtyGetErrors, - V2PtyGetResponses, - V2PtyListErrors, - V2PtyListResponses, - V2PtyRemoveErrors, - V2PtyRemoveResponses, - V2PtyUpdateErrors, - V2PtyUpdateResponses, - V2QuestionRequestListErrors, - V2QuestionRequestListResponses, - V2ReferenceListErrors, - V2ReferenceListResponses, - V2ServerGetErrors, - V2ServerGetResponses, - V2SessionActiveErrors, - V2SessionActiveResponses, - V2SessionBackgroundErrors, - V2SessionBackgroundResponses, - V2SessionCommandErrors, - V2SessionCommandResponses, - V2SessionCompactErrors, - V2SessionCompactResponses, - V2SessionContextErrors, - V2SessionContextResponses, - V2SessionCreateErrors, - V2SessionCreateResponses, - V2SessionForkErrors, - V2SessionForkResponses, - V2SessionFormCancelErrors, - V2SessionFormCancelResponses, - V2SessionFormCreateErrors, - V2SessionFormCreateResponses, - V2SessionFormGetErrors, - V2SessionFormGetResponses, - V2SessionFormListErrors, - V2SessionFormListResponses, - V2SessionFormReplyErrors, - V2SessionFormReplyResponses, - V2SessionFormStateErrors, - V2SessionFormStateResponses, - V2SessionGenerateErrors, - V2SessionGenerateResponses, - V2SessionGetErrors, - V2SessionGetResponses, - V2SessionInstructionsEntryListErrors, - V2SessionInstructionsEntryListResponses, - V2SessionInstructionsEntryPutErrors, - V2SessionInstructionsEntryPutResponses, - V2SessionInstructionsEntryRemoveErrors, - V2SessionInstructionsEntryRemoveResponses, - V2SessionInterruptErrors, - V2SessionInterruptResponses, - V2SessionListErrors, - V2SessionListResponses, - V2SessionLogErrors, - V2SessionLogResponses, - V2SessionMessageErrors, - V2SessionMessageResponses, - V2SessionMoveErrors, - V2SessionMoveResponses, - V2SessionPendingListErrors, - V2SessionPendingListResponses, - V2SessionPermissionCreateErrors, - V2SessionPermissionCreateResponses, - V2SessionPermissionGetErrors, - V2SessionPermissionGetResponses, - V2SessionPermissionListErrors, - V2SessionPermissionListResponses, - V2SessionPermissionReplyErrors, - V2SessionPermissionReplyResponses, - V2SessionPromptErrors, - V2SessionPromptResponses, - V2SessionQuestionListErrors, - V2SessionQuestionListResponses, - V2SessionQuestionRejectErrors, - V2SessionQuestionRejectResponses, - V2SessionQuestionReplyErrors, - V2SessionQuestionReplyResponses, - V2SessionRemoveErrors, - V2SessionRemoveResponses, - V2SessionRenameErrors, - V2SessionRenameResponses, - V2SessionRevertClearErrors, - V2SessionRevertClearResponses, - V2SessionRevertCommitErrors, - V2SessionRevertCommitResponses, - V2SessionRevertStageErrors, - V2SessionRevertStageResponses, - V2SessionShellErrors, - V2SessionShellResponses, - V2SessionSkillErrors, - V2SessionSkillResponses, - V2SessionSwitchAgentErrors, - V2SessionSwitchAgentResponses, - V2SessionSwitchModelErrors, - V2SessionSwitchModelResponses, - V2SessionSyntheticErrors, - V2SessionSyntheticResponses, - V2SessionWaitErrors, - V2SessionWaitResponses, - V2ShellCreateErrors, - V2ShellCreateResponses, - V2ShellGetErrors, - V2ShellGetResponses, - V2ShellListErrors, - V2ShellListResponses, - V2ShellOutputErrors, - V2ShellOutputResponses, - V2ShellRemoveErrors, - V2ShellRemoveResponses, - V2ShellTimeoutErrors, - V2ShellTimeoutResponses, - V2SkillListErrors, - V2SkillListResponses, - V2VcsDiffErrors, - V2VcsDiffResponses, - V2VcsStatusErrors, - V2VcsStatusResponses, - VcsApplyErrors, - VcsApplyResponses, - VcsDiffErrors, - VcsDiffRawErrors, - VcsDiffRawResponses, - VcsDiffResponses, - VcsGetErrors, - VcsGetResponses, - VcsMode, - VcsStatusErrors, - VcsStatusResponses, - WorktreeCreateErrors, - WorktreeCreateInput, - WorktreeCreateResponses, - WorktreeListErrors, - WorktreeListResponses, - WorktreeRemoveErrors, - WorktreeRemoveInput, - WorktreeRemoveResponses, - WorktreeResetErrors, - WorktreeResetInput, - WorktreeResetResponses, -} from "./types.gen.js" - -export type Options = Options2< - TData, - ThrowOnError -> & { - /** - * You can provide a client instance returned by `createClient()` instead of - * individual options. This might be also useful if you want to implement a - * custom client. - */ - client?: Client - /** - * You can pass arbitrary values through the `meta` object. This can be - * used to access values that aren't defined as part of the SDK function. - */ - meta?: Record -} - -class HeyApiClient { - protected client: Client - - constructor(args?: { client?: Client }) { - this.client = args?.client ?? client - } -} - -class HeyApiRegistry { - private readonly defaultKey = "default" - - private readonly instances: Map = new Map() - - get(key?: string): T { - const instance = this.instances.get(key ?? this.defaultKey) - if (!instance) { - throw new Error(`No SDK client found. Create one with "new OpencodeClient()" to fix this error.`) - } - return instance - } - - set(value: T, key?: string): void { - this.instances.set(key ?? this.defaultKey, value) - } -} - -export class Auth extends HeyApiClient { - /** - * Remove auth credentials - * - * Remove authentication credentials - */ - public remove( - parameters: { - providerID: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "providerID" }] }]) - return (options?.client ?? this.client).delete({ - url: "/auth/{providerID}", - ...options, - ...params, - }) - } - - /** - * Set auth credentials - * - * Set authentication credentials - */ - public set( - parameters: { - providerID: string - auth?: Auth3 - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "providerID" }, - { key: "auth", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).put({ - url: "/auth/{providerID}", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class App extends HeyApiClient { - /** - * Write log - * - * Write a log entry to the server logs with specified level and metadata. - */ - public log( - parameters?: { - directory?: string - workspace?: string - service?: string - level?: "debug" | "info" | "error" | "warn" - message?: string - extra?: { - [key: string]: unknown - } - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "service" }, - { in: "body", key: "level" }, - { in: "body", key: "message" }, - { in: "body", key: "extra" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/log", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * List agents - * - * Get a list of all available AI agents in the OpenCode system. - */ - public agents( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/agent", - ...options, - ...params, - }) - } - - /** - * List skills - * - * Get a list of all available skills in the OpenCode system. - */ - public skills( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/skill", - ...options, - ...params, - }) - } -} - -export class ControlPlane extends HeyApiClient { - /** - * Move session - * - * Move a session to another project directory, optionally transferring local changes. - */ - public moveSession( - parameters?: { - sessionID?: string - destination?: MoveSessionDestination - moveChanges?: boolean - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "body", key: "sessionID" }, - { in: "body", key: "destination" }, - { in: "body", key: "moveChanges" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - ExperimentalControlPlaneMoveSessionResponses, - ExperimentalControlPlaneMoveSessionErrors, - ThrowOnError - >({ - url: "/experimental/control-plane/move-session", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Capabilities extends HeyApiClient { - /** - * Get experimental capabilities - * - * Get experimental features enabled on the OpenCode server. - */ - public get( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get< - ExperimentalCapabilitiesGetResponses, - ExperimentalCapabilitiesGetErrors, - ThrowOnError - >({ - url: "/experimental/capabilities", - ...options, - ...params, - }) - } -} - -export class Console extends HeyApiClient { - /** - * Get active Console provider metadata - * - * Get the active Console org name and the set of provider IDs managed by that Console org. - */ - public get( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get< - ExperimentalConsoleGetResponses, - ExperimentalConsoleGetErrors, - ThrowOnError - >({ - url: "/experimental/console", - ...options, - ...params, - }) - } - - /** - * List switchable Console orgs - * - * Get the available Console orgs across logged-in accounts, including the current active org. - */ - public listOrgs( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get< - ExperimentalConsoleListOrgsResponses, - ExperimentalConsoleListOrgsErrors, - ThrowOnError - >({ - url: "/experimental/console/orgs", - ...options, - ...params, - }) - } - - /** - * Switch active Console org - * - * Persist a new active Console account/org selection for the current local OpenCode state. - */ - public switchOrg( - parameters?: { - directory?: string - workspace?: string - accountID?: string - orgID?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "accountID" }, - { in: "body", key: "orgID" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/experimental/console/switch", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Session extends HeyApiClient { - /** - * List sessions - * - * Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default. - */ - public list( - parameters?: { - directory?: string - workspace?: string - roots?: boolean | "true" | "false" - start?: number - cursor?: number - search?: string - limit?: number - archived?: boolean | "true" | "false" - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "query", key: "roots" }, - { in: "query", key: "start" }, - { in: "query", key: "cursor" }, - { in: "query", key: "search" }, - { in: "query", key: "limit" }, - { in: "query", key: "archived" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get< - ExperimentalSessionListResponses, - ExperimentalSessionListErrors, - ThrowOnError - >({ - url: "/experimental/session", - ...options, - ...params, - }) - } - - /** - * Background subagents - * - * Detach any synchronous subagents currently blocking the session and continue them in the background. - */ - public background( - parameters: { - sessionID: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - ExperimentalSessionBackgroundResponses, - ExperimentalSessionBackgroundErrors, - ThrowOnError - >({ - url: "/experimental/session/{sessionID}/background", - ...options, - ...params, - }) - } -} - -export class Resource extends HeyApiClient { - /** - * Get MCP resources - * - * Get all available MCP resources from connected servers. Optionally filter by name. - */ - public list( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get< - ExperimentalResourceListResponses, - ExperimentalResourceListErrors, - ThrowOnError - >({ - url: "/experimental/resource", - ...options, - ...params, - }) - } -} - -export class ProjectCopy extends HeyApiClient { - /** - * Generate project copy name - * - * Generate a short name for a project copy from task context. - */ - public generateName( - parameters: { - projectID: string - directory?: string - workspace?: string - context?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "projectID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "context" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - ExperimentalProjectCopyGenerateNameResponses, - ExperimentalProjectCopyGenerateNameErrors, - ThrowOnError - >({ - url: "/experimental/project/{projectID}/copy/generate-name", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Adapter extends HeyApiClient { - /** - * List workspace adapters - * - * List all available workspace adapters for the current project. - */ - public list( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get< - ExperimentalWorkspaceAdapterListResponses, - ExperimentalWorkspaceAdapterListErrors, - ThrowOnError - >({ - url: "/experimental/workspace/adapter", - ...options, - ...params, - }) - } -} - -export class Workspace extends HeyApiClient { - /** - * List workspaces - * - * List all workspaces. - */ - public list( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get< - ExperimentalWorkspaceListResponses, - ExperimentalWorkspaceListErrors, - ThrowOnError - >({ - url: "/experimental/workspace", - ...options, - ...params, - }) - } - - /** - * Create workspace - * - * Create a workspace for the current project. - */ - public create( - parameters?: { - directory?: string - workspace?: string - id?: string - type?: string - branch?: string | null - extra?: unknown | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "id" }, - { in: "body", key: "type" }, - { in: "body", key: "branch" }, - { in: "body", key: "extra" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - ExperimentalWorkspaceCreateResponses, - ExperimentalWorkspaceCreateErrors, - ThrowOnError - >({ - url: "/experimental/workspace", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Sync workspace list - * - * Register missing workspaces returned by workspace adapters. - */ - public syncList( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - ExperimentalWorkspaceSyncListResponses, - ExperimentalWorkspaceSyncListErrors, - ThrowOnError - >({ - url: "/experimental/workspace/sync-list", - ...options, - ...params, - }) - } - - /** - * Workspace status - * - * Get connection status for workspaces in the current project. - */ - public status( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get< - ExperimentalWorkspaceStatusResponses, - ExperimentalWorkspaceStatusErrors, - ThrowOnError - >({ - url: "/experimental/workspace/status", - ...options, - ...params, - }) - } - - /** - * Remove workspace - * - * Remove an existing workspace. - */ - public remove( - parameters: { - id: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "id" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).delete< - ExperimentalWorkspaceRemoveResponses, - ExperimentalWorkspaceRemoveErrors, - ThrowOnError - >({ - url: "/experimental/workspace/{id}", - ...options, - ...params, - }) - } - - /** - * Warp session into workspace - * - * Move a session's sync history into the target workspace, or detach it to the local project. - */ - public warp( - parameters?: { - directory?: string - workspace?: string - id?: string | null - sessionID?: string - copyChanges?: boolean - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "id" }, - { in: "body", key: "sessionID" }, - { in: "body", key: "copyChanges" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - ExperimentalWorkspaceWarpResponses, - ExperimentalWorkspaceWarpErrors, - ThrowOnError - >({ - url: "/experimental/workspace/warp", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - private _adapter?: Adapter - get adapter(): Adapter { - return (this._adapter ??= new Adapter({ client: this.client })) - } -} - -export class Experimental extends HeyApiClient { - private _controlPlane?: ControlPlane - get controlPlane(): ControlPlane { - return (this._controlPlane ??= new ControlPlane({ client: this.client })) - } - - private _capabilities?: Capabilities - get capabilities(): Capabilities { - return (this._capabilities ??= new Capabilities({ client: this.client })) - } - - private _console?: Console - get console(): Console { - return (this._console ??= new Console({ client: this.client })) - } - - private _session?: Session - get session(): Session { - return (this._session ??= new Session({ client: this.client })) - } - - private _resource?: Resource - get resource(): Resource { - return (this._resource ??= new Resource({ client: this.client })) - } - - private _projectCopy?: ProjectCopy - get projectCopy(): ProjectCopy { - return (this._projectCopy ??= new ProjectCopy({ client: this.client })) - } - - private _workspace?: Workspace - get workspace(): Workspace { - return (this._workspace ??= new Workspace({ client: this.client })) - } -} - -export class Config extends HeyApiClient { - /** - * Get global configuration - * - * Retrieve the current global OpenCode configuration settings and preferences. - */ - public get(options?: Options) { - return (options?.client ?? this.client).get({ - url: "/global/config", - ...options, - }) - } - - /** - * Update global configuration - * - * Update global OpenCode configuration settings and preferences. - */ - public update( - parameters?: { - config?: Config3 - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ key: "config", map: "body" }] }]) - return (options?.client ?? this.client).patch({ - url: "/global/config", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Global extends HeyApiClient { - /** - * Get health - * - * Get health information about the OpenCode server. - */ - public health(options?: Options) { - return (options?.client ?? this.client).get({ - url: "/global/health", - ...options, - }) - } - - /** - * Get global events - * - * Subscribe to global events from the OpenCode system using server-sent events. - */ - public event(options?: Options) { - return (options?.client ?? this.client).sse.get({ - url: "/global/event", - ...options, - }) - } - - /** - * Dispose instance - * - * Clean up and dispose all OpenCode instances, releasing all resources. - */ - public dispose(options?: Options) { - return (options?.client ?? this.client).post({ - url: "/global/dispose", - ...options, - }) - } - - /** - * Upgrade opencode - * - * Upgrade opencode to the specified version or latest if not specified. - */ - public upgrade( - parameters?: { - target?: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "body", key: "target" }] }]) - return (options?.client ?? this.client).post({ - url: "/global/upgrade", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - private _config?: Config - get config(): Config { - return (this._config ??= new Config({ client: this.client })) - } -} - -export class Event extends HeyApiClient { - /** - * Subscribe to events - * - * Get events - */ - public subscribe( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).sse.get({ - url: "/event", - ...options, - ...params, - }) - } -} - -export class Config2 extends HeyApiClient { - /** - * Get configuration - * - * Retrieve the current OpenCode configuration settings and preferences. - */ - public get( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/config", - ...options, - ...params, - }) - } - - /** - * Update configuration - * - * Update OpenCode configuration settings and preferences. - */ - public update( - parameters?: { - directory?: string - workspace?: string - config?: Config3 - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { key: "config", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).patch({ - url: "/config", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * List config providers - * - * Get a list of all configured AI providers and their default models. - */ - public providers( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/config/providers", - ...options, - ...params, - }) - } -} - -export class Tool extends HeyApiClient { - /** - * List tools - * - * Get a list of available tools with their JSON schema parameters for a specific provider and model combination. - */ - public list( - parameters: { - directory?: string - workspace?: string - provider: string - model: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "query", key: "provider" }, - { in: "query", key: "model" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/experimental/tool", - ...options, - ...params, - }) - } - - /** - * List tool IDs - * - * Get a list of all available tool IDs, including both built-in tools and dynamically registered tools. - */ - public ids( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/experimental/tool/ids", - ...options, - ...params, - }) - } -} - -export class Worktree extends HeyApiClient { - /** - * Remove worktree - * - * Remove a git worktree and delete its branch. - */ - public remove( - parameters?: { - directory?: string - workspace?: string - worktreeRemoveInput?: WorktreeRemoveInput - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { key: "worktreeRemoveInput", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).delete({ - url: "/experimental/worktree", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * List worktrees - * - * List all sandbox worktrees for the current project. - */ - public list( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/experimental/worktree", - ...options, - ...params, - }) - } - - /** - * Create worktree - * - * Create a new git worktree for the current project and run any configured startup scripts. - */ - public create( - parameters?: { - directory?: string - workspace?: string - worktreeCreateInput?: WorktreeCreateInput - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { key: "worktreeCreateInput", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/experimental/worktree", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Reset worktree - * - * Reset a worktree branch to the primary default branch. - */ - public reset( - parameters?: { - directory?: string - workspace?: string - worktreeResetInput?: WorktreeResetInput - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { key: "worktreeResetInput", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/experimental/worktree/reset", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Find extends HeyApiClient { - /** - * Find text - * - * Search for text patterns across files in the project using ripgrep. - */ - public text( - parameters: { - directory?: string - workspace?: string - pattern: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "query", key: "pattern" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/find", - ...options, - ...params, - }) - } - - /** - * Find files - * - * Search for files or directories by name or pattern in the project directory. - */ - public files( - parameters: { - directory?: string - workspace?: string - query: string - dirs?: "true" | "false" - type?: "file" | "directory" - limit?: number - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "query", key: "query" }, - { in: "query", key: "dirs" }, - { in: "query", key: "type" }, - { in: "query", key: "limit" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/find/file", - ...options, - ...params, - }) - } - - /** - * Find symbols - * - * Search for workspace symbols like functions, classes, and variables using LSP. - */ - public symbols( - parameters: { - directory?: string - workspace?: string - query: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "query", key: "query" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/find/symbol", - ...options, - ...params, - }) - } -} - -export class File extends HeyApiClient { - /** - * List files - * - * List files and directories in a specified path. - */ - public list( - parameters: { - directory?: string - workspace?: string - path: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "query", key: "path" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/file", - ...options, - ...params, - }) - } - - /** - * Read file - * - * Read the content of a specified file. - */ - public read( - parameters: { - directory?: string - workspace?: string - path: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "query", key: "path" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/file/content", - ...options, - ...params, - }) - } - - /** - * Get file status - * - * Get the git status of all files in the project. - */ - public status( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/file/status", - ...options, - ...params, - }) - } -} - -export class Instance extends HeyApiClient { - /** - * Dispose instance - * - * Clean up and dispose the current OpenCode instance, releasing all resources. - */ - public dispose( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/instance/dispose", - ...options, - ...params, - }) - } -} - -export class Path extends HeyApiClient { - /** - * Get paths - * - * Retrieve the current working directory and related path information for the OpenCode instance. - */ - public get( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/path", - ...options, - ...params, - }) - } -} - -export class Diff extends HeyApiClient { - /** - * Get raw VCS diff - * - * Retrieve a raw patch for current uncommitted changes. - */ - public raw( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/vcs/diff/raw", - ...options, - ...params, - }) - } -} - -export class Vcs extends HeyApiClient { - /** - * Get VCS info - * - * Retrieve version control system (VCS) information for the current project, such as git branch. - */ - public get( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/vcs", - ...options, - ...params, - }) - } - - /** - * Get VCS status - * - * Retrieve changed files in the current working tree without patches. - */ - public status( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/vcs/status", - ...options, - ...params, - }) - } - - /** - * Get VCS diff - * - * Retrieve the current git diff for the working tree or against the default branch. - */ - public diff( - parameters: { - directory?: string - workspace?: string - mode: "git" | "branch" - context?: number - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "query", key: "mode" }, - { in: "query", key: "context" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/vcs/diff", - ...options, - ...params, - }) - } - - /** - * Apply VCS patch - * - * Apply a raw patch to the current working tree. - */ - public apply( - parameters?: { - directory?: string - workspace?: string - patch?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "patch" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/vcs/apply", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - private _diff?: Diff - get diff2(): Diff { - return (this._diff ??= new Diff({ client: this.client })) - } -} - -export class Command extends HeyApiClient { - /** - * List commands - * - * Get a list of all available commands in the OpenCode system. - */ - public list( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/command", - ...options, - ...params, - }) - } -} - -export class Lsp extends HeyApiClient { - /** - * Get LSP status - * - * Get LSP server status - */ - public status( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/lsp", - ...options, - ...params, - }) - } -} - -export class Formatter extends HeyApiClient { - /** - * Get formatter status - * - * Get formatter status - */ - public status( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/formatter", - ...options, - ...params, - }) - } -} - -export class Auth2 extends HeyApiClient { - /** - * Remove MCP OAuth - * - * Remove OAuth credentials for an MCP server. - */ - public remove( - parameters: { - name: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "name" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).delete({ - url: "/mcp/{name}/auth", - ...options, - ...params, - }) - } - - /** - * Start MCP OAuth - * - * Start OAuth authentication flow for a Model Context Protocol (MCP) server. - */ - public start( - parameters: { - name: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "name" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/mcp/{name}/auth", - ...options, - ...params, - }) - } - - /** - * Complete MCP OAuth - * - * Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code. - */ - public callback( - parameters: { - name: string - directory?: string - workspace?: string - code?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "name" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "code" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/mcp/{name}/auth/callback", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Authenticate MCP OAuth - * - * Start OAuth flow and wait for callback (opens browser). - */ - public authenticate( - parameters: { - name: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "name" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post( - { - url: "/mcp/{name}/auth/authenticate", - ...options, - ...params, - }, - ) - } -} - -export class Mcp extends HeyApiClient { - /** - * Get MCP status - * - * Get the status of all Model Context Protocol (MCP) servers. - */ - public status( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/mcp", - ...options, - ...params, - }) - } - - /** - * Add MCP server - * - * Dynamically add a new Model Context Protocol (MCP) server to the system. - */ - public add( - parameters?: { - directory?: string - workspace?: string - name?: string - config?: McpLocalConfig | McpRemoteConfig - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "name" }, - { in: "body", key: "config" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/mcp", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Connect an MCP server. - */ - public connect( - parameters: { - name: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "name" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/mcp/{name}/connect", - ...options, - ...params, - }) - } - - /** - * Disconnect an MCP server. - */ - public disconnect( - parameters: { - name: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "name" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/mcp/{name}/disconnect", - ...options, - ...params, - }) - } - - private _auth?: Auth2 - get auth(): Auth2 { - return (this._auth ??= new Auth2({ client: this.client })) - } -} - -export class Project extends HeyApiClient { - /** - * List all projects - * - * Get a list of projects that have been opened with OpenCode. - */ - public list( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/project", - ...options, - ...params, - }) - } - - /** - * Get current project - * - * Retrieve the currently active project that OpenCode is working with. - */ - public current( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/project/current", - ...options, - ...params, - }) - } - - /** - * Initialize git repository - * - * Create a git repository for the current project and return the refreshed project info. - */ - public initGit( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/project/git/init", - ...options, - ...params, - }) - } - - /** - * Update project - * - * Update project properties such as name, icon, and commands. - */ - public update( - parameters: { - projectID: string - directory?: string - workspace?: string - name?: string - icon?: ProjectIcon - commands?: ProjectCommands - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "projectID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "name" }, - { in: "body", key: "icon" }, - { in: "body", key: "commands" }, - ], - }, - ], - ) - return (options?.client ?? this.client).patch({ - url: "/project/{projectID}", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * List project directories - * - * List known local absolute directories for a project. - */ - public directories( - parameters: { - projectID: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "projectID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/project/{projectID}/directories", - ...options, - ...params, - }) - } -} - -export class Pty extends HeyApiClient { - /** - * List available shells - * - * Get a list of available shells on the system. - */ - public shells( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/pty/shells", - ...options, - ...params, - }) - } - - /** - * List PTY sessions - * - * Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode. - */ - public list( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/pty", - ...options, - ...params, - }) - } - - /** - * Create PTY session - * - * Create a new pseudo-terminal (PTY) session for running shell commands and processes. - */ - public create( - parameters?: { - directory?: string - workspace?: string - command?: string - args?: Array - cwd?: string - title?: string - env?: { - [key: string]: string - } - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "command" }, - { in: "body", key: "args" }, - { in: "body", key: "cwd" }, - { in: "body", key: "title" }, - { in: "body", key: "env" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/pty", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Remove PTY session - * - * Remove and terminate a specific pseudo-terminal (PTY) session. - */ - public remove( - parameters: { - ptyID: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "ptyID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).delete({ - url: "/pty/{ptyID}", - ...options, - ...params, - }) - } - - /** - * Get PTY session - * - * Retrieve detailed information about a specific pseudo-terminal (PTY) session. - */ - public get( - parameters: { - ptyID: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "ptyID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/pty/{ptyID}", - ...options, - ...params, - }) - } - - /** - * Update PTY session - * - * Update properties of an existing pseudo-terminal (PTY) session. - */ - public update( - parameters: { - ptyID: string - directory?: string - workspace?: string - title?: string - size?: { - rows: number - cols: number - } - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "ptyID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "title" }, - { in: "body", key: "size" }, - ], - }, - ], - ) - return (options?.client ?? this.client).put({ - url: "/pty/{ptyID}", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Create PTY WebSocket token - * - * Create a short-lived ticket for opening a PTY WebSocket connection. - */ - public connectToken( - parameters: { - ptyID: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "ptyID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/pty/{ptyID}/connect-token", - ...options, - ...params, - }) - } - - /** - * Connect to PTY session - * - * Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time. - */ - public connect( - parameters: { - ptyID: string - directory?: string - workspace?: string - cursor?: string - ticket?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "ptyID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "query", key: "cursor" }, - { in: "query", key: "ticket" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/pty/{ptyID}/connect", - ...options, - ...params, - }) - } -} - -export class Question extends HeyApiClient { - /** - * List pending questions - * - * Get all pending question requests across all sessions. - */ - public list( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/question", - ...options, - ...params, - }) - } - - /** - * Reply to question request - * - * Provide answers to a question request from the AI assistant. - */ - public reply( - parameters: { - requestID: string - directory?: string - workspace?: string - answers?: Array - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "requestID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "answers" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/question/{requestID}/reply", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Reject question request - * - * Reject a question request from the AI assistant. - */ - public reject( - parameters: { - requestID: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "requestID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/question/{requestID}/reject", - ...options, - ...params, - }) - } -} - -export class Permission extends HeyApiClient { - /** - * List pending permissions - * - * Get all pending permission requests across all sessions. - */ - public list( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/permission", - ...options, - ...params, - }) - } - - /** - * Respond to permission request - * - * Approve or deny a permission request from the AI assistant. - */ - public reply( - parameters: { - requestID: string - directory?: string - workspace?: string - reply?: "once" | "always" | "reject" - message?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "requestID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "reply" }, - { in: "body", key: "message" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/permission/{requestID}/reply", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Respond to permission - * - * Approve or deny a permission request from the AI assistant. - * - * @deprecated - */ - public respond( - parameters: { - sessionID: string - permissionID: string - directory?: string - workspace?: string - response?: "once" | "always" | "reject" - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "permissionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "response" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/permissions/{permissionID}", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Oauth extends HeyApiClient { - /** - * Start OAuth authorization - * - * Start the OAuth authorization flow for a provider. - */ - public authorize( - parameters: { - providerID: string - directory?: string - workspace?: string - method?: number - inputs?: { - [key: string]: string - } - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "providerID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "method" }, - { in: "body", key: "inputs" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - ProviderOauthAuthorizeResponses, - ProviderOauthAuthorizeErrors, - ThrowOnError - >({ - url: "/provider/{providerID}/oauth/authorize", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Handle OAuth callback - * - * Handle the OAuth callback from a provider after user authorization. - */ - public callback( - parameters: { - providerID: string - directory?: string - workspace?: string - method?: number - code?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "providerID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "method" }, - { in: "body", key: "code" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - ProviderOauthCallbackResponses, - ProviderOauthCallbackErrors, - ThrowOnError - >({ - url: "/provider/{providerID}/oauth/callback", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Provider extends HeyApiClient { - /** - * List providers - * - * Get a list of all available AI providers, including both available and connected ones. - */ - public list( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/provider", - ...options, - ...params, - }) - } - - /** - * Get provider auth methods - * - * Retrieve available authentication methods for all AI providers. - */ - public auth( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/provider/auth", - ...options, - ...params, - }) - } - - private _oauth?: Oauth - get oauth(): Oauth { - return (this._oauth ??= new Oauth({ client: this.client })) - } -} - -export class Session2 extends HeyApiClient { - /** - * List sessions - * - * Get a list of all OpenCode sessions, sorted by most recently updated. - */ - public list( - parameters?: { - directory?: string - workspace?: string - scope?: "project" - path?: string - roots?: boolean | "true" | "false" - start?: number - search?: string - limit?: number - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "query", key: "scope" }, - { in: "query", key: "path" }, - { in: "query", key: "roots" }, - { in: "query", key: "start" }, - { in: "query", key: "search" }, - { in: "query", key: "limit" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/session", - ...options, - ...params, - }) - } - - /** - * Create session - * - * Create a new OpenCode session for interacting with AI assistants and managing conversations. - */ - public create( - parameters?: { - directory?: string - workspace?: string - parentID?: string - title?: string - agent?: string - model?: { - id: string - providerID: string - variant?: string - } - metadata?: { - [key: string]: unknown - } - permission?: PermissionRuleset - workspaceID?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "parentID" }, - { in: "body", key: "title" }, - { in: "body", key: "agent" }, - { in: "body", key: "model" }, - { in: "body", key: "metadata" }, - { in: "body", key: "permission" }, - { in: "body", key: "workspaceID" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/session", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Get session status - * - * Retrieve the current status of all sessions, including active, idle, and completed states. - */ - public status( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/session/status", - ...options, - ...params, - }) - } - - /** - * Delete session - * - * Delete a session and permanently remove all associated data, including messages and history. - */ - public delete( - parameters: { - sessionID: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).delete({ - url: "/session/{sessionID}", - ...options, - ...params, - }) - } - - /** - * Get session - * - * Retrieve detailed information about a specific OpenCode session. - */ - public get( - parameters: { - sessionID: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/session/{sessionID}", - ...options, - ...params, - }) - } - - /** - * Update session - * - * Update properties of an existing session, such as title or other metadata. - */ - public update( - parameters: { - sessionID: string - directory?: string - workspace?: string - title?: string - metadata?: { - [key: string]: unknown - } - permission?: PermissionRuleset - time?: { - archived?: number - } - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "title" }, - { in: "body", key: "metadata" }, - { in: "body", key: "permission" }, - { in: "body", key: "time" }, - ], - }, - ], - ) - return (options?.client ?? this.client).patch({ - url: "/session/{sessionID}", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Get session children - * - * Retrieve all child sessions that were forked from the specified parent session. - */ - public children( - parameters: { - sessionID: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/session/{sessionID}/children", - ...options, - ...params, - }) - } - - /** - * Get message diff - * - * Get the file changes (diff) that resulted from a specific user message in the session. - */ - public diff( - parameters: { - sessionID: string - directory?: string - workspace?: string - messageID?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "query", key: "messageID" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/session/{sessionID}/diff", - ...options, - ...params, - }) - } - - /** - * Get session messages - * - * Retrieve all messages in a session, including user prompts and AI responses. - */ - public messages( - parameters: { - sessionID: string - directory?: string - workspace?: string - limit?: number - before?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "query", key: "limit" }, - { in: "query", key: "before" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/session/{sessionID}/message", - ...options, - ...params, - }) - } - - /** - * Send message - * - * Create and send a new message to a session, streaming the AI response. - */ - public prompt( - parameters: { - sessionID: string - directory?: string - workspace?: string - messageID?: string - model?: { - providerID: string - modelID: string - } - agent?: string - noReply?: boolean - tools?: { - [key: string]: boolean - } - format?: OutputFormat - system?: string - variant?: string - parts?: Array - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "messageID" }, - { in: "body", key: "model" }, - { in: "body", key: "agent" }, - { in: "body", key: "noReply" }, - { in: "body", key: "tools" }, - { in: "body", key: "format" }, - { in: "body", key: "system" }, - { in: "body", key: "variant" }, - { in: "body", key: "parts" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/message", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Delete message - * - * Permanently delete a specific message and all of its parts from a session without reverting file changes. - */ - public deleteMessage( - parameters: { - sessionID: string - messageID: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "messageID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).delete< - SessionDeleteMessageResponses, - SessionDeleteMessageErrors, - ThrowOnError - >({ - url: "/session/{sessionID}/message/{messageID}", - ...options, - ...params, - }) - } - - /** - * Get message - * - * Retrieve a specific message from a session by its message ID. - */ - public message( - parameters: { - sessionID: string - messageID: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "messageID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/session/{sessionID}/message/{messageID}", - ...options, - ...params, - }) - } - - /** - * Fork session - * - * Create a new session by forking an existing session at a specific message point. - */ - public fork( - parameters: { - sessionID: string - directory?: string - workspace?: string - messageID?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "messageID" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/fork", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Abort session - * - * Abort an active session and stop any ongoing AI processing or command execution. - */ - public abort( - parameters: { - sessionID: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/abort", - ...options, - ...params, - }) - } - - /** - * Initialize session - * - * Analyze the current application and create an AGENTS.md file with project-specific agent configurations. - */ - public init( - parameters: { - sessionID: string - directory?: string - workspace?: string - modelID?: string - providerID?: string - messageID?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "modelID" }, - { in: "body", key: "providerID" }, - { in: "body", key: "messageID" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/init", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Unshare session - * - * Remove the shareable link for a session, making it private again. - */ - public unshare( - parameters: { - sessionID: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).delete({ - url: "/session/{sessionID}/share", - ...options, - ...params, - }) - } - - /** - * Share session - * - * Create a shareable link for a session, allowing others to view the conversation. - */ - public share( - parameters: { - sessionID: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/share", - ...options, - ...params, - }) - } - - /** - * Summarize session - * - * Generate a concise summary of the session using AI compaction to preserve key information. - */ - public summarize( - parameters: { - sessionID: string - directory?: string - workspace?: string - providerID?: string - modelID?: string - auto?: boolean - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "providerID" }, - { in: "body", key: "modelID" }, - { in: "body", key: "auto" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/summarize", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Send async message - * - * Create and send a new message to a session asynchronously, starting the session if needed and returning immediately. - */ - public promptAsync( - parameters: { - sessionID: string - directory?: string - workspace?: string - messageID?: string - model?: { - providerID: string - modelID: string - } - agent?: string - noReply?: boolean - tools?: { - [key: string]: boolean - } - format?: OutputFormat - system?: string - variant?: string - parts?: Array - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "messageID" }, - { in: "body", key: "model" }, - { in: "body", key: "agent" }, - { in: "body", key: "noReply" }, - { in: "body", key: "tools" }, - { in: "body", key: "format" }, - { in: "body", key: "system" }, - { in: "body", key: "variant" }, - { in: "body", key: "parts" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/prompt_async", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Send command - * - * Send a new command to a session for execution by the AI assistant. - */ - public command( - parameters: { - sessionID: string - directory?: string - workspace?: string - messageID?: string - agent?: string - model?: string - arguments?: string - command?: string - variant?: string - parts?: Array<{ - id?: string - type: "file" - mime: string - filename?: string - url: string - source?: FilePartSource - }> - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "messageID" }, - { in: "body", key: "agent" }, - { in: "body", key: "model" }, - { in: "body", key: "arguments" }, - { in: "body", key: "command" }, - { in: "body", key: "variant" }, - { in: "body", key: "parts" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/command", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Run shell command - * - * Execute a shell command within the session context and return the AI's response. - */ - public shell( - parameters: { - sessionID: string - directory?: string - workspace?: string - messageID?: string - agent?: string - model?: { - providerID: string - modelID: string - } - command?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "messageID" }, - { in: "body", key: "agent" }, - { in: "body", key: "model" }, - { in: "body", key: "command" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/shell", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Revert message - * - * Revert a specific message in a session, undoing its effects and restoring the previous state. - */ - public revert( - parameters: { - sessionID: string - directory?: string - workspace?: string - messageID?: string - partID?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "messageID" }, - { in: "body", key: "partID" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/revert", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Restore reverted messages - * - * Restore all previously reverted messages in a session. - */ - public unrevert( - parameters: { - sessionID: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/unrevert", - ...options, - ...params, - }) - } -} - -export class Part extends HeyApiClient { - /** - * Delete a part from a message. - */ - public delete( - parameters: { - sessionID: string - messageID: string - partID: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "messageID" }, - { in: "path", key: "partID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).delete({ - url: "/session/{sessionID}/message/{messageID}/part/{partID}", - ...options, - ...params, - }) - } - - /** - * Update a part in a message. - */ - public update( - parameters: { - sessionID: string - messageID: string - partID: string - directory?: string - workspace?: string - part?: Part2 - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "messageID" }, - { in: "path", key: "partID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { key: "part", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).patch({ - url: "/session/{sessionID}/message/{messageID}/part/{partID}", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class History extends HeyApiClient { - /** - * List sync events - * - * List sync events for all aggregates. Keys are aggregate IDs the client already knows about, values are the last known sequence ID. Events with seq > value are returned for those aggregates. Aggregates not listed in the input get their full history. - */ - public list( - parameters?: { - directory?: string - workspace?: string - body?: { - [key: string]: number - } - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { key: "body", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/sync/history", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Sync extends HeyApiClient { - /** - * Start workspace sync - * - * Start sync loops for workspaces in the current project that have active sessions. - */ - public start( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/sync/start", - ...options, - ...params, - }) - } - - /** - * Replay sync events - * - * Validate and replay a complete sync event history. - */ - public replay( - parameters?: { - query_directory?: string - workspace?: string - body_directory?: string - events?: Array<{ - id: string - aggregateID: string - seq: number - type: string - data: { - [key: string]: unknown - } - }> - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { - in: "query", - key: "query_directory", - map: "directory", - }, - { in: "query", key: "workspace" }, - { - in: "body", - key: "body_directory", - map: "directory", - }, - { in: "body", key: "events" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/sync/replay", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Steal session into workspace - * - * Update a session to belong to the current workspace through the sync event system. - */ - public steal( - parameters?: { - directory?: string - workspace?: string - sessionID?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "sessionID" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/sync/steal", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - private _history?: History - get history(): History { - return (this._history ??= new History({ client: this.client })) - } -} - -export class Control extends HeyApiClient { - /** - * Get next TUI request - * - * Retrieve the next TUI request from the queue for processing. - */ - public next( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/tui/control/next", - ...options, - ...params, - }) - } - - /** - * Submit TUI response - * - * Submit a response to the TUI request queue to complete a pending request. - */ - public response( - parameters?: { - directory?: string - workspace?: string - body?: unknown - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { key: "body", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/tui/control/response", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Tui extends HeyApiClient { - /** - * Append TUI prompt - * - * Append prompt to the TUI. - */ - public appendPrompt( - parameters?: { - directory?: string - workspace?: string - text?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "text" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/tui/append-prompt", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Open help dialog - * - * Open the help dialog in the TUI to display user assistance information. - */ - public openHelp( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/tui/open-help", - ...options, - ...params, - }) - } - - /** - * Open sessions dialog - * - * Open the session dialog. - */ - public openSessions( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/tui/open-sessions", - ...options, - ...params, - }) - } - - /** - * Open themes dialog - * - * Open the theme dialog. - */ - public openThemes( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/tui/open-themes", - ...options, - ...params, - }) - } - - /** - * Open models dialog - * - * Open the model dialog. - */ - public openModels( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/tui/open-models", - ...options, - ...params, - }) - } - - /** - * Submit TUI prompt - * - * Submit the prompt. - */ - public submitPrompt( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/tui/submit-prompt", - ...options, - ...params, - }) - } - - /** - * Clear TUI prompt - * - * Clear the prompt. - */ - public clearPrompt( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/tui/clear-prompt", - ...options, - ...params, - }) - } - - /** - * Execute TUI command - * - * Execute a TUI command. - */ - public executeCommand( - parameters?: { - directory?: string - workspace?: string - command?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "command" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/tui/execute-command", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Show TUI toast - * - * Show a toast notification in the TUI. - */ - public showToast( - parameters?: { - directory?: string - workspace?: string - title?: string - message?: string - variant?: "info" | "success" | "warning" | "error" - duration?: number - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "title" }, - { in: "body", key: "message" }, - { in: "body", key: "variant" }, - { in: "body", key: "duration" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/tui/show-toast", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Publish TUI event - * - * Publish a TUI event. - */ - public publish( - parameters?: { - directory?: string - workspace?: string - body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { key: "body", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/tui/publish", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Select session - * - * Navigate the TUI to display the specified session. - */ - public selectSession( - parameters?: { - directory?: string - workspace?: string - sessionID?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "sessionID" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/tui/select-session", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - private _control?: Control - get control(): Control { - return (this._control ??= new Control({ client: this.client })) - } -} - -export class Health extends HeyApiClient { - /** - * Check server health - * - * Report the owning server process and its application status. - */ - public get(options?: Options) { - return (options?.client ?? this.client).get({ - url: "/api/health", - ...options, - }) - } - - /** - * Stop the managed server - * - * Request graceful shutdown of one exact managed server instance. - */ - public stop( - parameters: { - serviceStopRequest: ServiceStopRequest - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ key: "serviceStopRequest", map: "body" }] }]) - return (options?.client ?? this.client).post({ - url: "/api/service/stop", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Server extends HeyApiClient { - /** - * Get server information - * - * Return the URLs that can be used to connect to this server. - */ - public get(options?: Options) { - return (options?.client ?? this.client).get({ - url: "/api/server", - ...options, - }) - } -} - -export class Location extends HeyApiClient { - /** - * Get location - * - * Resolve the requested location or the server default location. - */ - public get( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/location", - ...options, - ...params, - }) - } -} - -export class Agent extends HeyApiClient { - /** - * List agents - * - * Retrieve currently registered agents. - */ - public list( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/agent", - ...options, - ...params, - }) - } -} - -export class Plugin extends HeyApiClient { - /** - * List plugins - * - * Retrieve currently loaded plugins. - */ - public list( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/plugin", - ...options, - ...params, - }) - } -} - -export class Revert extends HeyApiClient { - /** - * Stage session revert - * - * Stage or move a reversible session boundary and optionally apply its file changes. - */ - public stage( - parameters: { - sessionID: string - messageID?: string - files?: boolean | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "body", key: "messageID" }, - { in: "body", key: "files" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - V2SessionRevertStageResponses, - V2SessionRevertStageErrors, - ThrowOnError - >({ - url: "/api/session/{sessionID}/revert/stage", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Clear staged revert - */ - public clear( - parameters: { - sessionID: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) - return (options?.client ?? this.client).post< - V2SessionRevertClearResponses, - V2SessionRevertClearErrors, - ThrowOnError - >({ - url: "/api/session/{sessionID}/revert/clear", - ...options, - ...params, - }) - } - - /** - * Commit staged revert - */ - public commit( - parameters: { - sessionID: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) - return (options?.client ?? this.client).post< - V2SessionRevertCommitResponses, - V2SessionRevertCommitErrors, - ThrowOnError - >({ - url: "/api/session/{sessionID}/revert/commit", - ...options, - ...params, - }) - } -} - -export class Pending extends HeyApiClient { - /** - * List pending session work - * - * List durable admitted session work not yet visible in projected history, ordered by admission. Includes unpromoted user and synthetic inputs and unhandled compaction barriers. The runner owns consumption; items disappear once promoted or handled. - */ - public list( - parameters: { - sessionID: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) - return (options?.client ?? this.client).get< - V2SessionPendingListResponses, - V2SessionPendingListErrors, - ThrowOnError - >({ - url: "/api/session/{sessionID}/pending", - ...options, - ...params, - }) - } -} - -export class Entry extends HeyApiClient { - /** - * List instruction entries - * - * List API-managed instruction entries attached to the session. - */ - public list( - parameters: { - sessionID: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) - return (options?.client ?? this.client).get< - V2SessionInstructionsEntryListResponses, - V2SessionInstructionsEntryListErrors, - ThrowOnError - >({ - url: "/api/session/{sessionID}/instructions/entries", - ...options, - ...params, - }) - } - - /** - * Remove instruction entry - * - * Remove one instruction entry; the removal is announced to the model at the next step boundary. - */ - public remove( - parameters: { - sessionID: string - key: InstructionEntryKeyV2 - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "key" }, - ], - }, - ], - ) - return (options?.client ?? this.client).delete< - V2SessionInstructionsEntryRemoveResponses, - V2SessionInstructionsEntryRemoveErrors, - ThrowOnError - >({ - url: "/api/session/{sessionID}/instructions/entries/{key}", - ...options, - ...params, - }) - } - - /** - * Put instruction entry - * - * Attach or replace one durable instruction entry. Changes announce as updates at the next step boundary. - */ - public put( - parameters: { - sessionID: string - key: InstructionEntryKeyV2 - value?: unknown - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "key" }, - { in: "body", key: "value" }, - ], - }, - ], - ) - return (options?.client ?? this.client).put< - V2SessionInstructionsEntryPutResponses, - V2SessionInstructionsEntryPutErrors, - ThrowOnError - >({ - url: "/api/session/{sessionID}/instructions/entries/{key}", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Instructions extends HeyApiClient { - private _entry?: Entry - get entry(): Entry { - return (this._entry ??= new Entry({ client: this.client })) - } -} - -export class Form extends HeyApiClient { - /** - * List session forms - * - * Retrieve pending forms for a session. - */ - public list( - parameters: { - sessionID: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/session/{sessionID}/form", - ...options, - ...params, - }) - } - - /** - * Create session form - * - * Create a form for a session. - */ - public create( - parameters: { - sessionID: string - formCreatePayloadV2: FormCreatePayloadV2 - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { key: "formCreatePayloadV2", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post( - { - url: "/api/session/{sessionID}/form", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }, - ) - } - - /** - * Get session form - * - * Retrieve a form for a session. - */ - public get( - parameters: { - sessionID: string - formID: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "formID" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/session/{sessionID}/form/{formID}", - ...options, - ...params, - }) - } - - /** - * Get form state - * - * Retrieve the current state for a form. - */ - public state( - parameters: { - sessionID: string - formID: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "formID" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/session/{sessionID}/form/{formID}/state", - ...options, - ...params, - }) - } - - /** - * Reply to form - * - * Submit an answer to a pending form. - */ - public reply( - parameters: { - sessionID: string - formID: string - formReply: FormReply - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "formID" }, - { key: "formReply", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/api/session/{sessionID}/form/{formID}/reply", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Cancel form - * - * Cancel a pending form. - */ - public cancel( - parameters: { - sessionID: string - formID: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "formID" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post( - { - url: "/api/session/{sessionID}/form/{formID}/cancel", - ...options, - ...params, - }, - ) - } -} - -export class Permission2 extends HeyApiClient { - /** - * List session permission requests - * - * Retrieve pending permission requests owned by a session. - */ - public list( - parameters: { - sessionID: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) - return (options?.client ?? this.client).get< - V2SessionPermissionListResponses, - V2SessionPermissionListErrors, - ThrowOnError - >({ - url: "/api/session/{sessionID}/permission", - ...options, - ...params, - }) - } - - /** - * Create permission request - * - * Evaluate and, when approval is required, create a permission request for a session. - */ - public create( - parameters: { - sessionID: string - id?: string | null - action?: string - resources?: Array - save?: Array - metadata?: { - [key: string]: unknown - } - source?: PermissionV2SourceV2 - agent?: string | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "body", key: "id" }, - { in: "body", key: "action" }, - { in: "body", key: "resources" }, - { in: "body", key: "save" }, - { in: "body", key: "metadata" }, - { in: "body", key: "source" }, - { in: "body", key: "agent" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - V2SessionPermissionCreateResponses, - V2SessionPermissionCreateErrors, - ThrowOnError - >({ - url: "/api/session/{sessionID}/permission", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Get permission request - * - * Retrieve a pending permission request owned by a session. - */ - public get( - parameters: { - sessionID: string - requestID: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "requestID" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get< - V2SessionPermissionGetResponses, - V2SessionPermissionGetErrors, - ThrowOnError - >({ - url: "/api/session/{sessionID}/permission/{requestID}", - ...options, - ...params, - }) - } - - /** - * Reply to pending permission request - * - * Respond to a pending permission request owned by a session. - */ - public reply( - parameters: { - sessionID: string - requestID: string - reply?: PermissionV2Reply - message?: string | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "requestID" }, - { in: "body", key: "reply" }, - { in: "body", key: "message" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - V2SessionPermissionReplyResponses, - V2SessionPermissionReplyErrors, - ThrowOnError - >({ - url: "/api/session/{sessionID}/permission/{requestID}/reply", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Question2 extends HeyApiClient { - /** - * List session question requests - * - * Retrieve pending question requests owned by a session. - */ - public list( - parameters: { - sessionID: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) - return (options?.client ?? this.client).get< - V2SessionQuestionListResponses, - V2SessionQuestionListErrors, - ThrowOnError - >({ - url: "/api/session/{sessionID}/question", - ...options, - ...params, - }) - } - - /** - * Reply to pending question request - * - * Answer a pending question request owned by a session. - */ - public reply( - parameters: { - sessionID: string - requestID: string - questionV2Reply: QuestionV2Reply - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "requestID" }, - { key: "questionV2Reply", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - V2SessionQuestionReplyResponses, - V2SessionQuestionReplyErrors, - ThrowOnError - >({ - url: "/api/session/{sessionID}/question/{requestID}/reply", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Reject pending question request - * - * Reject a pending question request owned by a session. - */ - public reject( - parameters: { - sessionID: string - requestID: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "requestID" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - V2SessionQuestionRejectResponses, - V2SessionQuestionRejectErrors, - ThrowOnError - >({ - url: "/api/session/{sessionID}/question/{requestID}/reject", - ...options, - ...params, - }) - } -} - -export class Session3 extends HeyApiClient { - /** - * List sessions - * - * Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list. - */ - public list( - parameters?: { - workspace?: string | null - limit?: number | null - order?: "asc" | "desc" | null - search?: string | null - parentID?: string | "null" | null - directory?: string | null - project?: string | null - subpath?: string | null - cursor?: string | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "workspace" }, - { in: "query", key: "limit" }, - { in: "query", key: "order" }, - { in: "query", key: "search" }, - { in: "query", key: "parentID" }, - { in: "query", key: "directory" }, - { in: "query", key: "project" }, - { in: "query", key: "subpath" }, - { in: "query", key: "cursor" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/session", - ...options, - ...params, - }) - } - - /** - * Create session - * - * Create a session at the requested location. - */ - public create( - parameters?: { - id?: string | null - agent?: string | null - model?: ModelRef | null - location?: LocationRefV2 | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "body", key: "id" }, - { in: "body", key: "agent" }, - { in: "body", key: "model" }, - { in: "body", key: "location" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/api/session", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * List active sessions - * - * Retrieve foreground Session drains currently owned by this OpenCode process. Sessions absent from the result are inactive. - */ - public active(options?: Options) { - return (options?.client ?? this.client).get({ - url: "/api/session/active", - ...options, - }) - } - - /** - * Delete session - * - * Delete a session and its child sessions. - */ - public remove( - parameters: { - sessionID: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) - return (options?.client ?? this.client).delete({ - url: "/api/session/{sessionID}", - ...options, - ...params, - }) - } - - /** - * Get session - * - * Retrieve a session by ID. - */ - public get( - parameters: { - sessionID: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/session/{sessionID}", - ...options, - ...params, - }) - } - - /** - * Fork session - * - * Create a child session by copying projected history from the parent. When messageID is supplied, copy messages before that boundary. - */ - public fork( - parameters: { - sessionID: string - messageID?: string | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "body", key: "messageID" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/api/session/{sessionID}/fork", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Switch session agent - * - * Switch the agent used by subsequent provider turns. - */ - public switchAgent( - parameters: { - sessionID: string - agent?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "body", key: "agent" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - V2SessionSwitchAgentResponses, - V2SessionSwitchAgentErrors, - ThrowOnError - >({ - url: "/api/session/{sessionID}/agent", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Switch session model - * - * Switch the model used by subsequent provider turns. - */ - public switchModel( - parameters: { - sessionID: string - model?: ModelRef - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "body", key: "model" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - V2SessionSwitchModelResponses, - V2SessionSwitchModelErrors, - ThrowOnError - >({ - url: "/api/session/{sessionID}/model", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Rename session - * - * Update the session title. - */ - public rename( - parameters: { - sessionID: string - title?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "body", key: "title" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/api/session/{sessionID}/rename", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Move session - * - * Move a session to another project directory, optionally transferring local changes. - */ - public move( - parameters: { - sessionID: string - locationRefV2: LocationRefV2 - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { key: "locationRefV2", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/api/session/{sessionID}/move", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Send message - * - * Durably admit one session input and schedule agent-loop execution unless resume is false. - */ - public prompt( - parameters: { - sessionID: string - id?: string | null - text?: string - files?: Array - agents?: Array - metadata?: { - [key: string]: unknown - } - delivery?: "steer" | "queue" | null - resume?: boolean | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "body", key: "id" }, - { in: "body", key: "text" }, - { in: "body", key: "files" }, - { in: "body", key: "agents" }, - { in: "body", key: "metadata" }, - { in: "body", key: "delivery" }, - { in: "body", key: "resume" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/api/session/{sessionID}/prompt", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Run command - * - * Resolve a slash command into prompt input, admit it durably, and schedule execution unless resume is false. - */ - public command( - parameters: { - sessionID: string - id?: string | null - command?: string - arguments?: string | null - agent?: string | null - model?: ModelRef | null - files?: Array - agents?: Array - delivery?: "steer" | "queue" | null - resume?: boolean | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "body", key: "id" }, - { in: "body", key: "command" }, - { in: "body", key: "arguments" }, - { in: "body", key: "agent" }, - { in: "body", key: "model" }, - { in: "body", key: "files" }, - { in: "body", key: "agents" }, - { in: "body", key: "delivery" }, - { in: "body", key: "resume" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/api/session/{sessionID}/command", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Activate skill - * - * Activate a skill for a session by appending a skill message and resuming execution. - */ - public skill( - parameters: { - sessionID: string - id?: string | null - skill?: string - resume?: boolean | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "body", key: "id" }, - { in: "body", key: "skill" }, - { in: "body", key: "resume" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/api/session/{sessionID}/skill", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Add synthetic message - * - * Durably admit synthetic session input and schedule execution unless resume is false. - */ - public synthetic( - parameters: { - sessionID: string - id?: string | null - text?: string - description?: string | null - metadata?: { - [key: string]: unknown - } - delivery?: "steer" | "queue" | null - resume?: boolean | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "body", key: "id" }, - { in: "body", key: "text" }, - { in: "body", key: "description" }, - { in: "body", key: "metadata" }, - { in: "body", key: "delivery" }, - { in: "body", key: "resume" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/api/session/{sessionID}/synthetic", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Run shell command - * - * Execute one shell command in the session's working directory. Emits a shell.started event before execution and a shell.ended event with the merged output after. - */ - public shell( - parameters: { - sessionID: string - id?: string | null - command?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "body", key: "id" }, - { in: "body", key: "command" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/api/session/{sessionID}/shell", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Compact session - * - * Queue a durable session compaction request. - */ - public compact( - parameters: { - sessionID: string - id?: string | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "body", key: "id" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/api/session/{sessionID}/compact", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Wait for session - * - * Wait for a session agent loop to become idle. - */ - public wait( - parameters: { - sessionID: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) - return (options?.client ?? this.client).post({ - url: "/api/session/{sessionID}/wait", - ...options, - ...params, - }) - } - - /** - * Get session context - * - * Retrieve the active context messages for a session (all messages after the last compaction). - */ - public context( - parameters: { - sessionID: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/session/{sessionID}/context", - ...options, - ...params, - }) - } - - /** - * Generate text from session context - * - * Generate transient text from the current session context without mutating session history. - */ - public generate( - parameters: { - sessionID: string - prompt?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "body", key: "prompt" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/api/session/{sessionID}/generate", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Read the session log - * - * Experimental durable session event log. Reads events after an exclusive aggregate sequence and continues with live events when follow=true. - */ - public log( - parameters: { - sessionID: string - after?: number | null - follow?: "true" | "false" | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "after" }, - { in: "query", key: "follow" }, - ], - }, - ], - ) - return (options?.client ?? this.client).sse.get({ - url: "/api/experimental/session/{sessionID}/log", - ...options, - ...params, - }) - } - - /** - * Interrupt session execution - * - * Interrupt active execution owned by this OpenCode process. Idle interruption is a no-op. - */ - public interrupt( - parameters: { - sessionID: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) - return (options?.client ?? this.client).post({ - url: "/api/session/{sessionID}/interrupt", - ...options, - ...params, - }) - } - - /** - * Background blocking session tools - * - * Move active foreground backgroundable tools for this session into background observation. Idle requests are a no-op. - */ - public background( - parameters: { - sessionID: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) - return (options?.client ?? this.client).post( - { - url: "/api/session/{sessionID}/background", - ...options, - ...params, - }, - ) - } - - /** - * Get session message - * - * Retrieve one projected message owned by the Session. - */ - public message( - parameters: { - sessionID: string - messageID: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "messageID" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/session/{sessionID}/message/{messageID}", - ...options, - ...params, - }) - } - - private _revert?: Revert - get revert(): Revert { - return (this._revert ??= new Revert({ client: this.client })) - } - - private _pending?: Pending - get pending(): Pending { - return (this._pending ??= new Pending({ client: this.client })) - } - - private _instructions?: Instructions - get instructions(): Instructions { - return (this._instructions ??= new Instructions({ client: this.client })) - } - - private _form?: Form - get form(): Form { - return (this._form ??= new Form({ client: this.client })) - } - - private _permission?: Permission2 - get permission(): Permission2 { - return (this._permission ??= new Permission2({ client: this.client })) - } - - private _question?: Question2 - get question(): Question2 { - return (this._question ??= new Question2({ client: this.client })) - } -} - -export class Message extends HeyApiClient { - /** - * Get session messages - * - * Retrieve projected messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline. - */ - public list( - parameters: { - sessionID: string - limit?: number | null - order?: "asc" | "desc" | null - cursor?: string | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "limit" }, - { in: "query", key: "order" }, - { in: "query", key: "cursor" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/session/{sessionID}/message", - ...options, - ...params, - }) - } -} - -export class Model extends HeyApiClient { - /** - * List models - * - * Retrieve available models ordered by release date. - */ - public list( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/model", - ...options, - ...params, - }) - } - - /** - * Get default model - * - * Retrieve the model used when a session has no explicit model selection. - */ - public default( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/model/default", - ...options, - ...params, - }) - } -} - -export class Generate extends HeyApiClient { - /** - * Generate text - * - * Run one stateless model generation at the requested location and return the assistant text. Uses the location's default model when none is specified. - */ - public text( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - prompt?: string - model?: ModelRef | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "location" }, - { in: "body", key: "prompt" }, - { in: "body", key: "model" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/api/generate", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Provider2 extends HeyApiClient { - /** - * List providers - * - * Retrieve active AI providers so clients can show provider availability and configuration. - */ - public list( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/provider", - ...options, - ...params, - }) - } - - /** - * Get provider - * - * Retrieve a single AI provider so clients can inspect its availability and endpoint settings. - */ - public get( - parameters: { - providerID: string - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "providerID" }, - { in: "query", key: "location" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/provider/{providerID}", - ...options, - ...params, - }) - } -} - -export class Connect extends HeyApiClient { - /** - * Connect with key - * - * Run a key authentication method and store the resulting credential. - */ - public key( - parameters: { - integrationID: string - location?: { - directory?: string | null - workspace?: string | null - } | null - key?: string - label?: string | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "integrationID" }, - { in: "query", key: "location" }, - { in: "body", key: "key" }, - { in: "body", key: "label" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - V2IntegrationConnectKeyResponses, - V2IntegrationConnectKeyErrors, - ThrowOnError - >({ - url: "/api/integration/{integrationID}/connect/key", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Oauth2 extends HeyApiClient { - /** - * Begin OAuth connection - * - * Start an OAuth attempt and return the authorization details. - */ - public connect( - parameters: { - integrationID: string - location?: { - directory?: string | null - workspace?: string | null - } | null - methodID?: string - inputs?: { - [key: string]: string - } - label?: string | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "integrationID" }, - { in: "query", key: "location" }, - { in: "body", key: "methodID" }, - { in: "body", key: "inputs" }, - { in: "body", key: "label" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - V2IntegrationOauthConnectResponses, - V2IntegrationOauthConnectErrors, - ThrowOnError - >({ - url: "/api/integration/{integrationID}/connect/oauth", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Cancel OAuth connection - * - * Cancel an OAuth attempt and release its resources. - */ - public cancel( - parameters: { - integrationID: string - attemptID: string - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "integrationID" }, - { in: "path", key: "attemptID" }, - { in: "query", key: "location" }, - ], - }, - ], - ) - return (options?.client ?? this.client).delete< - V2IntegrationOauthCancelResponses, - V2IntegrationOauthCancelErrors, - ThrowOnError - >({ - url: "/api/integration/{integrationID}/connect/oauth/{attemptID}", - ...options, - ...params, - }) - } - - /** - * Get OAuth attempt status - * - * Poll the current status of an OAuth attempt. - */ - public status( - parameters: { - integrationID: string - attemptID: string - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "integrationID" }, - { in: "path", key: "attemptID" }, - { in: "query", key: "location" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get< - V2IntegrationOauthStatusResponses, - V2IntegrationOauthStatusErrors, - ThrowOnError - >({ - url: "/api/integration/{integrationID}/connect/oauth/{attemptID}", - ...options, - ...params, - }) - } - - /** - * Complete OAuth connection - * - * Complete a code-based OAuth attempt and store the resulting credential. - */ - public complete( - parameters: { - integrationID: string - attemptID: string - location?: { - directory?: string | null - workspace?: string | null - } | null - code?: string | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "integrationID" }, - { in: "path", key: "attemptID" }, - { in: "query", key: "location" }, - { in: "body", key: "code" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - V2IntegrationOauthCompleteResponses, - V2IntegrationOauthCompleteErrors, - ThrowOnError - >({ - url: "/api/integration/{integrationID}/connect/oauth/{attemptID}/complete", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Command2 extends HeyApiClient { - /** - * Begin command connection - * - * Start a command authentication attempt. - */ - public connect( - parameters: { - integrationID: string - location?: { - directory?: string | null - workspace?: string | null - } | null - methodID?: string - label?: string | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "integrationID" }, - { in: "query", key: "location" }, - { in: "body", key: "methodID" }, - { in: "body", key: "label" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - V2IntegrationCommandConnectResponses, - V2IntegrationCommandConnectErrors, - ThrowOnError - >({ - url: "/api/integration/{integrationID}/connect/command", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Cancel command connection - * - * Cancel a command authentication attempt and terminate its process. - */ - public cancel( - parameters: { - integrationID: string - attemptID: string - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "integrationID" }, - { in: "path", key: "attemptID" }, - { in: "query", key: "location" }, - ], - }, - ], - ) - return (options?.client ?? this.client).delete< - V2IntegrationCommandCancelResponses, - V2IntegrationCommandCancelErrors, - ThrowOnError - >({ - url: "/api/integration/{integrationID}/connect/command/{attemptID}", - ...options, - ...params, - }) - } - - /** - * Get command attempt status - * - * Poll the current status and output of a command authentication attempt. - */ - public status( - parameters: { - integrationID: string - attemptID: string - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "integrationID" }, - { in: "path", key: "attemptID" }, - { in: "query", key: "location" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get< - V2IntegrationCommandStatusResponses, - V2IntegrationCommandStatusErrors, - ThrowOnError - >({ - url: "/api/integration/{integrationID}/connect/command/{attemptID}", - ...options, - ...params, - }) - } -} - -export class Integration extends HeyApiClient { - /** - * List integrations - * - * Retrieve available integrations and their authentication methods. - */ - public list( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/integration", - ...options, - ...params, - }) - } - - /** - * Get integration - * - * Retrieve one integration and its authentication methods. - */ - public get( - parameters: { - integrationID: string - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "integrationID" }, - { in: "query", key: "location" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/integration/{integrationID}", - ...options, - ...params, - }) - } - - private _connect?: Connect - get connect(): Connect { - return (this._connect ??= new Connect({ client: this.client })) - } - - private _oauth?: Oauth2 - get oauth(): Oauth2 { - return (this._oauth ??= new Oauth2({ client: this.client })) - } - - private _command?: Command2 - get command(): Command2 { - return (this._command ??= new Command2({ client: this.client })) - } -} - -export class Wellknown extends HeyApiClient { - /** - * Add wellknown integration - * - * Discover and persist an experimental wellknown integration source. - */ - public add( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - url?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "location" }, - { in: "body", key: "url" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - V2ExperimentalIntegrationWellknownAddResponses, - V2ExperimentalIntegrationWellknownAddErrors, - ThrowOnError - >({ - url: "/api/experimental/integration/wellknown", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Integration2 extends HeyApiClient { - private _wellknown?: Wellknown - get wellknown(): Wellknown { - return (this._wellknown ??= new Wellknown({ client: this.client })) - } -} - -export class Experimental2 extends HeyApiClient { - private _integration?: Integration2 - get integration(): Integration2 { - return (this._integration ??= new Integration2({ client: this.client })) - } -} - -export class Resource2 extends HeyApiClient { - /** - * List MCP resources - * - * Retrieve resources and resource templates from connected MCP servers. - */ - public catalog( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get< - V2McpResourceCatalogResponses, - V2McpResourceCatalogErrors, - ThrowOnError - >({ - url: "/api/mcp/resource", - ...options, - ...params, - }) - } -} - -export class Mcp2 extends HeyApiClient { - /** - * List MCP servers - * - * Retrieve configured MCP servers and their connection status. - */ - public list( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/mcp", - ...options, - ...params, - }) - } - - private _resource?: Resource2 - get resource(): Resource2 { - return (this._resource ??= new Resource2({ client: this.client })) - } -} - -export class Credential extends HeyApiClient { - /** - * Remove credential - * - * Remove a stored integration credential. - */ - public remove( - parameters: { - credentialID: string - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "credentialID" }, - { in: "query", key: "location" }, - ], - }, - ], - ) - return (options?.client ?? this.client).delete( - { - url: "/api/credential/{credentialID}", - ...options, - ...params, - }, - ) - } - - /** - * Update credential - * - * Update a stored credential label. - */ - public update( - parameters: { - credentialID: string - location?: { - directory?: string | null - workspace?: string | null - } | null - label?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "credentialID" }, - { in: "query", key: "location" }, - { in: "body", key: "label" }, - ], - }, - ], - ) - return (options?.client ?? this.client).patch({ - url: "/api/credential/{credentialID}", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Project2 extends HeyApiClient { - /** - * List projects - * - * List known projects. - */ - public list(options?: Options) { - return (options?.client ?? this.client).get({ - url: "/api/project", - ...options, - }) - } - - /** - * Get current project - * - * Resolve the project for the requested location. - */ - public current( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/project/current", - ...options, - ...params, - }) - } - - /** - * List project directories - * - * List known local absolute directories for a project. - */ - public directories( - parameters: { - projectID: string - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "projectID" }, - { in: "query", key: "location" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get< - V2ProjectDirectoriesResponses, - V2ProjectDirectoriesErrors, - ThrowOnError - >({ - url: "/api/project/{projectID}/directories", - ...options, - ...params, - }) - } -} - -export class Request extends HeyApiClient { - /** - * List pending form requests - * - * Retrieve pending forms for a location. - */ - public list( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/form/request", - ...options, - ...params, - }) - } -} - -export class Form2 extends HeyApiClient { - private _request?: Request - get request(): Request { - return (this._request ??= new Request({ client: this.client })) - } -} - -export class Request2 extends HeyApiClient { - /** - * List pending permission requests - * - * Retrieve pending permission requests for a location. - */ - public list( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get< - V2PermissionRequestListResponses, - V2PermissionRequestListErrors, - ThrowOnError - >({ - url: "/api/permission/request", - ...options, - ...params, - }) - } -} - -export class Saved extends HeyApiClient { - /** - * List saved permissions - * - * Retrieve saved permissions, optionally filtered by project. - */ - public list( - parameters?: { - projectID?: string | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "projectID" }] }]) - return (options?.client ?? this.client).get< - V2PermissionSavedListResponses, - V2PermissionSavedListErrors, - ThrowOnError - >({ - url: "/api/permission/saved", - ...options, - ...params, - }) - } - - /** - * Remove saved permission - * - * Remove a saved permission by ID. - */ - public remove( - parameters: { - id: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "id" }] }]) - return (options?.client ?? this.client).delete< - V2PermissionSavedRemoveResponses, - V2PermissionSavedRemoveErrors, - ThrowOnError - >({ - url: "/api/permission/saved/{id}", - ...options, - ...params, - }) - } -} - -export class Permission3 extends HeyApiClient { - private _request?: Request2 - get request(): Request2 { - return (this._request ??= new Request2({ client: this.client })) - } - - private _saved?: Saved - get saved(): Saved { - return (this._saved ??= new Saved({ client: this.client })) - } -} - -export class Fs extends HeyApiClient { - /** - * Read file - * - * Serve one file relative to the requested location. - */ - public read( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/fs/read/*", - ...options, - ...params, - }) - } - - /** - * List directory - * - * List direct children of one directory relative to the requested location. - */ - public list( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - path?: string | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "location" }, - { in: "query", key: "path" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/fs/list", - ...options, - ...params, - }) - } - - /** - * Find files - * - * Find recursively ranked filesystem entries relative to the requested location. - */ - public find( - parameters: { - location?: { - directory?: string | null - workspace?: string | null - } | null - query: string - type?: "file" | "directory" - limit?: string | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "location" }, - { in: "query", key: "query" }, - { in: "query", key: "type" }, - { in: "query", key: "limit" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/fs/find", - ...options, - ...params, - }) - } -} - -export class Command3 extends HeyApiClient { - /** - * List commands - * - * Retrieve currently registered commands. - */ - public list( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/command", - ...options, - ...params, - }) - } -} - -export class Skill extends HeyApiClient { - /** - * List skills - * - * Retrieve currently registered skills. - */ - public list( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/skill", - ...options, - ...params, - }) - } -} - -export class Event2 extends HeyApiClient { - /** - * Subscribe to events - * - * Subscribe to native event payloads for the server. Volatile by contract: a slow consumer overflows and fails the stream, and events during disconnection are missed. - */ - public subscribe(options?: Options) { - return (options?.client ?? this.client).sse.get({ - url: "/api/event", - ...options, - }) - } -} - -export class Connect2 extends HeyApiClient { - /** - * Create PTY WebSocket token - * - * Create a short-lived single-use ticket for opening a PTY WebSocket connection. - */ - public token( - parameters: { - ptyID: string - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "ptyID" }, - { in: "query", key: "location" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/api/pty/{ptyID}/connect-token", - ...options, - ...params, - }) - } -} - -export class Pty2 extends HeyApiClient { - /** - * List PTY sessions - * - * List PTY sessions for a location, including exited sessions retained until removal. - */ - public list( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/pty", - ...options, - ...params, - }) - } - - /** - * Create PTY session - * - * Create a pseudo-terminal session for a location. - */ - public create( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - command?: string - args?: Array - cwd?: string - title?: string - env?: { - [key: string]: string - } - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "location" }, - { in: "body", key: "command" }, - { in: "body", key: "args" }, - { in: "body", key: "cwd" }, - { in: "body", key: "title" }, - { in: "body", key: "env" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/api/pty", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Remove PTY session - * - * Terminate and remove one PTY session. - */ - public remove( - parameters: { - ptyID: string - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "ptyID" }, - { in: "query", key: "location" }, - ], - }, - ], - ) - return (options?.client ?? this.client).delete({ - url: "/api/pty/{ptyID}", - ...options, - ...params, - }) - } - - /** - * Get PTY session - * - * Get one PTY session, including its exit code once exited. - */ - public get( - parameters: { - ptyID: string - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "ptyID" }, - { in: "query", key: "location" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/pty/{ptyID}", - ...options, - ...params, - }) - } - - /** - * Update PTY session - * - * Update the title or viewport size of one PTY session. - */ - public update( - parameters: { - ptyID: string - location?: { - directory?: string | null - workspace?: string | null - } | null - title?: string - size?: { - rows: number - cols: number - } - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "ptyID" }, - { in: "query", key: "location" }, - { in: "body", key: "title" }, - { in: "body", key: "size" }, - ], - }, - ], - ) - return (options?.client ?? this.client).put({ - url: "/api/pty/{ptyID}", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Connect to PTY session - * - * Establish a WebSocket connection streaming PTY output and accepting terminal input. - */ - public connect( - parameters: { - ptyID: string - "location[directory]"?: string - "location[workspace]"?: string - cursor?: string - ticket?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "ptyID" }, - { in: "query", key: "location[directory]" }, - { in: "query", key: "location[workspace]" }, - { in: "query", key: "cursor" }, - { in: "query", key: "ticket" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/pty/{ptyID}/connect", - ...options, - ...params, - }) - } - - private _connect?: Connect2 - get connect2(): Connect2 { - return (this._connect ??= new Connect2({ client: this.client })) - } -} - -export class Shell extends HeyApiClient { - /** - * List running shell commands - * - * List currently running shell commands for a location. Exited commands are not included. - */ - public list( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/shell", - ...options, - ...params, - }) - } - - /** - * Run shell command - * - * Spawn one non-interactive shell command for a location. Combined stdout/stderr is captured to a file pageable via output. - */ - public create( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - command?: string - cwd?: string - timeout?: number - metadata?: { - [key: string]: unknown - } - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "location" }, - { in: "body", key: "command" }, - { in: "body", key: "cwd" }, - { in: "body", key: "timeout" }, - { in: "body", key: "metadata" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/api/shell", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Remove shell command - * - * Terminate and remove one shell command and its retained output. - */ - public remove( - parameters: { - id: string - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "id" }, - { in: "query", key: "location" }, - ], - }, - ], - ) - return (options?.client ?? this.client).delete({ - url: "/api/shell/{id}", - ...options, - ...params, - }) - } - - /** - * Get shell command - * - * Get one shell command, including its status and exit code once exited. - */ - public get( - parameters: { - id: string - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "id" }, - { in: "query", key: "location" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/shell/{id}", - ...options, - ...params, - }) - } - - /** - * Update shell timeout - * - * Replace a running shell command's timeout from now, or clear it with zero. - */ - public timeout( - parameters: { - id: string - location?: { - directory?: string | null - workspace?: string | null - } | null - timeout?: number - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "id" }, - { in: "query", key: "location" }, - { in: "body", key: "timeout" }, - ], - }, - ], - ) - return (options?.client ?? this.client).patch({ - url: "/api/shell/{id}/timeout", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Read shell output - * - * Page through captured combined output by absolute byte cursor. - */ - public output( - parameters: { - id: string - location?: { - directory?: string | null - workspace?: string | null - } | null - cursor?: string - limit?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "id" }, - { in: "query", key: "location" }, - { in: "query", key: "cursor" }, - { in: "query", key: "limit" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/shell/{id}/output", - ...options, - ...params, - }) - } -} - -export class Request3 extends HeyApiClient { - /** - * List pending question requests - * - * Retrieve pending question requests for a location. - */ - public list( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get< - V2QuestionRequestListResponses, - V2QuestionRequestListErrors, - ThrowOnError - >({ - url: "/api/question/request", - ...options, - ...params, - }) - } -} - -export class Question3 extends HeyApiClient { - private _request?: Request3 - get request(): Request3 { - return (this._request ??= new Request3({ client: this.client })) - } -} - -export class Reference extends HeyApiClient { - /** - * List references - * - * List references available in the requested location. - */ - public list( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/reference", - ...options, - ...params, - }) - } -} - -export class ProjectCopy2 extends HeyApiClient { - public remove( - parameters: { - projectID: string - location?: { - directory?: string | null - workspace?: string | null - } | null - directory?: string - force?: boolean - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "projectID" }, - { in: "query", key: "location" }, - { in: "body", key: "directory" }, - { in: "body", key: "force" }, - ], - }, - ], - ) - return (options?.client ?? this.client).delete< - V2ProjectCopyRemoveResponses, - V2ProjectCopyRemoveErrors, - ThrowOnError - >({ - url: "/experimental/project/{projectID}/copy", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - public create( - parameters: { - projectID: string - location?: { - directory?: string | null - workspace?: string | null - } | null - strategy?: string - directory?: string - name?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "projectID" }, - { in: "query", key: "location" }, - { in: "body", key: "strategy" }, - { in: "body", key: "directory" }, - { in: "body", key: "name" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post( - { - url: "/experimental/project/{projectID}/copy", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }, - ) - } - - public refresh( - parameters: { - projectID: string - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "projectID" }, - { in: "query", key: "location" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - V2ProjectCopyRefreshResponses, - V2ProjectCopyRefreshErrors, - ThrowOnError - >({ - url: "/experimental/project/{projectID}/copy/refresh", - ...options, - ...params, - }) - } -} - -export class Vcs2 extends HeyApiClient { - /** - * VCS status - * - * List uncommitted working-copy changes relative to the requested location. - */ - public status( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/vcs/status", - ...options, - ...params, - }) - } - - /** - * VCS diff - * - * Diff the working copy against HEAD (mode git) or the default-branch merge base (mode branch) for the requested location. - */ - public diff( - parameters: { - location?: { - directory?: string | null - workspace?: string | null - } | null - mode: VcsMode - context?: string | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "location" }, - { in: "query", key: "mode" }, - { in: "query", key: "context" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/vcs/diff", - ...options, - ...params, - }) - } -} - -export class Location2 extends HeyApiClient { - /** - * Evict a loaded location - * - * Dispose the requested location's cached services so its next use boots them fresh. - */ - public evict( - parameters?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).delete< - V2DebugLocationEvictResponses, - V2DebugLocationEvictErrors, - ThrowOnError - >({ - url: "/api/debug/location", - ...options, - ...params, - }) - } - - /** - * List loaded locations - * - * List locations currently loaded by the server. - */ - public list(options?: Options) { - return (options?.client ?? this.client).get({ - url: "/api/debug/location", - ...options, - }) - } -} - -export class Debug extends HeyApiClient { - private _location?: Location2 - get location(): Location2 { - return (this._location ??= new Location2({ client: this.client })) - } -} - -export class V2 extends HeyApiClient { - private _health?: Health - get health(): Health { - return (this._health ??= new Health({ client: this.client })) - } - - private _server?: Server - get server(): Server { - return (this._server ??= new Server({ client: this.client })) - } - - private _location?: Location - get location(): Location { - return (this._location ??= new Location({ client: this.client })) - } - - private _agent?: Agent - get agent(): Agent { - return (this._agent ??= new Agent({ client: this.client })) - } - - private _plugin?: Plugin - get plugin(): Plugin { - return (this._plugin ??= new Plugin({ client: this.client })) - } - - private _session?: Session3 - get session(): Session3 { - return (this._session ??= new Session3({ client: this.client })) - } - - private _message?: Message - get message(): Message { - return (this._message ??= new Message({ client: this.client })) - } - - private _model?: Model - get model(): Model { - return (this._model ??= new Model({ client: this.client })) - } - - private _generate?: Generate - get generate(): Generate { - return (this._generate ??= new Generate({ client: this.client })) - } - - private _provider?: Provider2 - get provider(): Provider2 { - return (this._provider ??= new Provider2({ client: this.client })) - } - - private _integration?: Integration - get integration(): Integration { - return (this._integration ??= new Integration({ client: this.client })) - } - - private _experimental?: Experimental2 - get experimental(): Experimental2 { - return (this._experimental ??= new Experimental2({ client: this.client })) - } - - private _mcp?: Mcp2 - get mcp(): Mcp2 { - return (this._mcp ??= new Mcp2({ client: this.client })) - } - - private _credential?: Credential - get credential(): Credential { - return (this._credential ??= new Credential({ client: this.client })) - } - - private _project?: Project2 - get project(): Project2 { - return (this._project ??= new Project2({ client: this.client })) - } - - private _form?: Form2 - get form(): Form2 { - return (this._form ??= new Form2({ client: this.client })) - } - - private _permission?: Permission3 - get permission(): Permission3 { - return (this._permission ??= new Permission3({ client: this.client })) - } - - private _fs?: Fs - get fs(): Fs { - return (this._fs ??= new Fs({ client: this.client })) - } - - private _command?: Command3 - get command(): Command3 { - return (this._command ??= new Command3({ client: this.client })) - } - - private _skill?: Skill - get skill(): Skill { - return (this._skill ??= new Skill({ client: this.client })) - } - - private _event?: Event2 - get event(): Event2 { - return (this._event ??= new Event2({ client: this.client })) - } - - private _pty?: Pty2 - get pty(): Pty2 { - return (this._pty ??= new Pty2({ client: this.client })) - } - - private _shell?: Shell - get shell(): Shell { - return (this._shell ??= new Shell({ client: this.client })) - } - - private _question?: Question3 - get question(): Question3 { - return (this._question ??= new Question3({ client: this.client })) - } - - private _reference?: Reference - get reference(): Reference { - return (this._reference ??= new Reference({ client: this.client })) - } - - private _projectCopy?: ProjectCopy2 - get projectCopy(): ProjectCopy2 { - return (this._projectCopy ??= new ProjectCopy2({ client: this.client })) - } - - private _vcs?: Vcs2 - get vcs(): Vcs2 { - return (this._vcs ??= new Vcs2({ client: this.client })) - } - - private _debug?: Debug - get debug(): Debug { - return (this._debug ??= new Debug({ client: this.client })) - } -} - -export class OpencodeClient extends HeyApiClient { - public static readonly __registry = new HeyApiRegistry() - - constructor(args?: { client?: Client; key?: string }) { - super(args) - OpencodeClient.__registry.set(this, args?.key) - } - - private _auth?: Auth - get auth(): Auth { - return (this._auth ??= new Auth({ client: this.client })) - } - - private _app?: App - get app(): App { - return (this._app ??= new App({ client: this.client })) - } - - private _experimental?: Experimental - get experimental(): Experimental { - return (this._experimental ??= new Experimental({ client: this.client })) - } - - private _global?: Global - get global(): Global { - return (this._global ??= new Global({ client: this.client })) - } - - private _event?: Event - get event(): Event { - return (this._event ??= new Event({ client: this.client })) - } - - private _config?: Config2 - get config(): Config2 { - return (this._config ??= new Config2({ client: this.client })) - } - - private _tool?: Tool - get tool(): Tool { - return (this._tool ??= new Tool({ client: this.client })) - } - - private _worktree?: Worktree - get worktree(): Worktree { - return (this._worktree ??= new Worktree({ client: this.client })) - } - - private _find?: Find - get find(): Find { - return (this._find ??= new Find({ client: this.client })) - } - - private _file?: File - get file(): File { - return (this._file ??= new File({ client: this.client })) - } - - private _instance?: Instance - get instance(): Instance { - return (this._instance ??= new Instance({ client: this.client })) - } - - private _path?: Path - get path(): Path { - return (this._path ??= new Path({ client: this.client })) - } - - private _vcs?: Vcs - get vcs(): Vcs { - return (this._vcs ??= new Vcs({ client: this.client })) - } - - private _command?: Command - get command(): Command { - return (this._command ??= new Command({ client: this.client })) - } - - private _lsp?: Lsp - get lsp(): Lsp { - return (this._lsp ??= new Lsp({ client: this.client })) - } - - private _formatter?: Formatter - get formatter(): Formatter { - return (this._formatter ??= new Formatter({ client: this.client })) - } - - private _mcp?: Mcp - get mcp(): Mcp { - return (this._mcp ??= new Mcp({ client: this.client })) - } - - private _project?: Project - get project(): Project { - return (this._project ??= new Project({ client: this.client })) - } - - private _pty?: Pty - get pty(): Pty { - return (this._pty ??= new Pty({ client: this.client })) - } - - private _question?: Question - get question(): Question { - return (this._question ??= new Question({ client: this.client })) - } - - private _permission?: Permission - get permission(): Permission { - return (this._permission ??= new Permission({ client: this.client })) - } - - private _provider?: Provider - get provider(): Provider { - return (this._provider ??= new Provider({ client: this.client })) - } - - private _session?: Session2 - get session(): Session2 { - return (this._session ??= new Session2({ client: this.client })) - } - - private _part?: Part - get part(): Part { - return (this._part ??= new Part({ client: this.client })) - } - - private _sync?: Sync - get sync(): Sync { - return (this._sync ??= new Sync({ client: this.client })) - } - - private _tui?: Tui - get tui(): Tui { - return (this._tui ??= new Tui({ client: this.client })) - } - - private _v2?: V2 - get v2(): V2 { - return (this._v2 ??= new V2({ client: this.client })) - } -} diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts deleted file mode 100644 index b742ed2f407e..000000000000 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ /dev/null @@ -1,19353 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type ClientOptions = { - baseUrl: `${string}://${string}` | (string & {}) -} - -export type Event = - | EventModelsDevRefreshed - | EventIntegrationUpdated - | EventIntegrationConnectionUpdated - | EventCatalogUpdated - | EventAgentUpdated - | EventSessionCreated - | EventSessionUpdated - | EventSessionDeleted - | EventMessageUpdated - | EventMessageRemoved - | EventMessagePartUpdated - | EventMessagePartRemoved - | EventSessionAgentSelected - | EventSessionModelSelected - | EventSessionMoved - | EventSessionRenamed - | EventSessionUsageUpdated - | EventSessionForked - | EventSessionInputPromoted - | EventSessionInputAdmitted - | EventSessionExecutionStarted - | EventSessionExecutionSucceeded - | EventSessionExecutionFailed - | EventSessionExecutionInterrupted - | EventSessionInstructionsUpdated - | EventSessionSynthetic - | EventSessionSkillActivated - | EventSessionShellStarted - | EventSessionShellEnded - | EventSessionStepStarted - | EventSessionStepEnded - | EventSessionStepFailed - | EventSessionTextStarted - | EventSessionTextDelta - | EventSessionTextEnded - | EventSessionReasoningStarted - | EventSessionReasoningDelta - | EventSessionReasoningEnded - | EventSessionToolInputStarted - | EventSessionToolInputDelta - | EventSessionToolInputEnded - | EventSessionToolCalled - | EventSessionToolProgress - | EventSessionToolSuccess - | EventSessionToolFailed - | EventSessionRetryScheduled - | EventSessionCompactionAdmitted - | EventSessionCompactionStarted - | EventSessionCompactionDelta - | EventSessionCompactionEnded - | EventSessionCompactionFailed - | EventSessionRevertStaged - | EventSessionRevertCleared - | EventSessionRevertCommitted - | EventMessagePartDelta - | EventSessionDiff - | EventSessionError - | EventInstallationUpdated - | EventInstallationUpdateAvailable - | EventFilesystemChanged - | EventReferenceUpdated - | EventPermissionV2Asked - | EventPermissionV2Replied - | EventPluginAdded - | EventPluginUpdated - | EventProjectDirectoriesUpdated - | EventCommandUpdated - | EventConfigUpdated - | EventSkillUpdated - | EventPtyCreated - | EventPtyUpdated - | EventPtyExited - | EventPtyDeleted - | EventShellCreated - | EventShellExited - | EventShellDeleted - | EventQuestionV2Asked - | EventQuestionV2Replied - | EventQuestionV2Rejected - | EventFormCreated - | EventFormReplied - | EventFormCancelled - | EventLspUpdated - | EventPermissionAsked - | EventPermissionReplied - | EventTuiPromptAppend2 - | EventTuiCommandExecute2 - | EventTuiToastShow2 - | EventTuiSessionSelect2 - | EventMcpToolsChanged - | EventMcpResourcesChanged - | EventMcpStatusChanged - | EventCommandExecuted - | EventFileEdited - | EventProjectUpdated - | EventSessionStatus - | EventSessionIdle - | EventQuestionAsked - | EventQuestionReplied - | EventQuestionRejected - | EventSessionCompacted - | EventVcsBranchUpdated - | EventWorkspaceReady - | EventWorkspaceFailed - | EventWorkspaceStatus - | EventWorktreeReady - | EventWorktreeFailed - | EventServerConnected - | EventGlobalDisposed - | EventServerInstanceDisposed - -export type QuestionReplied = { - sessionID: string - requestID: string - answers: Array -} - -export type QuestionRejected = { - sessionID: string - requestID: string -} - -export type OAuth = { - type: "oauth" - refresh: string - access: string - expires: number - accountId?: string - enterpriseUrl?: string -} - -export type ApiAuth = { - type: "api" - key: string - metadata?: { - [key: string]: string - } -} - -export type WellKnownAuth = { - type: "wellknown" - key: string - token: string -} - -export type Auth = OAuth | ApiAuth | WellKnownAuth - -export type EffectHttpApiErrorBadRequest = { - _tag: "BadRequest" -} - -export type InvalidRequestError = { - _tag: "InvalidRequestError" - message: string - kind?: string - field?: string -} - -export type MoveSessionError = { - name: "MoveSessionError" - data: { - message: string - } -} - -export type PermissionAction = "allow" | "deny" | "ask" - -export type PermissionRule = { - permission: string - pattern: string - action: PermissionAction -} - -export type PermissionRuleset = Array - -export type OutputFormatText = { - type: "text" -} - -export type JsonSchema = { - [key: string]: unknown -} - -export type OutputFormatJsonSchema = { - type: "json_schema" - schema: JsonSchema - retryCount?: number -} - -export type OutputFormat = OutputFormatText | OutputFormatJsonSchema - -export type UserMessage = { - id: string - sessionID: string - role: "user" - time: { - created: number - } - format?: OutputFormat - summary?: { - title?: string - body?: string - diffs: Array - } - agent: string - model: { - providerID: string - modelID: string - variant?: string - } - system?: string - tools?: { - [key: string]: boolean - } -} - -export type ProviderAuthError = { - name: "ProviderAuthError" - data: { - providerID: string - message: string - } -} - -export type UnknownError = { - name: "UnknownError" - data: { - message: string - ref?: string - } -} - -export type MessageOutputLengthError = { - name: "MessageOutputLengthError" - data: { - [key: string]: unknown - } -} - -export type MessageAbortedError = { - name: "MessageAbortedError" - data: { - message: string - } -} - -export type StructuredOutputError = { - name: "StructuredOutputError" - data: { - message: string - retries: number - } -} - -export type ContextOverflowError = { - name: "ContextOverflowError" - data: { - message: string - responseBody?: string - } -} - -export type ContentFilterError = { - name: "ContentFilterError" - data: { - message: string - } -} - -export type ApiError = { - name: "APIError" - data: { - message: string - statusCode?: number - isRetryable: boolean - responseHeaders?: { - [key: string]: string - } - responseBody?: string - metadata?: { - [key: string]: string - } - } -} - -export type AssistantMessage = { - id: string - sessionID: string - role: "assistant" - time: { - created: number - completed?: number - } - error?: - | ProviderAuthError - | UnknownError - | MessageOutputLengthError - | MessageAbortedError - | StructuredOutputError - | ContextOverflowError - | ContentFilterError - | ApiError - parentID: string - modelID: string - providerID: string - mode: string - agent: string - path: { - cwd: string - root: string - } - summary?: boolean - cost: number - tokens: { - total?: number - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } - structured?: unknown - variant?: string - finish?: string -} - -export type Message = UserMessage | AssistantMessage - -export type TextPart = { - id: string - sessionID: string - messageID: string - type: "text" - text: string - synthetic?: boolean - ignored?: boolean - time?: { - start: number - end?: number - } - metadata?: { - [key: string]: unknown - } -} - -export type SubtaskPart = { - id: string - sessionID: string - messageID: string - type: "subtask" - prompt: string - description: string - agent: string - model?: { - providerID: string - modelID: string - } - command?: string -} - -export type ReasoningPart = { - id: string - sessionID: string - messageID: string - type: "reasoning" - text: string - metadata?: { - [key: string]: unknown - } - time: { - start: number - end?: number - } -} - -export type FilePartSourceText = { - value: string - start: number - end: number -} - -export type FileSource = { - text: FilePartSourceText - type: "file" - path: string -} - -export type Range = { - start: { - line: number - character: number - } - end: { - line: number - character: number - } -} - -export type SymbolSource = { - text: FilePartSourceText - type: "symbol" - path: string - range: Range - name: string - kind: number -} - -export type ResourceSource = { - text: FilePartSourceText - type: "resource" - clientName: string - uri: string -} - -export type FilePartSource = FileSource | SymbolSource | ResourceSource - -export type FilePart = { - id: string - sessionID: string - messageID: string - type: "file" - mime: string - filename?: string - url: string - source?: FilePartSource -} - -export type ToolStatePending = { - status: "pending" - input: { - [key: string]: unknown - } - raw: string -} - -export type ToolStateRunning = { - status: "running" - input: { - [key: string]: unknown - } - title?: string - metadata?: { - [key: string]: unknown - } - time: { - start: number - } -} - -export type ToolStateCompleted = { - status: "completed" - input: { - [key: string]: unknown - } - output: string - title: string - metadata: { - [key: string]: unknown - } - time: { - start: number - end: number - compacted?: number - } - attachments?: Array -} - -export type ToolStateError = { - status: "error" - input: { - [key: string]: unknown - } - error: string - metadata?: { - [key: string]: unknown - } - time: { - start: number - end: number - } -} - -export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError - -export type ToolPart = { - id: string - sessionID: string - messageID: string - type: "tool" - callID: string - tool: string - state: ToolState - metadata?: { - [key: string]: unknown - } -} - -export type StepStartPart = { - id: string - sessionID: string - messageID: string - type: "step-start" - snapshot?: string -} - -export type StepFinishPart = { - id: string - sessionID: string - messageID: string - type: "step-finish" - reason: string - snapshot?: string - cost: number - tokens: { - total?: number - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } -} - -export type SnapshotPart = { - id: string - sessionID: string - messageID: string - type: "snapshot" - snapshot: string -} - -export type PatchPart = { - id: string - sessionID: string - messageID: string - type: "patch" - hash: string - files: Array -} - -export type AgentPart = { - id: string - sessionID: string - messageID: string - type: "agent" - name: string - source?: { - value: string - start: number - end: number - } -} - -export type RetryPart = { - id: string - sessionID: string - messageID: string - type: "retry" - attempt: number - error: ApiError - time: { - created: number - } -} - -export type CompactionPart = { - id: string - sessionID: string - messageID: string - type: "compaction" - auto: boolean - overflow?: boolean - tail_start_id?: string -} - -export type Part = - | TextPart - | SubtaskPart - | ReasoningPart - | FilePart - | ToolPart - | StepStartPart - | StepFinishPart - | SnapshotPart - | PatchPart - | AgentPart - | RetryPart - | CompactionPart - -export type Pty = { - id: string - title: string - command: string - args: Array - cwd: string - status: "running" | "exited" - pid: number - exitCode?: number -} - -export type SessionStatus = - | { - type: "idle" - } - | { - type: "retry" - attempt: number - message: string - action?: { - reason: string - provider: string - title: string - message: string - label: string - link?: string - } - next: number - } - | { - type: "busy" - } - -export type QuestionOption = { - /** - * Display text (1-5 words, concise) - */ - label: string - /** - * Explanation of choice - */ - description: string -} - -export type QuestionInfo = { - /** - * Complete question - */ - question: string - /** - * Very short label (max 30 chars) - */ - header: string - /** - * Available choices - */ - options: Array - multiple?: boolean - custom?: boolean -} - -export type QuestionTool = { - messageID: string - callID: string -} - -export type QuestionAnswer = Array - -export type GlobalEvent = { - directory: string - project?: string - workspace?: string - payload: - | { - id: string - type: "models-dev.refreshed" - properties: { - [key: string]: unknown - } - } - | { - id: string - type: "integration.updated" - properties: { - [key: string]: unknown - } - } - | { - id: string - type: "integration.connection.updated" - properties: { - integrationID: string - } - } - | { - id: string - type: "catalog.updated" - properties: { - [key: string]: unknown - } - } - | { - id: string - type: "agent.updated" - properties: { - [key: string]: unknown - } - } - | { - id: string - type: "session.created" - properties: { - sessionID: string - info: SessionV1Info - } - } - | { - id: string - type: "session.updated" - properties: { - sessionID: string - info: SessionV1Info - } - } - | { - id: string - type: "session.deleted" - properties: { - sessionID: string - } - } - | { - id: string - type: "message.updated" - properties: { - sessionID: string - info: Message - } - } - | { - id: string - type: "message.removed" - properties: { - sessionID: string - messageID: string - } - } - | { - id: string - type: "message.part.updated" - properties: { - sessionID: string - part: Part - time: number - } - } - | { - id: string - type: "message.part.removed" - properties: { - sessionID: string - messageID: string - partID: string - } - } - | { - id: string - type: "session.agent.selected" - properties: { - sessionID: string - agent: string - } - } - | { - id: string - type: "session.model.selected" - properties: { - sessionID: string - model: ModelRef - } - } - | { - id: string - type: "session.moved" - properties: { - sessionID: string - location: LocationRef - projectID?: string - subpath?: string - } - } - | { - id: string - type: "session.renamed" - properties: { - sessionID: string - title: string - } - } - | { - id: string - type: "session.usage.updated" - properties: { - sessionID: string - cost: MoneyUsd - tokens: TokenUsageInfo - } - } - | { - id: string - type: "session.forked" - properties: { - sessionID: string - parentID: string - parentSeq: number - from?: string - } - } - | { - id: string - type: "session.input.promoted" - properties: { - sessionID: string - inputID: string - } - } - | { - id: string - type: "session.input.admitted" - properties: { - sessionID: string - inputID: string - input: SessionPendingMessage - } - } - | { - id: string - type: "session.execution.started" - properties: { - sessionID: string - } - } - | { - id: string - type: "session.execution.succeeded" - properties: { - sessionID: string - } - } - | { - id: string - type: "session.execution.failed" - properties: { - sessionID: string - error: SessionStructuredError - } - } - | { - id: string - type: "session.execution.interrupted" - properties: { - sessionID: string - reason: "user" | "shutdown" | "superseded" - } - } - | { - id: string - type: "session.instructions.updated" - properties: { - sessionID: string - delta: { - [key: string]: string | "removed" - } - } - } - | { - id: string - type: "session.synthetic" - properties: { - sessionID: string - text: string - description?: string - metadata?: { - [key: string]: unknown - } - } - } - | { - id: string - type: "session.skill.activated" - properties: { - sessionID: string - id: string - name: string - text: string - } - } - | { - id: string - type: "session.shell.started" - properties: { - sessionID: string - shell: ShellInfo - } - } - | { - id: string - type: "session.shell.ended" - properties: { - sessionID: string - shell: ShellInfo - output: { - output: string - cursor: number - size: number - truncated: boolean - } - } - } - | { - id: string - type: "session.step.started" - properties: { - sessionID: string - assistantMessageID: string - agent: string - model: ModelRef - snapshot?: string - } - } - | { - id: string - type: "session.step.ended" - properties: { - sessionID: string - assistantMessageID: string - finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" - cost: MoneyUsd - tokens: TokenUsageInfo - snapshot?: string - files?: Array - } - } - | { - id: string - type: "session.step.failed" - properties: { - sessionID: string - assistantMessageID: string - error: SessionStructuredError - cost?: MoneyUsd - tokens?: TokenUsageInfo - } - } - | { - id: string - type: "session.text.started" - properties: { - sessionID: string - assistantMessageID: string - ordinal: number - } - } - | { - id: string - type: "session.text.delta" - properties: { - sessionID: string - assistantMessageID: string - ordinal: number - delta: string - } - } - | { - id: string - type: "session.text.ended" - properties: { - sessionID: string - assistantMessageID: string - ordinal: number - text: string - } - } - | { - id: string - type: "session.reasoning.started" - properties: { - sessionID: string - assistantMessageID: string - ordinal: number - state?: SessionMessageProviderState - } - } - | { - id: string - type: "session.reasoning.delta" - properties: { - sessionID: string - assistantMessageID: string - ordinal: number - delta: string - } - } - | { - id: string - type: "session.reasoning.ended" - properties: { - sessionID: string - assistantMessageID: string - ordinal: number - text: string - state?: SessionMessageProviderState - } - } - | { - id: string - type: "session.tool.input.started" - properties: { - sessionID: string - assistantMessageID: string - callID: string - name: string - } - } - | { - id: string - type: "session.tool.input.delta" - properties: { - sessionID: string - assistantMessageID: string - callID: string - delta: string - } - } - | { - id: string - type: "session.tool.input.ended" - properties: { - sessionID: string - assistantMessageID: string - callID: string - text: string - } - } - | { - id: string - type: "session.tool.called" - properties: { - sessionID: string - assistantMessageID: string - callID: string - input: { - [key: string]: unknown - } - executed: boolean - state?: SessionMessageProviderState - } - } - | { - id: string - type: "session.tool.progress" - properties: { - sessionID: string - assistantMessageID: string - callID: string - structured: { - [key: string]: unknown - } - content: Array - } - } - | { - id: string - type: "session.tool.success" - properties: { - sessionID: string - assistantMessageID: string - callID: string - structured: { - [key: string]: unknown - } - content: Array - result?: unknown - executed: boolean - resultState?: SessionMessageProviderState - } - } - | { - id: string - type: "session.tool.failed" - properties: { - sessionID: string - assistantMessageID: string - callID: string - error: SessionStructuredError - result?: unknown - executed: boolean - resultState?: SessionMessageProviderState - } - } - | { - id: string - type: "session.retry.scheduled" - properties: { - sessionID: string - assistantMessageID: string - attempt: number - at: number - error: SessionStructuredError - } - } - | { - id: string - type: "session.compaction.admitted" - properties: { - sessionID: string - inputID: string - } - } - | { - id: string - type: "session.compaction.started" - properties: { - sessionID: string - reason: "auto" | "manual" - recent: string - inputID?: string - } - } - | { - id: string - type: "session.compaction.delta" - properties: { - sessionID: string - text: string - } - } - | { - id: string - type: "session.compaction.ended" - properties: { - sessionID: string - reason: "auto" | "manual" - text: string - recent: string - } - } - | { - id: string - type: "session.compaction.failed" - properties: { - sessionID: string - reason: "auto" | "manual" - error: SessionStructuredError - inputID?: string - } - } - | { - id: string - type: "session.revert.staged" - properties: { - sessionID: string - revert: SessionRevert - } - } - | { - id: string - type: "session.revert.cleared" - properties: { - sessionID: string - } - } - | { - id: string - type: "session.revert.committed" - properties: { - sessionID: string - to: string - } - } - | { - id: string - type: "message.part.delta" - properties: { - sessionID: string - messageID: string - partID: string - field: string - delta: string - } - } - | { - id: string - type: "session.diff" - properties: { - sessionID: string - diff: Array - } - } - | { - id: string - type: "session.error" - properties: { - sessionID?: string - error?: - | ProviderAuthError - | UnknownError - | MessageOutputLengthError - | MessageAbortedError - | StructuredOutputError - | ContextOverflowError - | ContentFilterError - | ApiError - } - } - | { - id: string - type: "installation.updated" - properties: { - version: string - } - } - | { - id: string - type: "installation.update-available" - properties: { - version: string - } - } - | { - id: string - type: "filesystem.changed" - properties: { - file: string - event: "add" | "change" | "unlink" - } - } - | { - id: string - type: "reference.updated" - properties: { - [key: string]: unknown - } - } - | { - id: string - type: "permission.v2.asked" - properties: { - id: string - sessionID: string - action: string - resources: Array - save?: Array - metadata?: { - [key: string]: unknown - } - source?: PermissionV2Source - } - } - | { - id: string - type: "permission.v2.replied" - properties: { - sessionID: string - requestID: string - reply: PermissionV2Reply - } - } - | { - id: string - type: "plugin.added" - properties: { - id: string - } - } - | { - id: string - type: "plugin.updated" - properties: { - [key: string]: unknown - } - } - | { - id: string - type: "project.directories.updated" - properties: { - projectID: string - } - } - | { - id: string - type: "command.updated" - properties: { - [key: string]: unknown - } - } - | { - id: string - type: "config.updated" - properties: { - [key: string]: unknown - } - } - | { - id: string - type: "skill.updated" - properties: { - [key: string]: unknown - } - } - | { - id: string - type: "pty.created" - properties: { - info: Pty - } - } - | { - id: string - type: "pty.updated" - properties: { - info: Pty - } - } - | { - id: string - type: "pty.exited" - properties: { - id: string - exitCode: number - } - } - | { - id: string - type: "pty.deleted" - properties: { - id: string - } - } - | { - id: string - type: "shell.created" - properties: { - info: ShellInfo - } - } - | { - id: string - type: "shell.exited" - properties: { - id: string - exit?: number - status: "running" | "exited" | "timeout" | "killed" - } - } - | { - id: string - type: "shell.deleted" - properties: { - id: string - } - } - | { - id: string - type: "question.v2.asked" - properties: { - id: string - sessionID: string - /** - * Questions to ask - */ - questions: Array - tool?: QuestionV2Tool - } - } - | { - id: string - type: "question.v2.replied" - properties: { - sessionID: string - requestID: string - answers: Array - } - } - | { - id: string - type: "question.v2.rejected" - properties: { - sessionID: string - requestID: string - } - } - | { - id: string - type: "form.created" - properties: { - form: FormInfo - } - } - | { - id: string - type: "form.replied" - properties: { - id: string - sessionID: string - answer: FormAnswer - } - } - | { - id: string - type: "form.cancelled" - properties: { - id: string - sessionID: string - } - } - | { - id: string - type: "lsp.updated" - properties: { - [key: string]: unknown - } - } - | { - id: string - type: "permission.asked" - properties: { - id: string - sessionID: string - permission: string - patterns: Array - metadata: { - [key: string]: unknown - } - always: Array - tool?: { - messageID: string - callID: string - } - } - } - | { - id: string - type: "permission.replied" - properties: { - sessionID: string - requestID: string - reply: "once" | "always" | "reject" - } - } - | { - id: string - type: "tui.prompt.append" - properties: { - text: string - } - } - | { - id: string - type: "tui.command.execute" - properties: { - command: - | "session.list" - | "session.new" - | "session.share" - | "session.interrupt" - | "session.background" - | "session.compact" - | "session.page.up" - | "session.page.down" - | "session.line.up" - | "session.line.down" - | "session.half.page.up" - | "session.half.page.down" - | "session.first" - | "session.last" - | "prompt.clear" - | "prompt.submit" - | "agent.cycle" - | string - } - } - | { - id: string - type: "tui.toast.show" - properties: { - title?: string - message: string - variant: "info" | "success" | "warning" | "error" - duration?: number - } - } - | { - id: string - type: "tui.session.select" - properties: { - /** - * Session ID to navigate to - */ - sessionID: string - } - } - | { - id: string - type: "mcp.tools.changed" - properties: { - server: string - } - } - | { - id: string - type: "mcp.resources.changed" - properties: { - server: string - } - } - | { - id: string - type: "mcp.status.changed" - properties: { - server: string - } - } - | { - id: string - type: "command.executed" - properties: { - name: string - sessionID: string - arguments: string - messageID: string - } - } - | { - id: string - type: "file.edited" - properties: { - file: string - } - } - | { - id: string - type: "project.updated" - properties: { - id: string - worktree: string - vcs?: ProjectVcs - name?: string - icon?: ProjectIcon - commands?: ProjectCommands - time: ProjectTime - sandboxes: Array - } - } - | { - id: string - type: "session.status" - properties: { - sessionID: string - status: SessionStatus - } - } - | { - id: string - type: "session.idle" - properties: { - sessionID: string - } - } - | { - id: string - type: "question.asked" - properties: { - id: string - sessionID: string - /** - * Questions to ask - */ - questions: Array - tool?: QuestionTool - } - } - | { - id: string - type: "question.replied" - properties: { - sessionID: string - requestID: string - answers: Array - } - } - | { - id: string - type: "question.rejected" - properties: { - sessionID: string - requestID: string - } - } - | { - id: string - type: "session.compacted" - properties: { - sessionID: string - } - } - | { - id: string - type: "vcs.branch.updated" - properties: { - branch?: string - } - } - | { - id: string - type: "workspace.ready" - properties: { - name: string - } - } - | { - id: string - type: "workspace.failed" - properties: { - message: string - } - } - | { - id: string - type: "workspace.status" - properties: { - workspaceID: string - status: "connected" | "connecting" | "disconnected" | "error" - } - } - | { - id: string - type: "worktree.ready" - properties: { - name: string - branch?: string - } - } - | { - id: string - type: "worktree.failed" - properties: { - message: string - } - } - | { - id: string - type: "server.connected" - properties: { - [key: string]: unknown - } - } - | { - id: string - type: "global.disposed" - properties: { - [key: string]: unknown - } - } - | EventServerInstanceDisposed - | SyncEventSessionCreated - | SyncEventSessionUpdated - | SyncEventSessionDeleted - | SyncEventMessageUpdated - | SyncEventMessageRemoved - | SyncEventMessagePartUpdated - | SyncEventMessagePartRemoved - | SyncEventSessionAgentSelected - | SyncEventSessionModelSelected - | SyncEventSessionMoved - | SyncEventSessionRenamed - | SyncEventSessionForked - | SyncEventSessionInputPromoted - | SyncEventSessionInputAdmitted - | SyncEventSessionExecutionStarted - | SyncEventSessionExecutionSucceeded - | SyncEventSessionExecutionFailed - | SyncEventSessionExecutionInterrupted - | SyncEventSessionInstructionsUpdated - | SyncEventSessionSynthetic - | SyncEventSessionSkillActivated - | SyncEventSessionShellStarted - | SyncEventSessionShellEnded - | SyncEventSessionStepStarted - | SyncEventSessionStepEnded - | SyncEventSessionStepFailed - | SyncEventSessionTextStarted - | SyncEventSessionTextEnded - | SyncEventSessionReasoningStarted - | SyncEventSessionReasoningEnded - | SyncEventSessionToolInputStarted - | SyncEventSessionToolInputEnded - | SyncEventSessionToolCalled - | SyncEventSessionToolProgress - | SyncEventSessionToolSuccess - | SyncEventSessionToolFailed - | SyncEventSessionRetryScheduled - | SyncEventSessionCompactionAdmitted - | SyncEventSessionCompactionStarted - | SyncEventSessionCompactionEnded - | SyncEventSessionCompactionFailed - | SyncEventSessionRevertStaged - | SyncEventSessionRevertCleared - | SyncEventSessionRevertCommitted -} - -/** - * Log level - */ -export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" - -/** - * Server configuration for opencode serve and web commands - */ -export type ServerConfig = { - port?: number - hostname?: string - mdns?: boolean - mdnsDomain?: string - cors?: Array -} - -export type PermissionActionConfig = "ask" | "allow" | "deny" - -export type PermissionObjectConfig = { - [key: string]: PermissionActionConfig -} - -export type PermissionRuleConfig = PermissionActionConfig | PermissionObjectConfig - -export type PermissionConfig = - | PermissionActionConfig - | { - read?: PermissionRuleConfig - edit?: PermissionRuleConfig - glob?: PermissionRuleConfig - grep?: PermissionRuleConfig - list?: PermissionRuleConfig - bash?: PermissionRuleConfig - task?: PermissionRuleConfig - external_directory?: PermissionRuleConfig - question?: PermissionActionConfig - webfetch?: PermissionActionConfig - websearch?: PermissionActionConfig - lsp?: PermissionRuleConfig - doom_loop?: PermissionActionConfig - skill?: PermissionRuleConfig - [key: string]: PermissionRuleConfig | PermissionActionConfig | undefined - } - -export type AgentConfig = { - model?: string - variant?: string - temperature?: number - top_p?: number - prompt?: string - tools?: { - [key: string]: boolean - } - disable?: boolean - description?: string - mode?: "subagent" | "primary" | "all" - hidden?: boolean - options?: { - [key: string]: unknown - } - /** - * Hex color code (e.g., #FF5733) or theme color (e.g., primary) - */ - color?: string | "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info" - steps?: number - maxSteps?: number - permission?: PermissionConfig - [key: string]: - | unknown - | string - | number - | { - [key: string]: boolean - } - | boolean - | "subagent" - | "primary" - | "all" - | { - [key: string]: unknown - } - | string - | "primary" - | "secondary" - | "accent" - | "success" - | "warning" - | "error" - | "info" - | number - | PermissionConfig - | undefined -} - -export type ProviderConfig = { - api?: string - name?: string - env?: Array - id?: string - npm?: string - whitelist?: Array - blacklist?: Array - options?: { - apiKey?: string - baseURL?: string - enterpriseUrl?: string - setCacheKey?: boolean - /** - * Timeout in milliseconds for full requests to this provider. Set to false to disable timeout. - */ - timeout?: number | false - /** - * Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout. - */ - headerTimeout?: number | false - chunkTimeout?: number - [key: string]: unknown | string | boolean | number | false | number | false | number | undefined - } - models?: { - [key: string]: { - id?: string - name?: string - family?: string - release_date?: string - attachment?: boolean - reasoning?: boolean - temperature?: boolean - tool_call?: boolean - interleaved?: - | true - | { - field: "reasoning" | "reasoning_content" | "reasoning_details" - } - cost?: { - input: number - output: number - cache_read?: number - cache_write?: number - context_over_200k?: { - input: number - output: number - cache_read?: number - cache_write?: number - } - } - limit?: { - context: number - input?: number - output: number - } - modalities?: { - input?: Array<"text" | "audio" | "image" | "video" | "pdf"> - output?: Array<"text" | "audio" | "image" | "video" | "pdf"> - } - experimental?: boolean - status?: "alpha" | "beta" | "deprecated" | "active" - provider?: { - npm?: string - api?: string - } - options?: { - [key: string]: unknown - } - headers?: { - [key: string]: string - } - /** - * Variant-specific configuration - */ - variants?: { - [key: string]: { - disabled?: boolean - [key: string]: unknown | boolean | undefined - } - } - } - } -} - -export type McpLocalConfig = { - /** - * Type of MCP server connection - */ - type: "local" - /** - * Command and arguments to run the MCP server - */ - command: Array - cwd?: string - environment?: { - [key: string]: string - } - enabled?: boolean - timeout?: number -} - -export type McpOAuthConfig = { - clientId?: string - clientSecret?: string - scope?: string - callbackPort?: number - redirectUri?: string -} - -export type McpRemoteConfig = { - /** - * Type of MCP server connection - */ - type: "remote" - /** - * URL of the remote MCP server - */ - url: string - enabled?: boolean - headers?: { - [key: string]: string - } - /** - * OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection. - */ - oauth?: McpOAuthConfig | false - timeout?: number -} - -/** - * @deprecated Always uses stretch layout. - */ -export type LayoutConfig = "auto" | "stretch" - -export type ImageAttachmentConfig = { - auto_resize?: boolean - max_width?: number - max_height?: number - max_base64_bytes?: number -} - -export type AttachmentConfig = { - image?: ImageAttachmentConfig -} - -export type Config = { - $schema?: string - shell?: string - logLevel?: LogLevel - server?: ServerConfig - command?: { - [key: string]: { - template: string - description?: string - agent?: string - model?: string - variant?: string - subtask?: boolean - } - } - skills?: { - paths?: Array - urls?: Array - } - references?: { - [key: string]: string | ConfigV2ReferenceGit | ConfigV2ReferenceLocal - } - reference?: { - [key: string]: string | ConfigV2ReferenceGit | ConfigV2ReferenceLocal - } - watcher?: { - ignore?: Array - } - snapshot?: boolean - plugin?: Array< - | string - | [ - string, - { - [key: string]: unknown - }, - ] - > - share?: "manual" | "auto" | "disabled" - autoshare?: boolean - /** - * Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications - */ - autoupdate?: boolean | "notify" - disabled_providers?: Array - enabled_providers?: Array - model?: string - small_model?: string - default_agent?: string - subagent_depth?: number - username?: string - mode?: { - build?: AgentConfig - plan?: AgentConfig - [key: string]: AgentConfig | undefined - } - agent?: { - plan?: AgentConfig - build?: AgentConfig - general?: AgentConfig - explore?: AgentConfig - title?: AgentConfig - summary?: AgentConfig - compaction?: AgentConfig - [key: string]: AgentConfig | undefined - } - provider?: { - [key: string]: ProviderConfig - } - mcp?: { - [key: string]: - | McpLocalConfig - | McpRemoteConfig - | { - enabled: boolean - } - } - /** - * Enable or configure formatters. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides. - */ - formatter?: - | boolean - | { - [key: string]: { - disabled?: boolean - command?: Array - environment?: { - [key: string]: string - } - extensions?: Array - } - } - /** - * Enable or configure LSP servers. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides. - */ - lsp?: - | boolean - | { - [key: string]: - | { - disabled: true - } - | { - command: Array - extensions?: Array - disabled?: boolean - env?: { - [key: string]: string - } - initialization?: { - [key: string]: unknown - } - } - } - instructions?: Array - layout?: LayoutConfig - permission?: PermissionConfig - tools?: { - [key: string]: boolean - } - attachment?: AttachmentConfig - enterprise?: { - url?: string - } - tool_output?: { - max_lines?: number - max_bytes?: number - } - compaction?: { - auto?: boolean - prune?: boolean - tail_turns?: number - preserve_recent_tokens?: number - reserved?: number - } - experimental?: { - disable_paste_summary?: boolean - batch_tool?: boolean - openTelemetry?: boolean - primary_tools?: Array - subagent_depth?: number - continue_loop_on_deny?: boolean - mcp_timeout?: number - } -} - -export type Model = { - id: string - providerID: string - api: { - id: string - url: string - npm: string - } - name: string - family?: string - capabilities: { - temperature: boolean - reasoning: boolean - attachment: boolean - toolcall: boolean - input: { - text: boolean - audio: boolean - image: boolean - video: boolean - pdf: boolean - } - output: { - text: boolean - audio: boolean - image: boolean - video: boolean - pdf: boolean - } - interleaved: - | boolean - | { - field: "reasoning" | "reasoning_content" | "reasoning_details" - } - } - cost: { - input: number - output: number - cache: { - read: number - write: number - } - tiers?: Array<{ - input: number - output: number - cache: { - read: number - write: number - } - tier: { - type: "context" - size: number - } - }> - experimentalOver200K?: { - input: number - output: number - cache: { - read: number - write: number - } - } - } - limit: { - context: number - input?: number - output: number - } - status: "alpha" | "beta" | "deprecated" | "active" - options: { - [key: string]: unknown - } - headers: { - [key: string]: string - } - release_date: string - variants?: { - [key: string]: { - [key: string]: unknown - } - } -} - -export type Provider = { - id: string - name: string - source: "env" | "config" | "custom" | "api" - env: Array - key?: string - options: { - [key: string]: unknown - } - models: { - [key: string]: Model - } -} - -export type ExperimentalCapabilities = { - backgroundSubagents: boolean -} - -export type ConsoleState = { - consoleManagedProviders: Array - activeOrgName?: string - switchableOrgCount: number -} - -export type EffectHttpApiErrorInternalServerError = { - _tag: "InternalServerError" -} - -export type ToolListItem = { - id: string - description: string - parameters: unknown -} - -export type ToolList = Array - -export type ToolIds = Array - -export type WorktreeError = { - name: - | "WorktreeNotGitError" - | "WorktreeNameGenerationFailedError" - | "WorktreeCreateFailedError" - | "WorktreeStartCommandFailedError" - | "WorktreeRemoveFailedError" - | "WorktreeResetFailedError" - | "WorktreeListFailedError" - data: { - message: string - } -} - -export type WorktreeCreateInput = { - name?: string - /** - * Additional startup script to run after the project's start command - */ - startCommand?: string -} - -export type Worktree = { - name: string - branch?: string - directory: string -} - -export type WorktreeRemoveInput = { - directory: string -} - -export type WorktreeResetInput = { - directory: string -} - -export type ProjectSummary = { - id: string - name?: string - worktree: string -} - -export type GlobalSession = { - id: string - slug: string - projectID: string - workspaceID?: string - directory: string - path?: string - parentID?: string - summary?: { - additions: number - deletions: number - files: number - diffs?: Array - } - cost?: number - tokens?: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } - share?: { - url: string - } - title: string - agent?: string - model?: { - id: string - providerID: string - variant?: string - } - version: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - updated: number - compacting?: number - archived?: number - } - permission?: PermissionRuleset - revert?: { - messageID: string - partID?: string - snapshot?: string - diff?: string - } - project: ProjectSummary | null -} - -export type McpResource = { - name: string - uri: string - description?: string - mimeType?: string - client: string -} - -export type Symbol = { - name: string - kind: number - location: { - uri: string - range: Range - } -} - -export type FileNode = { - name: string - path: string - absolute: string - type: "file" | "directory" - ignored: boolean -} - -export type FileContent = { - type: "text" | "binary" - content: string - diff?: string - patch?: { - oldFileName: string - newFileName: string - oldHeader?: string - newHeader?: string - hunks: Array<{ - oldStart: number - oldLines: number - newStart: number - newLines: number - lines: Array - }> - index?: string - } - encoding?: "base64" - mimeType?: string -} - -export type File = { - path: string - added: number - removed: number - status: "added" | "deleted" | "modified" -} - -export type Path = { - home: string - state: string - config: string - worktree: string - directory: string -} - -export type VcsInfo = { - branch?: string - default_branch?: string -} - -export type VcsFileStatus = { - file: string - additions: number - deletions: number - status: "added" | "deleted" | "modified" -} - -export type VcsFileDiff = { - file: string - patch?: string - additions: number - deletions: number - status?: "added" | "deleted" | "modified" -} - -export type VcsApplyError = { - name: "VcsApplyError" - data: { - message: string - reason: "non-git" | "not-clean" - } -} - -export type Command = { - name: string - description?: string - agent?: string - model?: string - source?: "command" | "mcp" | "skill" - template: string - subtask?: boolean - hints: Array -} - -export type Agent = { - name: string - description?: string - mode: "subagent" | "primary" | "all" - native?: boolean - hidden?: boolean - topP?: number - temperature?: number - color?: string - permission: PermissionRuleset - model?: { - modelID: string - providerID: string - } - variant?: string - prompt?: string - options: { - [key: string]: unknown - } - steps?: number -} - -export type LspStatus = { - id: string - name: string - root: string - status: "connected" | "error" -} - -export type FormatterStatus = { - name: string - extensions: Array - enabled: boolean -} - -export type McpStatusConnected = { - status: "connected" -} - -export type McpStatusDisabled = { - status: "disabled" -} - -export type McpStatusFailed = { - status: "failed" - error: string -} - -export type McpStatusNeedsAuth = { - status: "needs_auth" -} - -export type McpStatusNeedsClientRegistration = { - status: "needs_client_registration" - error: string -} - -export type McpStatus = - | McpStatusConnected - | McpStatusDisabled - | McpStatusFailed - | McpStatusNeedsAuth - | McpStatusNeedsClientRegistration - -export type McpUnsupportedOAuthError = { - error: string -} - -export type McpServerNotFoundError = { - _tag: "McpServerNotFoundError" - name: string - message: string -} - -export type Project = { - id: string - worktree: string - vcs?: ProjectVcs - name?: string - icon?: ProjectIcon - commands?: ProjectCommands - time: ProjectTime - sandboxes: Array -} - -export type ProjectNotFoundError = { - _tag: "ProjectNotFoundError" - projectID: string - message: string -} - -export type PtyNotFoundError = { - _tag: "PtyNotFoundError" - ptyID: string - message: string -} - -export type PtyForbiddenError = { - _tag: "PtyForbiddenError" - message: string -} - -export type QuestionRequest = { - id: string - sessionID: string - /** - * Questions to ask - */ - questions: Array - tool?: QuestionTool -} - -export type QuestionNotFoundError = { - _tag: "QuestionNotFoundError" - requestID: string - message: string -} - -export type PermissionRequest = { - id: string - sessionID: string - permission: string - patterns: Array - metadata: { - [key: string]: unknown - } - always: Array - tool?: { - messageID: string - callID: string - } -} - -export type PermissionNotFoundError = { - _tag: "PermissionNotFoundError" - requestID: string - message: string -} - -export type ProviderAuthMethod = { - type: "oauth" | "api" - label: string - prompts?: Array< - | { - type: "text" - key: string - message: string - placeholder?: string - when?: { - key: string - op: "eq" | "neq" - value: string - } - } - | { - type: "select" - key: string - message: string - options: Array<{ - label: string - value: string - hint?: string - }> - when?: { - key: string - op: "eq" | "neq" - value: string - } - } - > -} - -export type ProviderAuthAuthorization = { - url: string - method: "auto" | "code" - instructions: string -} - -export type ProviderAuthError1 = { - name: - | "BadRequest" - | "ProviderAuthOauthMissing" - | "ProviderAuthOauthCodeMissing" - | "ProviderAuthOauthCallbackFailed" - | "ProviderAuthValidationFailed" - data: { - providerID?: string - field?: string - message?: string - kind?: string - } -} - -export type Session = { - id: string - slug: string - projectID: string - workspaceID?: string - directory: string - path?: string - parentID?: string - summary?: { - additions: number - deletions: number - files: number - diffs?: Array - } - cost?: number - tokens?: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } - share?: { - url: string - } - title: string - agent?: string - model?: { - id: string - providerID: string - variant?: string - } - version: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - updated: number - compacting?: number - archived?: number - } - permission?: PermissionRuleset - revert?: { - messageID: string - partID?: string - snapshot?: string - diff?: string - } -} - -export type NotFoundError = { - name: "NotFoundError" - data: { - message: string - } -} - -export type TextPartInput = { - id?: string - type: "text" - text: string - synthetic?: boolean - ignored?: boolean - time?: { - start: number - end?: number - } - metadata?: { - [key: string]: unknown - } -} - -export type FilePartInput = { - id?: string - type: "file" - mime: string - filename?: string - url: string - source?: FilePartSource -} - -export type AgentPartInput = { - id?: string - type: "agent" - name: string - source?: { - value: string - start: number - end: number - } -} - -export type SubtaskPartInput = { - id?: string - type: "subtask" - prompt: string - description: string - agent: string - model?: { - providerID: string - modelID: string - } - command?: string -} - -export type SessionBusyError = { - _tag: "SessionBusyError" - sessionID: string - message: string -} - -export type EventTuiPromptAppend = { - type: "tui.prompt.append" - properties: { - text: string - } -} - -export type EventTuiCommandExecute = { - type: "tui.command.execute" - properties: { - command: - | "session.list" - | "session.new" - | "session.share" - | "session.interrupt" - | "session.background" - | "session.compact" - | "session.page.up" - | "session.page.down" - | "session.line.up" - | "session.line.down" - | "session.half.page.up" - | "session.half.page.down" - | "session.first" - | "session.last" - | "prompt.clear" - | "prompt.submit" - | "agent.cycle" - | string - } -} - -export type EventTuiToastShow = { - type: "tui.toast.show" - properties: { - title?: string - message: string - variant: "info" | "success" | "warning" | "error" - duration?: number - } -} - -export type EventTuiSessionSelect = { - type: "tui.session.select" - properties: { - /** - * Session ID to navigate to - */ - sessionID: string - } -} - -export type Workspace = { - id: string - type: string - name: string - branch?: string | null - directory?: string | null - extra?: unknown | null - projectID: string - timeUsed: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" -} - -export type WorkspaceCreateError = { - name: "WorkspaceCreateError" - data: { - message: string - } -} - -export type WorkspaceWarpError = { - name: "WorkspaceWarpError" - data: { - message: string - } -} - -export type ServiceHealth = { - healthy: true - version: string - pid: number -} - -export type UnauthorizedError = { - _tag: "UnauthorizedError" - message: string -} - -export type ServiceStopRequest = { - instanceID: string -} - -export type ServiceStopResponse = { - accepted: boolean -} - -export type SessionsResponse = { - data: Array - cursor: { - previous?: string - next?: string - } -} - -export type InvalidCursorError = { - _tag: "InvalidCursorError" - message: string -} - -export type SessionActive = { - type: "running" -} - -export type SessionNotFoundError = { - _tag: "SessionNotFoundError" - sessionID: string - message: string -} - -export type MessageNotFoundError = { - _tag: "MessageNotFoundError" - sessionID: string - messageID: string - message: string -} - -export type ConflictError = { - _tag: "ConflictError" - message: string - resource?: string -} - -export type CommandNotFoundError = { - _tag: "CommandNotFoundError" - command: string - message: string -} - -export type CommandEvaluationError = { - _tag: "CommandEvaluationError" - command: string - message: string -} - -export type SkillNotFoundError = { - _tag: "SkillNotFoundError" - skill: string - message: string -} - -export type ServiceUnavailableError = { - _tag: "ServiceUnavailableError" - message: string - service?: string -} - -export type UnknownError1 = { - _tag: "UnknownError" - message: string - ref?: string -} - -export type InstructionEntryValueTooLargeError = { - _tag: "InstructionEntryValueTooLargeError" - actualBytes: number - maxBytes: number - message: string -} - -export type SessionGenerateResponse = { - data: { - text: string - } -} - -export type SessionLogItem = SessionEventDurable | EventLogSynced - -export type SessionLogItemJsonString = string - -export type SessionMessagesResponse = { - data: Array - cursor: { - previous?: string - next?: string - } -} - -export type GenerateTextResponse = { - data: { - text: string - } -} - -export type ProviderNotFoundError = { - _tag: "ProviderNotFoundError" - providerID: string - message: string -} - -export type McpResource2 = { - server: string - name: string - uri: string - description?: string - mimeType?: string -} - -export type FormNotFoundError = { - _tag: "FormNotFoundError" - id: string - message: string -} - -export type FormAlreadySettledError = { - _tag: "FormAlreadySettledError" - id: string - message: string -} - -export type FormInvalidAnswerError = { - _tag: "FormInvalidAnswerError" - id: string - message: string -} - -export type OutputFormat1 = - | { - type: "text" - } - | { - type: "json_schema" - schema: JsonSchema - retryCount?: number - } - -export type SessionStatus2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.status" - location?: LocationRef - data: { - sessionID: string - status: SessionStatus - } -} - -export type QuestionReplied2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "question.replied" - location?: LocationRef - data: { - sessionID: string - requestID: string - answers: Array - } -} - -export type QuestionRejected2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "question.rejected" - location?: LocationRef - data: { - sessionID: string - requestID: string - } -} - -export type V2Event = - | ModelsDevRefreshed - | IntegrationUpdated - | IntegrationConnectionUpdated - | CatalogUpdated - | AgentUpdated - | SessionCreated - | SessionUpdated - | SessionDeleted - | MessageUpdated - | MessageRemoved - | MessagePartUpdated - | MessagePartRemoved - | SessionAgentSelected - | SessionModelSelected - | SessionMoved - | SessionRenamed - | SessionUsageUpdated - | SessionForked - | SessionInputPromoted - | SessionInputAdmitted - | SessionExecutionStarted - | SessionExecutionSucceeded - | SessionExecutionFailed - | SessionExecutionInterrupted - | SessionInstructionsUpdated - | SessionSynthetic - | SessionSkillActivated - | SessionShellStarted - | SessionShellEnded - | SessionStepStarted - | SessionStepEnded - | SessionStepFailed - | SessionTextStarted - | SessionTextDelta - | SessionTextEnded - | SessionReasoningStarted - | SessionReasoningDelta - | SessionReasoningEnded - | SessionToolInputStarted - | SessionToolInputDelta - | SessionToolInputEnded - | SessionToolCalled - | SessionToolProgress - | SessionToolSuccess - | SessionToolFailed - | SessionRetryScheduled - | SessionCompactionAdmitted - | SessionCompactionStarted - | SessionCompactionDelta - | SessionCompactionEnded - | SessionCompactionFailed - | SessionRevertStaged - | SessionRevertCleared - | SessionRevertCommitted - | MessagePartDelta - | SessionDiff - | SessionError - | InstallationUpdated - | InstallationUpdateAvailable - | FilesystemChanged - | ReferenceUpdated - | PermissionV2Asked - | PermissionV2Replied - | PluginAdded - | PluginUpdated - | ProjectDirectoriesUpdated - | CommandUpdated - | ConfigUpdated - | SkillUpdated - | PtyCreated - | PtyUpdated - | PtyExited - | PtyDeleted - | ShellCreated - | ShellExited - | ShellDeleted - | QuestionV2Asked - | QuestionV2Replied - | QuestionV2Rejected - | FormCreated - | FormReplied - | FormCancelled - | LspUpdated - | PermissionAsked - | PermissionReplied - | TuiPromptAppend - | TuiCommandExecute - | TuiToastShow - | TuiSessionSelect - | McpToolsChanged - | McpResourcesChanged - | McpStatusChanged - | CommandExecuted - | FileEdited - | ProjectUpdated - | SessionStatus2 - | SessionIdle - | QuestionAsked - | QuestionReplied2 - | QuestionRejected2 - | SessionCompacted - | VcsBranchUpdated - | WorkspaceReady - | WorkspaceFailed - | WorkspaceStatus - | WorktreeReady - | WorktreeFailed - | ServerConnected - | GlobalDisposed - -export type V2EventJsonString = string - -export type ForbiddenError = { - _tag: "ForbiddenError" - message: string -} - -export type ShellNotFoundError = { - _tag: "ShellNotFoundError" - id: string - message: string -} - -export type ProjectCopyError = { - name: "ProjectCopyError" - data: { - message: string - forceRequired?: boolean - } -} - -export type VcsFileStatus2 = { - file: string - additions: number - deletions: number - status: "added" | "deleted" | "modified" -} - -export type EffectHttpApiErrorForbidden = { - _tag: "Forbidden" -} - -export type EventTuiPromptAppend2 = { - id: string - type: "tui.prompt.append" - properties: { - text: string - } -} - -export type EventTuiCommandExecute2 = { - id: string - type: "tui.command.execute" - properties: { - command: - | "session.list" - | "session.new" - | "session.share" - | "session.interrupt" - | "session.background" - | "session.compact" - | "session.page.up" - | "session.page.down" - | "session.line.up" - | "session.line.down" - | "session.half.page.up" - | "session.half.page.down" - | "session.first" - | "session.last" - | "prompt.clear" - | "prompt.submit" - | "agent.cycle" - | string - } -} - -export type EventTuiToastShow2 = { - id: string - type: "tui.toast.show" - properties: { - title?: string - message: string - variant: "info" | "success" | "warning" | "error" - duration?: number - } -} - -export type EventTuiSessionSelect2 = { - id: string - type: "tui.session.select" - properties: { - /** - * Session ID to navigate to - */ - sessionID: string - } -} - -export type CredentialValue = CredentialOAuth | CredentialKey - -export type IntegrationInputs = { - [key: string]: string -} - -export type IntegrationMethod = - | IntegrationOAuthMethod - | IntegrationCommandMethod - | IntegrationKeyMethod - | IntegrationEnvMethod - -export type IntegrationRef = { - id: string - name: string -} - -export type SkillSource = SkillDirectorySource | SkillUrlSource | SkillEmbeddedSource - -export type MoveSessionDestination = { - directory: string -} - -export type FileDiffLegacyInfo = { - file?: string - patch?: string - additions: number - deletions: number - status?: "added" | "deleted" | "modified" -} - -export type SessionV1Info = { - id: string - slug: string - projectID: string - workspaceID?: string - directory: string - path?: string - parentID?: string - summary?: { - additions: number - deletions: number - files: number - diffs?: Array - } - cost?: number - tokens?: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } - share?: { - url: string - } - title: string - agent?: string - model?: { - id: string - providerID: string - variant?: string - } - version: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - updated: number - compacting?: number - archived?: number - } - permission?: PermissionRuleset - revert?: { - messageID: string - partID?: string - snapshot?: string - diff?: string - } -} - -export type ModelRef = { - id: string - providerID: string - variant?: string -} - -export type LocationRef = { - directory: string - workspaceID?: string -} - -export type MoneyUsd = number - -export type TokenUsageInfo = { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } -} - -export type PromptBase64 = string - -export type PromptFileSource = - | { - type: "inline" - } - | { - type: "uri" - uri: string - } - -export type PromptMention = { - start: number - end: number - text: string -} - -export type PromptFileAttachment = { - data: PromptBase64 - mime: string - source: PromptFileSource - name?: string - description?: string - mention?: PromptMention -} - -export type PromptAgentAttachment = { - name: string - mention?: PromptMention -} - -export type SessionPendingUserData = { - text: string - files?: Array - agents?: Array - metadata?: { - [key: string]: unknown - } -} - -export type SessionPendingUserMessage = { - type: "user" - data: SessionPendingUserData - delivery: "steer" | "queue" -} - -export type SessionPendingSyntheticData = { - text: string - description?: string - metadata?: { - [key: string]: unknown - } -} - -export type SessionPendingSyntheticMessage = { - type: "synthetic" - data: SessionPendingSyntheticData - delivery: "steer" | "queue" -} - -export type SessionPendingMessage = SessionPendingUserMessage | SessionPendingSyntheticMessage - -export type SessionStructuredError = { - type: string - message: string -} - -export type ShellInfo = { - id: string - status: "running" | "exited" | "timeout" | "killed" - command: string - cwd: string - shell: string - file: string - pid?: number - exit?: number - metadata: { - [key: string]: unknown - } - time: { - started: number - completed?: number - } -} - -export type SessionMessageProviderState = { - [key: string]: unknown -} - -export type ToolTextContent = { - type: "text" - text: string -} - -export type ToolFileContent = { - type: "file" - uri: string - mime: string - name?: string -} - -export type LlmToolContent = ToolTextContent | ToolFileContent - -export type FileDiffInfo = { - file: string - patch: string - additions: number - deletions: number - status: "added" | "deleted" | "modified" -} - -export type SessionRevert = { - messageID: string - partID?: string - snapshot?: string - files?: Array -} - -export type PermissionV2Source = { - type: "tool" - messageID: string - callID: string -} - -export type PermissionV2Reply = "once" | "always" | "reject" - -export type QuestionV2Option = { - /** - * Display text (1-5 words, concise) - */ - label: string - /** - * Explanation of choice - */ - description: string -} - -export type QuestionV2Info = { - /** - * Complete question - */ - question: string - /** - * Very short label (max 30 chars) - */ - header: string - /** - * Available choices - */ - options: Array - multiple?: boolean - custom?: boolean -} - -export type QuestionV2Tool = { - messageID: string - callID: string -} - -export type QuestionV2Answer = Array - -export type FormMetadata = { - [key: string]: unknown -} - -export type FormWhen = { - key: string - op: "eq" | "neq" - value: string | number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" | boolean -} - -export type FormOption = { - value: string - label: string - description?: string -} - -export type FormStringField = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "string" - format?: "email" | "uri" | "date" | "date-time" - minLength?: number - maxLength?: number - pattern?: string - placeholder?: string - default?: string - options?: Array - custom?: boolean -} - -export type FormNumberField = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "number" - minimum?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - maximum?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - default?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" -} - -export type FormIntegerField = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "integer" - minimum?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - maximum?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - default?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" -} - -export type FormBooleanField = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "boolean" - default?: boolean -} - -export type FormMultiselectField = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "multiselect" - options: Array - minItems?: number - maxItems?: number - custom?: boolean - default?: Array -} - -export type FormExternalField = { - key: string - type: "external" - url: string - title?: string - description?: string -} - -export type FormField = - | FormStringField - | FormNumberField - | FormIntegerField - | FormBooleanField - | FormMultiselectField - | FormExternalField - -export type FormFields = Array - -export type FormInfo = { - id: string - sessionID: string - title: string - metadata?: FormMetadata - fields: FormFields -} - -export type FormValue = - | string - | number - | "NaN" - | "Infinity" - | "-Infinity" - | "Infinity" - | "-Infinity" - | "NaN" - | boolean - | Array - -export type FormAnswer = { - [key: string]: FormValue -} - -export type ProjectVcs = "git" | "hg" - -export type ProjectIcon = { - url?: string - override?: string - color?: string -} - -export type ProjectCommands = { - /** - * Startup script to run when creating a new workspace (worktree) - */ - start?: string -} - -export type ProjectTime = { - created: number - updated: number - initialized?: number -} - -export type EventServerInstanceDisposed = { - id: string - type: "server.instance.disposed" - properties: { - directory: string - } -} - -export type SyncEventSessionCreated = { - type: "sync" - id: string - syncEvent: { - type: "session.created.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - info: SessionV1Info - } - } -} - -export type SyncEventSessionUpdated = { - type: "sync" - id: string - syncEvent: { - type: "session.updated.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - info: SessionV1Info - } - } -} - -export type SyncEventSessionDeleted = { - type: "sync" - id: string - syncEvent: { - type: "session.deleted.2" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - } - } -} - -export type SyncEventMessageUpdated = { - type: "sync" - id: string - syncEvent: { - type: "message.updated.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - info: Message - } - } -} - -export type SyncEventMessageRemoved = { - type: "sync" - id: string - syncEvent: { - type: "message.removed.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - messageID: string - } - } -} - -export type SyncEventMessagePartUpdated = { - type: "sync" - id: string - syncEvent: { - type: "message.part.updated.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - part: Part - time: number - } - } -} - -export type SyncEventMessagePartRemoved = { - type: "sync" - id: string - syncEvent: { - type: "message.part.removed.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - messageID: string - partID: string - } - } -} - -export type SyncEventSessionAgentSelected = { - type: "sync" - id: string - syncEvent: { - type: "session.agent.selected.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - agent: string - } - } -} - -export type SyncEventSessionModelSelected = { - type: "sync" - id: string - syncEvent: { - type: "session.model.selected.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - model: ModelRef - } - } -} - -export type SyncEventSessionMoved = { - type: "sync" - id: string - syncEvent: { - type: "session.moved.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - location: LocationRef - projectID?: string - subpath?: string - } - } -} - -export type SyncEventSessionRenamed = { - type: "sync" - id: string - syncEvent: { - type: "session.renamed.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - title: string - } - } -} - -export type SyncEventSessionForked = { - type: "sync" - id: string - syncEvent: { - type: "session.forked.2" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - parentID: string - parentSeq: number - from?: string - } - } -} - -export type SyncEventSessionInputPromoted = { - type: "sync" - id: string - syncEvent: { - type: "session.input.promoted.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - inputID: string - } - } -} - -export type SyncEventSessionInputAdmitted = { - type: "sync" - id: string - syncEvent: { - type: "session.input.admitted.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - inputID: string - input: SessionPendingMessage - } - } -} - -export type SyncEventSessionExecutionStarted = { - type: "sync" - id: string - syncEvent: { - type: "session.execution.started.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - } - } -} - -export type SyncEventSessionExecutionSucceeded = { - type: "sync" - id: string - syncEvent: { - type: "session.execution.succeeded.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - } - } -} - -export type SyncEventSessionExecutionFailed = { - type: "sync" - id: string - syncEvent: { - type: "session.execution.failed.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - error: SessionStructuredError - } - } -} - -export type SyncEventSessionExecutionInterrupted = { - type: "sync" - id: string - syncEvent: { - type: "session.execution.interrupted.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - reason: "user" | "shutdown" | "superseded" - } - } -} - -export type SyncEventSessionInstructionsUpdated = { - type: "sync" - id: string - syncEvent: { - type: "session.instructions.updated.2" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - delta: { - [key: string]: string | "removed" - } - } - } -} - -export type SyncEventSessionSynthetic = { - type: "sync" - id: string - syncEvent: { - type: "session.synthetic.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - text: string - description?: string - metadata?: { - [key: string]: unknown - } - } - } -} - -export type SyncEventSessionSkillActivated = { - type: "sync" - id: string - syncEvent: { - type: "session.skill.activated.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - id: string - name: string - text: string - } - } -} - -export type SyncEventSessionShellStarted = { - type: "sync" - id: string - syncEvent: { - type: "session.shell.started.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - shell: ShellInfo - } - } -} - -export type SyncEventSessionShellEnded = { - type: "sync" - id: string - syncEvent: { - type: "session.shell.ended.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - shell: ShellInfo - output: { - output: string - cursor: number - size: number - truncated: boolean - } - } - } -} - -export type SyncEventSessionStepStarted = { - type: "sync" - id: string - syncEvent: { - type: "session.step.started.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - assistantMessageID: string - agent: string - model: ModelRef - snapshot?: string - } - } -} - -export type SyncEventSessionStepEnded = { - type: "sync" - id: string - syncEvent: { - type: "session.step.ended.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - assistantMessageID: string - finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" - cost: MoneyUsd - tokens: TokenUsageInfo - snapshot?: string - files?: Array - } - } -} - -export type SyncEventSessionStepFailed = { - type: "sync" - id: string - syncEvent: { - type: "session.step.failed.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - assistantMessageID: string - error: SessionStructuredError - cost?: MoneyUsd - tokens?: TokenUsageInfo - } - } -} - -export type SyncEventSessionTextStarted = { - type: "sync" - id: string - syncEvent: { - type: "session.text.started.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - assistantMessageID: string - ordinal: number - } - } -} - -export type SyncEventSessionTextEnded = { - type: "sync" - id: string - syncEvent: { - type: "session.text.ended.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - assistantMessageID: string - ordinal: number - text: string - } - } -} - -export type SyncEventSessionReasoningStarted = { - type: "sync" - id: string - syncEvent: { - type: "session.reasoning.started.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - assistantMessageID: string - ordinal: number - state?: SessionMessageProviderState - } - } -} - -export type SyncEventSessionReasoningEnded = { - type: "sync" - id: string - syncEvent: { - type: "session.reasoning.ended.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - assistantMessageID: string - ordinal: number - text: string - state?: SessionMessageProviderState - } - } -} - -export type SyncEventSessionToolInputStarted = { - type: "sync" - id: string - syncEvent: { - type: "session.tool.input.started.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - assistantMessageID: string - callID: string - name: string - } - } -} - -export type SyncEventSessionToolInputEnded = { - type: "sync" - id: string - syncEvent: { - type: "session.tool.input.ended.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - assistantMessageID: string - callID: string - text: string - } - } -} - -export type SyncEventSessionToolCalled = { - type: "sync" - id: string - syncEvent: { - type: "session.tool.called.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - assistantMessageID: string - callID: string - input: { - [key: string]: unknown - } - executed: boolean - state?: SessionMessageProviderState - } - } -} - -export type SyncEventSessionToolProgress = { - type: "sync" - id: string - syncEvent: { - type: "session.tool.progress.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - assistantMessageID: string - callID: string - structured: { - [key: string]: unknown - } - content: Array - } - } -} - -export type SyncEventSessionToolSuccess = { - type: "sync" - id: string - syncEvent: { - type: "session.tool.success.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - assistantMessageID: string - callID: string - structured: { - [key: string]: unknown - } - content: Array - result?: unknown - executed: boolean - resultState?: SessionMessageProviderState - } - } -} - -export type SyncEventSessionToolFailed = { - type: "sync" - id: string - syncEvent: { - type: "session.tool.failed.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - assistantMessageID: string - callID: string - error: SessionStructuredError - result?: unknown - executed: boolean - resultState?: SessionMessageProviderState - } - } -} - -export type SyncEventSessionRetryScheduled = { - type: "sync" - id: string - syncEvent: { - type: "session.retry.scheduled.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - assistantMessageID: string - attempt: number - at: number - error: SessionStructuredError - } - } -} - -export type SyncEventSessionCompactionAdmitted = { - type: "sync" - id: string - syncEvent: { - type: "session.compaction.admitted.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - inputID: string - } - } -} - -export type SyncEventSessionCompactionStarted = { - type: "sync" - id: string - syncEvent: { - type: "session.compaction.started.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - reason: "auto" | "manual" - recent: string - inputID?: string - } - } -} - -export type SyncEventSessionCompactionEnded = { - type: "sync" - id: string - syncEvent: { - type: "session.compaction.ended.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - reason: "auto" | "manual" - text: string - recent: string - } - } -} - -export type SyncEventSessionCompactionFailed = { - type: "sync" - id: string - syncEvent: { - type: "session.compaction.failed.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - reason: "auto" | "manual" - error: SessionStructuredError - inputID?: string - } - } -} - -export type SyncEventSessionRevertStaged = { - type: "sync" - id: string - syncEvent: { - type: "session.revert.staged.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - revert: SessionRevert - } - } -} - -export type SyncEventSessionRevertCleared = { - type: "sync" - id: string - syncEvent: { - type: "session.revert.cleared.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - } - } -} - -export type SyncEventSessionRevertCommitted = { - type: "sync" - id: string - syncEvent: { - type: "session.revert.committed.1" - id: string - seq: number - aggregateID: string - data: { - sessionID: string - to: string - } - } -} - -export type ConfigV2ReferenceGit = { - repository: string - branch?: string - description?: string - hidden?: boolean -} - -export type ConfigV2ReferenceLocal = { - path: string - description?: string - hidden?: boolean -} - -export type ProjectDirectory = { - directory: string - strategy?: string -} - -export type ProjectDirectories = Array - -export type PtyTicketConnectToken = { - ticket: string - expires_in: number -} - -export type WorkspaceEventConnectionStatus = { - workspaceID: string - status: "connected" | "connecting" | "disconnected" | "error" -} - -export type LocationInfo = { - directory: string - workspaceID?: string - project: { - id: string - directory: string - } -} - -export type ProviderSettings = { - [key: string]: unknown -} - -export type ProviderRequest = { - settings: ProviderSettings - headers: { - [key: string]: string - } - body: { - [key: string]: unknown - } -} - -export type AgentColor = string | "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info" - -export type PermissionV2Effect = "allow" | "deny" | "ask" - -export type PermissionV2Rule = { - action: string - resource: string - effect: PermissionV2Effect -} - -export type PermissionV2Ruleset = Array - -export type AgentInfo = { - id: string - name: string - model?: ModelRef - request: ProviderRequest - system?: string - description?: string - mode: "subagent" | "primary" | "all" - hidden: boolean - color?: AgentColor - steps?: number - permissions: PermissionV2Ruleset -} - -export type PluginInfo = { - id: string -} - -export type SessionInfo = { - id: string - parentID?: string - fork?: { - sessionID: string - messageID?: string - } - projectID: string - agent?: string - model?: ModelRef - cost: MoneyUsd - tokens: TokenUsageInfo - time: { - created: number - updated: number - archived?: number - } - title: string - location: LocationRef - subpath?: string - revert?: SessionRevert -} - -export type PromptInputFileAttachment = { - uri: string - name?: string - description?: string - mention?: PromptMention -} - -export type SessionPendingUser = { - admittedSeq: number - id: string - sessionID: string - timeCreated: number - type: "user" - data: SessionPendingUserData - delivery: "steer" | "queue" -} - -export type SessionPendingSynthetic = { - admittedSeq: number - id: string - sessionID: string - timeCreated: number - type: "synthetic" - data: SessionPendingSyntheticData - delivery: "steer" | "queue" -} - -export type SessionPendingCompaction = { - admittedSeq: number - id: string - sessionID: string - timeCreated: number - type: "compaction" -} - -export type SessionMessageAgentSelected = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - type: "agent-switched" - agent: string -} - -export type SessionMessageModelSelected = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - type: "model-switched" - model: ModelRef - previous?: ModelRef -} - -export type SessionMessageUser = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - text: string - files?: Array - agents?: Array - type: "user" -} - -export type SessionMessageSynthetic = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - text: string - description?: string - type: "synthetic" -} - -export type SessionMessageSystem = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - type: "system" - text: string -} - -export type SessionMessageSkill = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - type: "skill" - skill: string - name: string - text: string -} - -export type SessionMessageShell = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - completed?: number - } - type: "shell" - shellID: string - command: string - status: "running" | "exited" | "timeout" | "killed" - exit?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - output?: { - output: string - cursor: number - size: number - truncated: boolean - } -} - -export type SessionMessageAssistantText = { - type: "text" - text: string -} - -export type SessionMessageAssistantReasoning = { - type: "reasoning" - text: string - state?: SessionMessageProviderState - time?: { - created: number - completed?: number - } -} - -export type SessionMessageToolStateStreaming = { - status: "streaming" - input: string -} - -export type SessionMessageToolStateRunning = { - status: "running" - input: { - [key: string]: unknown - } - structured: { - [key: string]: unknown - } - content: Array -} - -export type SessionMessageToolStateCompleted = { - status: "completed" - input: { - [key: string]: unknown - } - content: Array - structured: { - [key: string]: unknown - } - result?: unknown -} - -export type SessionMessageToolStateError = { - status: "error" - input: { - [key: string]: unknown - } - content: Array - structured: { - [key: string]: unknown - } - error: SessionStructuredError - result?: unknown -} - -export type SessionMessageAssistantTool = { - type: "tool" - id: string - name: string - executed?: boolean - providerState?: SessionMessageProviderState - providerResultState?: SessionMessageProviderState - state: - | SessionMessageToolStateStreaming - | SessionMessageToolStateRunning - | SessionMessageToolStateCompleted - | SessionMessageToolStateError - time: { - created: number - ran?: number - completed?: number - } -} - -export type SessionMessageAssistantRetry = { - attempt: number - at: number - error: SessionStructuredError -} - -export type SessionMessageAssistant = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - completed?: number - } - type: "assistant" - agent: string - model: ModelRef - content: Array - snapshot?: { - start?: string - end?: string - files?: Array - } - finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" - cost?: MoneyUsd - tokens?: TokenUsageInfo - error?: SessionStructuredError - retry?: SessionMessageAssistantRetry -} - -export type SessionMessageCompactionRunning = { - type: "compaction" - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - status: "running" - reason: "auto" | "manual" - summary: string - recent: string -} - -export type SessionMessageCompactionCompleted = { - type: "compaction" - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - status: "completed" - reason: "auto" | "manual" - summary: string - recent: string -} - -export type SessionMessageCompactionFailed = { - type: "compaction" - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - status: "failed" - reason: "auto" | "manual" - error: SessionStructuredError -} - -export type SessionMessageCompaction = - | SessionMessageCompactionRunning - | SessionMessageCompactionCompleted - | SessionMessageCompactionFailed - -export type SessionMessageInfo = - | SessionMessageAgentSelected - | SessionMessageModelSelected - | SessionMessageUser - | SessionMessageSynthetic - | SessionMessageSystem - | SessionMessageSkill - | SessionMessageShell - | SessionMessageAssistant - | SessionMessageCompaction - -export type SessionPendingInfo = SessionPendingUser | SessionPendingSynthetic | SessionPendingCompaction - -export type InstructionEntryKey = string - -export type InstructionEntryInfo = { - key: InstructionEntryKey - value: unknown -} - -export type SessionAgentSelected = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.agent.selected" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - agent: string - } -} - -export type SessionModelSelected = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.model.selected" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - model: ModelRef - } -} - -export type SessionMoved = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.moved" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - location: LocationRef - projectID?: string - subpath?: string - } -} - -export type SessionRenamed = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.renamed" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - title: string - } -} - -export type SessionDeleted = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.deleted" - durable: { - aggregateID: string - seq: number - version: 2 - } - location?: LocationRef - data: { - sessionID: string - } -} - -export type SessionForked = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.forked" - durable: { - aggregateID: string - seq: number - version: 2 - } - location?: LocationRef - data: { - sessionID: string - parentID: string - parentSeq: number - from?: string - } -} - -export type SessionInputPromoted = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.input.promoted" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - inputID: string - } -} - -export type SessionInputAdmitted = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.input.admitted" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - inputID: string - input: SessionPendingMessage - } -} - -export type SessionExecutionStarted = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.execution.started" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - } -} - -export type SessionExecutionSucceeded = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.execution.succeeded" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - } -} - -export type SessionExecutionFailed = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.execution.failed" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - error: SessionStructuredError - } -} - -export type SessionExecutionInterrupted = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.execution.interrupted" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - reason: "user" | "shutdown" | "superseded" - } -} - -export type SessionInstructionsUpdated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.instructions.updated" - durable: { - aggregateID: string - seq: number - version: 2 - } - location?: LocationRef - data: { - sessionID: string - delta: { - [key: string]: string | "removed" - } - } -} - -export type SessionSynthetic = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.synthetic" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - text: string - description?: string - metadata?: { - [key: string]: unknown - } - } -} - -export type SessionSkillActivated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.skill.activated" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - id: string - name: string - text: string - } -} - -export type SessionShellStarted = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.shell.started" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - shell: ShellInfo - } -} - -export type SessionShellEnded = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.shell.ended" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - shell: ShellInfo - output: { - output: string - cursor: number - size: number - truncated: boolean - } - } -} - -export type SessionStepStarted = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.step.started" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - agent: string - model: ModelRef - snapshot?: string - } -} - -export type SessionStepEnded = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.step.ended" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" - cost: MoneyUsd - tokens: TokenUsageInfo - snapshot?: string - files?: Array - } -} - -export type SessionStepFailed = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.step.failed" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - error: SessionStructuredError - cost?: MoneyUsd - tokens?: TokenUsageInfo - } -} - -export type SessionTextStarted = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.text.started" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - ordinal: number - } -} - -export type SessionTextEnded = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.text.ended" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - ordinal: number - text: string - } -} - -export type SessionReasoningStarted = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.reasoning.started" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - ordinal: number - state?: SessionMessageProviderState - } -} - -export type SessionReasoningEnded = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.reasoning.ended" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - ordinal: number - text: string - state?: SessionMessageProviderState - } -} - -export type SessionToolInputStarted = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.tool.input.started" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - callID: string - name: string - } -} - -export type SessionToolInputEnded = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.tool.input.ended" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - callID: string - text: string - } -} - -export type SessionToolCalled = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.tool.called" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - callID: string - input: { - [key: string]: unknown - } - executed: boolean - state?: SessionMessageProviderState - } -} - -export type SessionToolProgress = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.tool.progress" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - callID: string - structured: { - [key: string]: unknown - } - content: Array - } -} - -export type SessionToolSuccess = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.tool.success" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - callID: string - structured: { - [key: string]: unknown - } - content: Array - result?: unknown - executed: boolean - resultState?: SessionMessageProviderState - } -} - -export type SessionToolFailed = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.tool.failed" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - callID: string - error: SessionStructuredError - result?: unknown - executed: boolean - resultState?: SessionMessageProviderState - } -} - -export type SessionRetryScheduled = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.retry.scheduled" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - attempt: number - at: number - error: SessionStructuredError - } -} - -export type SessionCompactionAdmitted = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.compaction.admitted" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - inputID: string - } -} - -export type SessionCompactionStarted = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.compaction.started" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - reason: "auto" | "manual" - recent: string - inputID?: string - } -} - -export type SessionCompactionEnded = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.compaction.ended" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - reason: "auto" | "manual" - text: string - recent: string - } -} - -export type SessionCompactionFailed = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.compaction.failed" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - reason: "auto" | "manual" - error: SessionStructuredError - inputID?: string - } -} - -export type SessionRevertStaged = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.revert.staged" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - revert: SessionRevert - } -} - -export type SessionRevertCleared = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.revert.cleared" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - } -} - -export type SessionRevertCommitted = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.revert.committed" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - to: string - } -} - -export type SessionUsageRecorded = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.usage.recorded" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - source: "title" | "compaction" - cost: MoneyUsd - tokens: TokenUsageInfo - } -} - -export type SessionEventDurable = - | SessionAgentSelected - | SessionModelSelected - | SessionMoved - | SessionRenamed - | SessionDeleted - | SessionForked - | SessionInputPromoted - | SessionInputAdmitted - | SessionExecutionStarted - | SessionExecutionSucceeded - | SessionExecutionFailed - | SessionExecutionInterrupted - | SessionInstructionsUpdated - | SessionSynthetic - | SessionSkillActivated - | SessionShellStarted - | SessionShellEnded - | SessionStepStarted - | SessionStepEnded - | SessionStepFailed - | SessionTextStarted - | SessionTextEnded - | SessionReasoningStarted - | SessionReasoningEnded - | SessionToolInputStarted - | SessionToolInputEnded - | SessionToolCalled - | SessionToolProgress - | SessionToolSuccess - | SessionToolFailed - | SessionRetryScheduled - | SessionCompactionAdmitted - | SessionCompactionStarted - | SessionCompactionEnded - | SessionCompactionFailed - | SessionRevertStaged - | SessionRevertCleared - | SessionRevertCommitted - | SessionUsageRecorded - -export type EventLogSynced = { - type: "log.synced" - aggregateID: string - seq?: number -} - -export type ModelCapabilities = { - tools: boolean - input: Array - output: Array -} - -export type ModelVariant = { - id: string - settings?: { - [key: string]: unknown - } - headers?: { - [key: string]: string - } - body?: { - [key: string]: unknown - } -} - -export type MoneyUsdPerMillionTokens = number - -export type ModelCost = { - tier?: { - type: "context" - size: number - } - input: MoneyUsdPerMillionTokens - output: MoneyUsdPerMillionTokens - cache: { - read: MoneyUsdPerMillionTokens - write: MoneyUsdPerMillionTokens - } -} - -export type ModelInfo = { - id: string - modelID: string - providerID: string - family?: string - name: string - package?: string - settings?: { - [key: string]: unknown - } - headers?: { - [key: string]: string - } - body?: { - [key: string]: unknown - } - capabilities: ModelCapabilities - variants: Array - time: { - released: number - } - cost: Array - status: "alpha" | "beta" | "deprecated" | "active" - enabled: boolean - limit: { - context: number - input?: number - output: number - } -} - -export type ProviderV2Info = { - id: string - integrationID?: string - name: string - disabled?: boolean - package: string - settings?: { - [key: string]: unknown - } - headers?: { - [key: string]: string - } - body?: { - [key: string]: unknown - } -} - -export type IntegrationWhen = { - key: string - op: "eq" | "neq" - value: string -} - -export type IntegrationTextPrompt = { - type: "text" - key: string - message: string - placeholder?: string - when?: IntegrationWhen -} - -export type IntegrationSelectPrompt = { - type: "select" - key: string - message: string - options: Array<{ - label: string - value: string - hint?: string - }> - when?: IntegrationWhen -} - -export type IntegrationOAuthMethod = { - id: string - type: "oauth" - label: string - prompts?: Array -} - -export type IntegrationCommandMethod = { - id: string - type: "command" - label: string - command: Array -} - -export type IntegrationKeyMethod = { - type: "key" - label?: string -} - -export type IntegrationEnvMethod = { - type: "env" - names: Array -} - -export type ConnectionCredentialInfo = { - type: "credential" - id: string - label: string -} - -export type ConnectionEnvInfo = { - type: "env" - name: string -} - -export type ConnectionInfo = ConnectionCredentialInfo | ConnectionEnvInfo - -export type IntegrationInfo = { - id: string - name: string - methods: Array - connections: Array -} - -export type IntegrationAttempt = { - attemptID: string - url: string - instructions: string - mode: "auto" | "code" - time: { - created: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - expires: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - } -} - -export type IntegrationAttemptStatus = - | { - status: "pending" - time: { - created: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - expires: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - } - } - | { - status: "complete" - time: { - created: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - expires: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - } - } - | { - status: "failed" - message: string - time: { - created: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - expires: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - } - } - | { - status: "expired" - time: { - created: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - expires: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - } - } - -export type IntegrationCommandAttempt = { - attemptID: string - time: { - created: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - expires: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - } -} - -export type IntegrationCommandAttemptStatus = - | { - status: "pending" - message?: string - time: { - created: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - expires: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - } - } - | { - status: "complete" - time: { - created: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - expires: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - } - } - | { - status: "failed" - message: string - time: { - created: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - expires: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - } - } - | { - status: "expired" - time: { - created: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - expires: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - } - } - -export type McpStatusConnected2 = { - status: "connected" -} - -export type McpStatusPending = { - status: "pending" -} - -export type McpStatusDisabled2 = { - status: "disabled" -} - -export type McpStatusFailed2 = { - status: "failed" - error: string -} - -export type McpStatusNeedsAuth2 = { - status: "needs_auth" -} - -export type McpStatusNeedsClientRegistration2 = { - status: "needs_client_registration" - error: string -} - -export type McpServer = { - name: string - status: - | McpStatusConnected2 - | McpStatusPending - | McpStatusDisabled2 - | McpStatusFailed2 - | McpStatusNeedsAuth2 - | McpStatusNeedsClientRegistration2 - integrationID?: string -} - -export type McpResourceTemplate = { - server: string - name: string - uriTemplate: string - description?: string - mimeType?: string -} - -export type McpResourceCatalog = { - resources: Array - templates: Array -} - -export type ProjectCurrent = { - id: string - directory: string -} - -export type FormCreatePayload = { - id?: string - title: string - metadata?: FormMetadata - fields: FormFields -} - -export type FormState = - | { - status: "pending" - } - | { - status: "answered" - answer: FormAnswer - } - | { - status: "cancelled" - } - -export type FormReply = { - answer: FormAnswer -} - -export type PermissionV2Request = { - id: string - sessionID: string - action: string - resources: Array - save?: Array - metadata?: { - [key: string]: unknown - } - source?: PermissionV2Source -} - -export type PermissionSavedInfo = { - id: string - projectID: string - action: string - resource: string -} - -export type FileSystemEntry = { - path: string - type: "file" | "directory" -} - -export type CommandInfo = { - name: string - template: string - description?: string - agent?: string - model?: ModelRef - subtask?: boolean -} - -export type SkillInfo = { - id: string - name: string - description?: string - slash?: boolean - autoinvoke?: boolean - location: string - content: string -} - -export type ModelsDevRefreshed = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "models-dev.refreshed" - location?: LocationRef - data: { - [key: string]: unknown - } -} - -export type IntegrationUpdated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "integration.updated" - location?: LocationRef - data: { - [key: string]: unknown - } -} - -export type IntegrationConnectionUpdated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "integration.connection.updated" - location?: LocationRef - data: { - integrationID: string - } -} - -export type CatalogUpdated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "catalog.updated" - location?: LocationRef - data: { - [key: string]: unknown - } -} - -export type AgentUpdated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "agent.updated" - location?: LocationRef - data: { - [key: string]: unknown - } -} - -export type SessionCreated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.created" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - info: SessionV1Info - } -} - -export type SessionUpdated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.updated" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - info: SessionV1Info - } -} - -export type MessageUpdated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "message.updated" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - info: Message - } -} - -export type MessageRemoved = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "message.removed" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - messageID: string - } -} - -export type MessagePartUpdated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "message.part.updated" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - part: Part - time: number - } -} - -export type MessagePartRemoved = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "message.part.removed" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRef - data: { - sessionID: string - messageID: string - partID: string - } -} - -export type SessionUsageUpdated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.usage.updated" - location?: LocationRef - data: { - sessionID: string - cost: MoneyUsd - tokens: TokenUsageInfo - } -} - -export type SessionTextDelta = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.text.delta" - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - ordinal: number - delta: string - } -} - -export type SessionReasoningDelta = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.reasoning.delta" - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - ordinal: number - delta: string - } -} - -export type SessionToolInputDelta = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.tool.input.delta" - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - callID: string - delta: string - } -} - -export type SessionCompactionDelta = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.compaction.delta" - location?: LocationRef - data: { - sessionID: string - text: string - } -} - -export type MessagePartDelta = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "message.part.delta" - location?: LocationRef - data: { - sessionID: string - messageID: string - partID: string - field: string - delta: string - } -} - -export type SessionDiff = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.diff" - location?: LocationRef - data: { - sessionID: string - diff: Array - } -} - -export type SessionError = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.error" - location?: LocationRef - data: { - sessionID?: string - error?: - | ProviderAuthError - | UnknownError - | MessageOutputLengthError - | MessageAbortedError - | StructuredOutputError - | ContextOverflowError - | ContentFilterError - | ApiError - } -} - -export type InstallationUpdated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "installation.updated" - location?: LocationRef - data: { - version: string - } -} - -export type InstallationUpdateAvailable = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "installation.update-available" - location?: LocationRef - data: { - version: string - } -} - -export type FilesystemChanged = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "filesystem.changed" - location?: LocationRef - data: { - file: string - event: "add" | "change" | "unlink" - } -} - -export type ReferenceUpdated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "reference.updated" - location?: LocationRef - data: { - [key: string]: unknown - } -} - -export type PermissionV2Asked = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "permission.v2.asked" - location?: LocationRef - data: { - id: string - sessionID: string - action: string - resources: Array - save?: Array - metadata?: { - [key: string]: unknown - } - source?: PermissionV2Source - } -} - -export type PermissionV2Replied = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "permission.v2.replied" - location?: LocationRef - data: { - sessionID: string - requestID: string - reply: PermissionV2Reply - } -} - -export type PluginAdded = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "plugin.added" - location?: LocationRef - data: { - id: string - } -} - -export type PluginUpdated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "plugin.updated" - location?: LocationRef - data: { - [key: string]: unknown - } -} - -export type ProjectDirectoriesUpdated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "project.directories.updated" - location?: LocationRef - data: { - projectID: string - } -} - -export type CommandUpdated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "command.updated" - location?: LocationRef - data: { - [key: string]: unknown - } -} - -export type ConfigUpdated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "config.updated" - location?: LocationRef - data: { - [key: string]: unknown - } -} - -export type SkillUpdated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "skill.updated" - location?: LocationRef - data: { - [key: string]: unknown - } -} - -export type PtyCreated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "pty.created" - location?: LocationRef - data: { - info: Pty - } -} - -export type PtyUpdated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "pty.updated" - location?: LocationRef - data: { - info: Pty - } -} - -export type PtyExited = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "pty.exited" - location?: LocationRef - data: { - id: string - exitCode: number - } -} - -export type PtyDeleted = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "pty.deleted" - location?: LocationRef - data: { - id: string - } -} - -export type ShellCreated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "shell.created" - location?: LocationRef - data: { - info: ShellInfo - } -} - -export type ShellExited = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "shell.exited" - location?: LocationRef - data: { - id: string - exit?: number - status: "running" | "exited" | "timeout" | "killed" - } -} - -export type ShellDeleted = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "shell.deleted" - location?: LocationRef - data: { - id: string - } -} - -export type QuestionV2Asked = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "question.v2.asked" - location?: LocationRef - data: { - id: string - sessionID: string - /** - * Questions to ask - */ - questions: Array - tool?: QuestionV2Tool - } -} - -export type QuestionV2Replied = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "question.v2.replied" - location?: LocationRef - data: { - sessionID: string - requestID: string - answers: Array - } -} - -export type QuestionV2Rejected = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "question.v2.rejected" - location?: LocationRef - data: { - sessionID: string - requestID: string - } -} - -export type FormWhen1 = { - key: string - op: "eq" | "neq" - value: string | number | "NaN" | "Infinity" | "-Infinity" | boolean -} - -export type FormNumberField1 = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "number" - minimum?: number | "NaN" | "Infinity" | "-Infinity" - maximum?: number | "NaN" | "Infinity" | "-Infinity" - default?: number | "NaN" | "Infinity" | "-Infinity" -} - -export type FormIntegerField1 = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "integer" - minimum?: number | "NaN" | "Infinity" | "-Infinity" - maximum?: number | "NaN" | "Infinity" | "-Infinity" - default?: number | "NaN" | "Infinity" | "-Infinity" -} - -export type FormCreated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "form.created" - location?: LocationRef - data: { - form: FormInfo - } -} - -export type FormValue1 = string | number | "NaN" | "Infinity" | "-Infinity" | boolean | Array - -export type FormReplied = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "form.replied" - location?: LocationRef - data: { - id: string - sessionID: string - answer: FormAnswer - } -} - -export type FormCancelled = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "form.cancelled" - location?: LocationRef - data: { - id: string - sessionID: string - } -} - -export type LspUpdated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "lsp.updated" - location?: LocationRef - data: { - [key: string]: unknown - } -} - -export type PermissionAsked = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "permission.asked" - location?: LocationRef - data: { - id: string - sessionID: string - permission: string - patterns: Array - metadata: { - [key: string]: unknown - } - always: Array - tool?: { - messageID: string - callID: string - } - } -} - -export type PermissionReplied = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "permission.replied" - location?: LocationRef - data: { - sessionID: string - requestID: string - reply: "once" | "always" | "reject" - } -} - -export type TuiPromptAppend = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "tui.prompt.append" - location?: LocationRef - data: { - text: string - } -} - -export type TuiCommandExecute = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "tui.command.execute" - location?: LocationRef - data: { - command: - | "session.list" - | "session.new" - | "session.share" - | "session.interrupt" - | "session.background" - | "session.compact" - | "session.page.up" - | "session.page.down" - | "session.line.up" - | "session.line.down" - | "session.half.page.up" - | "session.half.page.down" - | "session.first" - | "session.last" - | "prompt.clear" - | "prompt.submit" - | "agent.cycle" - | string - } -} - -export type TuiToastShow = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "tui.toast.show" - location?: LocationRef - data: { - title?: string - message: string - variant: "info" | "success" | "warning" | "error" - duration?: number - } -} - -export type TuiSessionSelect = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "tui.session.select" - location?: LocationRef - data: { - /** - * Session ID to navigate to - */ - sessionID: string - } -} - -export type McpToolsChanged = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "mcp.tools.changed" - location?: LocationRef - data: { - server: string - } -} - -export type McpResourcesChanged = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "mcp.resources.changed" - location?: LocationRef - data: { - server: string - } -} - -export type McpStatusChanged = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "mcp.status.changed" - location?: LocationRef - data: { - server: string - } -} - -export type CommandExecuted = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "command.executed" - location?: LocationRef - data: { - name: string - sessionID: string - arguments: string - messageID: string - } -} - -export type FileEdited = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "file.edited" - location?: LocationRef - data: { - file: string - } -} - -export type ProjectUpdated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "project.updated" - location?: LocationRef - data: { - id: string - worktree: string - vcs?: ProjectVcs - name?: string - icon?: ProjectIcon - commands?: ProjectCommands - time: ProjectTime - sandboxes: Array - } -} - -export type SessionIdle = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.idle" - location?: LocationRef - data: { - sessionID: string - } -} - -export type QuestionAsked = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "question.asked" - location?: LocationRef - data: { - id: string - sessionID: string - /** - * Questions to ask - */ - questions: Array - tool?: QuestionTool - } -} - -export type SessionCompacted = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.compacted" - location?: LocationRef - data: { - sessionID: string - } -} - -export type VcsBranchUpdated = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "vcs.branch.updated" - location?: LocationRef - data: { - branch?: string - } -} - -export type WorkspaceReady = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "workspace.ready" - location?: LocationRef - data: { - name: string - } -} - -export type WorkspaceFailed = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "workspace.failed" - location?: LocationRef - data: { - message: string - } -} - -export type WorkspaceStatus = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "workspace.status" - location?: LocationRef - data: { - workspaceID: string - status: "connected" | "connecting" | "disconnected" | "error" - } -} - -export type WorktreeReady = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "worktree.ready" - location?: LocationRef - data: { - name: string - branch?: string - } -} - -export type WorktreeFailed = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "worktree.failed" - location?: LocationRef - data: { - message: string - } -} - -export type ServerConnected = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "server.connected" - location?: LocationRef - data: { - [key: string]: unknown - } -} - -export type GlobalDisposed = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "global.disposed" - location?: LocationRef - data: { - [key: string]: unknown - } -} - -export type QuestionV2Request = { - id: string - sessionID: string - /** - * Questions to ask - */ - questions: Array - tool?: QuestionV2Tool -} - -export type QuestionV2Reply = { - /** - * User answers in order of questions (each answer is an array of selected labels) - */ - answers: Array -} - -export type ReferenceLocalSource = { - type: "local" - path: string - description?: string - hidden?: boolean -} - -export type ReferenceGitSource = { - type: "git" - repository: string - branch?: string - description?: string - hidden?: boolean -} - -export type ReferenceSource = ReferenceLocalSource | ReferenceGitSource - -export type ReferenceInfo = { - name: string - path: string - description?: string - hidden?: boolean - source: ReferenceSource -} - -export type ProjectCopyCopy = { - directory: string -} - -export type VcsMode = "working" | "branch" - -export type EventModelsDevRefreshed = { - id: string - type: "models-dev.refreshed" - properties: { - [key: string]: unknown - } -} - -export type EventIntegrationUpdated = { - id: string - type: "integration.updated" - properties: { - [key: string]: unknown - } -} - -export type EventIntegrationConnectionUpdated = { - id: string - type: "integration.connection.updated" - properties: { - integrationID: string - } -} - -export type EventCatalogUpdated = { - id: string - type: "catalog.updated" - properties: { - [key: string]: unknown - } -} - -export type EventAgentUpdated = { - id: string - type: "agent.updated" - properties: { - [key: string]: unknown - } -} - -export type EventSessionCreated = { - id: string - type: "session.created" - properties: { - sessionID: string - info: SessionV1Info - } -} - -export type EventSessionUpdated = { - id: string - type: "session.updated" - properties: { - sessionID: string - info: SessionV1Info - } -} - -export type EventSessionDeleted = { - id: string - type: "session.deleted" - properties: { - sessionID: string - } -} - -export type EventMessageUpdated = { - id: string - type: "message.updated" - properties: { - sessionID: string - info: Message - } -} - -export type EventMessageRemoved = { - id: string - type: "message.removed" - properties: { - sessionID: string - messageID: string - } -} - -export type EventMessagePartUpdated = { - id: string - type: "message.part.updated" - properties: { - sessionID: string - part: Part - time: number - } -} - -export type EventMessagePartRemoved = { - id: string - type: "message.part.removed" - properties: { - sessionID: string - messageID: string - partID: string - } -} - -export type EventSessionAgentSelected = { - id: string - type: "session.agent.selected" - properties: { - sessionID: string - agent: string - } -} - -export type EventSessionModelSelected = { - id: string - type: "session.model.selected" - properties: { - sessionID: string - model: ModelRef - } -} - -export type EventSessionMoved = { - id: string - type: "session.moved" - properties: { - sessionID: string - location: LocationRef - projectID?: string - subpath?: string - } -} - -export type EventSessionRenamed = { - id: string - type: "session.renamed" - properties: { - sessionID: string - title: string - } -} - -export type EventSessionUsageUpdated = { - id: string - type: "session.usage.updated" - properties: { - sessionID: string - cost: MoneyUsd - tokens: TokenUsageInfo - } -} - -export type EventSessionForked = { - id: string - type: "session.forked" - properties: { - sessionID: string - parentID: string - parentSeq: number - from?: string - } -} - -export type EventSessionInputPromoted = { - id: string - type: "session.input.promoted" - properties: { - sessionID: string - inputID: string - } -} - -export type EventSessionInputAdmitted = { - id: string - type: "session.input.admitted" - properties: { - sessionID: string - inputID: string - input: SessionPendingMessage - } -} - -export type EventSessionExecutionStarted = { - id: string - type: "session.execution.started" - properties: { - sessionID: string - } -} - -export type EventSessionExecutionSucceeded = { - id: string - type: "session.execution.succeeded" - properties: { - sessionID: string - } -} - -export type EventSessionExecutionFailed = { - id: string - type: "session.execution.failed" - properties: { - sessionID: string - error: SessionStructuredError - } -} - -export type EventSessionExecutionInterrupted = { - id: string - type: "session.execution.interrupted" - properties: { - sessionID: string - reason: "user" | "shutdown" | "superseded" - } -} - -export type EventSessionInstructionsUpdated = { - id: string - type: "session.instructions.updated" - properties: { - sessionID: string - delta: { - [key: string]: string | "removed" - } - } -} - -export type EventSessionSynthetic = { - id: string - type: "session.synthetic" - properties: { - sessionID: string - text: string - description?: string - metadata?: { - [key: string]: unknown - } - } -} - -export type EventSessionSkillActivated = { - id: string - type: "session.skill.activated" - properties: { - sessionID: string - id: string - name: string - text: string - } -} - -export type EventSessionShellStarted = { - id: string - type: "session.shell.started" - properties: { - sessionID: string - shell: ShellInfo - } -} - -export type EventSessionShellEnded = { - id: string - type: "session.shell.ended" - properties: { - sessionID: string - shell: ShellInfo - output: { - output: string - cursor: number - size: number - truncated: boolean - } - } -} - -export type EventSessionStepStarted = { - id: string - type: "session.step.started" - properties: { - sessionID: string - assistantMessageID: string - agent: string - model: ModelRef - snapshot?: string - } -} - -export type EventSessionStepEnded = { - id: string - type: "session.step.ended" - properties: { - sessionID: string - assistantMessageID: string - finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" - cost: MoneyUsd - tokens: TokenUsageInfo - snapshot?: string - files?: Array - } -} - -export type EventSessionStepFailed = { - id: string - type: "session.step.failed" - properties: { - sessionID: string - assistantMessageID: string - error: SessionStructuredError - cost?: MoneyUsd - tokens?: TokenUsageInfo - } -} - -export type EventSessionTextStarted = { - id: string - type: "session.text.started" - properties: { - sessionID: string - assistantMessageID: string - ordinal: number - } -} - -export type EventSessionTextDelta = { - id: string - type: "session.text.delta" - properties: { - sessionID: string - assistantMessageID: string - ordinal: number - delta: string - } -} - -export type EventSessionTextEnded = { - id: string - type: "session.text.ended" - properties: { - sessionID: string - assistantMessageID: string - ordinal: number - text: string - } -} - -export type EventSessionReasoningStarted = { - id: string - type: "session.reasoning.started" - properties: { - sessionID: string - assistantMessageID: string - ordinal: number - state?: SessionMessageProviderState - } -} - -export type EventSessionReasoningDelta = { - id: string - type: "session.reasoning.delta" - properties: { - sessionID: string - assistantMessageID: string - ordinal: number - delta: string - } -} - -export type EventSessionReasoningEnded = { - id: string - type: "session.reasoning.ended" - properties: { - sessionID: string - assistantMessageID: string - ordinal: number - text: string - state?: SessionMessageProviderState - } -} - -export type EventSessionToolInputStarted = { - id: string - type: "session.tool.input.started" - properties: { - sessionID: string - assistantMessageID: string - callID: string - name: string - } -} - -export type EventSessionToolInputDelta = { - id: string - type: "session.tool.input.delta" - properties: { - sessionID: string - assistantMessageID: string - callID: string - delta: string - } -} - -export type EventSessionToolInputEnded = { - id: string - type: "session.tool.input.ended" - properties: { - sessionID: string - assistantMessageID: string - callID: string - text: string - } -} - -export type EventSessionToolCalled = { - id: string - type: "session.tool.called" - properties: { - sessionID: string - assistantMessageID: string - callID: string - input: { - [key: string]: unknown - } - executed: boolean - state?: SessionMessageProviderState - } -} - -export type EventSessionToolProgress = { - id: string - type: "session.tool.progress" - properties: { - sessionID: string - assistantMessageID: string - callID: string - structured: { - [key: string]: unknown - } - content: Array - } -} - -export type EventSessionToolSuccess = { - id: string - type: "session.tool.success" - properties: { - sessionID: string - assistantMessageID: string - callID: string - structured: { - [key: string]: unknown - } - content: Array - result?: unknown - executed: boolean - resultState?: SessionMessageProviderState - } -} - -export type EventSessionToolFailed = { - id: string - type: "session.tool.failed" - properties: { - sessionID: string - assistantMessageID: string - callID: string - error: SessionStructuredError - result?: unknown - executed: boolean - resultState?: SessionMessageProviderState - } -} - -export type EventSessionRetryScheduled = { - id: string - type: "session.retry.scheduled" - properties: { - sessionID: string - assistantMessageID: string - attempt: number - at: number - error: SessionStructuredError - } -} - -export type EventSessionCompactionAdmitted = { - id: string - type: "session.compaction.admitted" - properties: { - sessionID: string - inputID: string - } -} - -export type EventSessionCompactionStarted = { - id: string - type: "session.compaction.started" - properties: { - sessionID: string - reason: "auto" | "manual" - recent: string - inputID?: string - } -} - -export type EventSessionCompactionDelta = { - id: string - type: "session.compaction.delta" - properties: { - sessionID: string - text: string - } -} - -export type EventSessionCompactionEnded = { - id: string - type: "session.compaction.ended" - properties: { - sessionID: string - reason: "auto" | "manual" - text: string - recent: string - } -} - -export type EventSessionCompactionFailed = { - id: string - type: "session.compaction.failed" - properties: { - sessionID: string - reason: "auto" | "manual" - error: SessionStructuredError - inputID?: string - } -} - -export type EventSessionRevertStaged = { - id: string - type: "session.revert.staged" - properties: { - sessionID: string - revert: SessionRevert - } -} - -export type EventSessionRevertCleared = { - id: string - type: "session.revert.cleared" - properties: { - sessionID: string - } -} - -export type EventSessionRevertCommitted = { - id: string - type: "session.revert.committed" - properties: { - sessionID: string - to: string - } -} - -export type EventMessagePartDelta = { - id: string - type: "message.part.delta" - properties: { - sessionID: string - messageID: string - partID: string - field: string - delta: string - } -} - -export type EventSessionDiff = { - id: string - type: "session.diff" - properties: { - sessionID: string - diff: Array - } -} - -export type EventSessionError = { - id: string - type: "session.error" - properties: { - sessionID?: string - error?: - | ProviderAuthError - | UnknownError - | MessageOutputLengthError - | MessageAbortedError - | StructuredOutputError - | ContextOverflowError - | ContentFilterError - | ApiError - } -} - -export type EventInstallationUpdated = { - id: string - type: "installation.updated" - properties: { - version: string - } -} - -export type EventInstallationUpdateAvailable = { - id: string - type: "installation.update-available" - properties: { - version: string - } -} - -export type EventFilesystemChanged = { - id: string - type: "filesystem.changed" - properties: { - file: string - event: "add" | "change" | "unlink" - } -} - -export type EventReferenceUpdated = { - id: string - type: "reference.updated" - properties: { - [key: string]: unknown - } -} - -export type EventPermissionV2Asked = { - id: string - type: "permission.v2.asked" - properties: { - id: string - sessionID: string - action: string - resources: Array - save?: Array - metadata?: { - [key: string]: unknown - } - source?: PermissionV2Source - } -} - -export type EventPermissionV2Replied = { - id: string - type: "permission.v2.replied" - properties: { - sessionID: string - requestID: string - reply: PermissionV2Reply - } -} - -export type EventPluginAdded = { - id: string - type: "plugin.added" - properties: { - id: string - } -} - -export type EventPluginUpdated = { - id: string - type: "plugin.updated" - properties: { - [key: string]: unknown - } -} - -export type EventProjectDirectoriesUpdated = { - id: string - type: "project.directories.updated" - properties: { - projectID: string - } -} - -export type EventCommandUpdated = { - id: string - type: "command.updated" - properties: { - [key: string]: unknown - } -} - -export type EventConfigUpdated = { - id: string - type: "config.updated" - properties: { - [key: string]: unknown - } -} - -export type EventSkillUpdated = { - id: string - type: "skill.updated" - properties: { - [key: string]: unknown - } -} - -export type EventPtyCreated = { - id: string - type: "pty.created" - properties: { - info: Pty - } -} - -export type EventPtyUpdated = { - id: string - type: "pty.updated" - properties: { - info: Pty - } -} - -export type EventPtyExited = { - id: string - type: "pty.exited" - properties: { - id: string - exitCode: number - } -} - -export type EventPtyDeleted = { - id: string - type: "pty.deleted" - properties: { - id: string - } -} - -export type EventShellCreated = { - id: string - type: "shell.created" - properties: { - info: ShellInfo - } -} - -export type EventShellExited = { - id: string - type: "shell.exited" - properties: { - id: string - exit?: number - status: "running" | "exited" | "timeout" | "killed" - } -} - -export type EventShellDeleted = { - id: string - type: "shell.deleted" - properties: { - id: string - } -} - -export type EventQuestionV2Asked = { - id: string - type: "question.v2.asked" - properties: { - id: string - sessionID: string - /** - * Questions to ask - */ - questions: Array - tool?: QuestionV2Tool - } -} - -export type EventQuestionV2Replied = { - id: string - type: "question.v2.replied" - properties: { - sessionID: string - requestID: string - answers: Array - } -} - -export type EventQuestionV2Rejected = { - id: string - type: "question.v2.rejected" - properties: { - sessionID: string - requestID: string - } -} - -export type FormNumberField2 = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "number" - minimum?: number | "NaN" | "Infinity" | "-Infinity" - maximum?: number | "NaN" | "Infinity" | "-Infinity" - default?: number | "NaN" | "Infinity" | "-Infinity" -} - -export type FormIntegerField2 = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "integer" - minimum?: number | "NaN" | "Infinity" | "-Infinity" - maximum?: number | "NaN" | "Infinity" | "-Infinity" - default?: number | "NaN" | "Infinity" | "-Infinity" -} - -export type EventFormCreated = { - id: string - type: "form.created" - properties: { - form: FormInfo - } -} - -export type EventFormReplied = { - id: string - type: "form.replied" - properties: { - id: string - sessionID: string - answer: FormAnswer - } -} - -export type EventFormCancelled = { - id: string - type: "form.cancelled" - properties: { - id: string - sessionID: string - } -} - -export type EventLspUpdated = { - id: string - type: "lsp.updated" - properties: { - [key: string]: unknown - } -} - -export type EventPermissionAsked = { - id: string - type: "permission.asked" - properties: { - id: string - sessionID: string - permission: string - patterns: Array - metadata: { - [key: string]: unknown - } - always: Array - tool?: { - messageID: string - callID: string - } - } -} - -export type EventPermissionReplied = { - id: string - type: "permission.replied" - properties: { - sessionID: string - requestID: string - reply: "once" | "always" | "reject" - } -} - -export type EventMcpToolsChanged = { - id: string - type: "mcp.tools.changed" - properties: { - server: string - } -} - -export type EventMcpResourcesChanged = { - id: string - type: "mcp.resources.changed" - properties: { - server: string - } -} - -export type EventMcpStatusChanged = { - id: string - type: "mcp.status.changed" - properties: { - server: string - } -} - -export type EventCommandExecuted = { - id: string - type: "command.executed" - properties: { - name: string - sessionID: string - arguments: string - messageID: string - } -} - -export type EventFileEdited = { - id: string - type: "file.edited" - properties: { - file: string - } -} - -export type EventProjectUpdated = { - id: string - type: "project.updated" - properties: { - id: string - worktree: string - vcs?: ProjectVcs - name?: string - icon?: ProjectIcon - commands?: ProjectCommands - time: ProjectTime - sandboxes: Array - } -} - -export type EventSessionStatus = { - id: string - type: "session.status" - properties: { - sessionID: string - status: SessionStatus - } -} - -export type EventSessionIdle = { - id: string - type: "session.idle" - properties: { - sessionID: string - } -} - -export type EventQuestionAsked = { - id: string - type: "question.asked" - properties: { - id: string - sessionID: string - /** - * Questions to ask - */ - questions: Array - tool?: QuestionTool - } -} - -export type EventQuestionReplied = { - id: string - type: "question.replied" - properties: { - sessionID: string - requestID: string - answers: Array - } -} - -export type EventQuestionRejected = { - id: string - type: "question.rejected" - properties: { - sessionID: string - requestID: string - } -} - -export type EventSessionCompacted = { - id: string - type: "session.compacted" - properties: { - sessionID: string - } -} - -export type EventVcsBranchUpdated = { - id: string - type: "vcs.branch.updated" - properties: { - branch?: string - } -} - -export type EventWorkspaceReady = { - id: string - type: "workspace.ready" - properties: { - name: string - } -} - -export type EventWorkspaceFailed = { - id: string - type: "workspace.failed" - properties: { - message: string - } -} - -export type EventWorkspaceStatus = { - id: string - type: "workspace.status" - properties: { - workspaceID: string - status: "connected" | "connecting" | "disconnected" | "error" - } -} - -export type EventWorktreeReady = { - id: string - type: "worktree.ready" - properties: { - name: string - branch?: string - } -} - -export type EventWorktreeFailed = { - id: string - type: "worktree.failed" - properties: { - message: string - } -} - -export type EventServerConnected = { - id: string - type: "server.connected" - properties: { - [key: string]: unknown - } -} - -export type EventGlobalDisposed = { - id: string - type: "global.disposed" - properties: { - [key: string]: unknown - } -} - -export type CredentialOAuth = { - type: "oauth" - methodID: string - refresh: string - access: string - expires: number - metadata?: { - [key: string]: unknown - } -} - -export type CredentialKey = { - type: "key" - key: string - metadata?: { - [key: string]: unknown - } -} - -export type SkillDirectorySource = { - type: "directory" - path: string -} - -export type SkillUrlSource = { - type: "url" - url: string -} - -export type SkillEmbeddedSource = { - type: "embedded" - skill: SkillInfo -} - -export type BadRequestError = { - name: "BadRequest" - data: { - message: string - kind?: "Params" | "Headers" | "Query" | "Body" | "Payload" - } -} - -export type ServiceHealthV2 = { - healthy: true - version: string - pid: number -} - -export type InvalidRequestErrorV2 = { - _tag: "InvalidRequestError" - message: string - kind?: string | null - field?: string | null -} - -export type SessionsResponseV2 = { - data: Array - cursor: { - previous?: string | null - next?: string | null - } -} - -export type InvalidRequestError1 = { - _tag: "InvalidRequestError" - message: string - kind?: string | null - field?: string | null -} - -export type ConflictErrorV2 = { - _tag: "ConflictError" - message: string - resource?: string | null -} - -export type ServiceUnavailableErrorV2 = { - _tag: "ServiceUnavailableError" - message: string - service?: string | null -} - -export type UnknownErrorV2 = { - _tag: "UnknownError" - message: string - ref?: string | null -} - -export type SessionMessagesResponseV2 = { - data: Array - cursor: { - previous?: string | null - next?: string | null - } -} - -export type OutputFormatV2 = - | { - type: "text" - } - | { - type: "json_schema" - schema: JsonSchema - retryCount?: number | null | null - } - -export type UserMessageV2 = { - id: string - sessionID: string - role: "user" - time: { - created: number - } - format?: OutputFormatV2 | null - summary?: { - title?: string | null - body?: string | null - diffs: Array - } | null - agent: string - model: { - providerID: string - modelID: string - variant?: string | null - } - system?: string | null - tools?: { - [key: string]: boolean - } | null -} - -export type UnknownError1V2 = { - name: "UnknownError" - data: { - message: string - ref?: string | null - } -} - -export type MessageOutputLengthErrorV2 = { - name: "MessageOutputLengthError" - data: - | { - [key: string]: unknown - } - | Array -} - -export type StructuredOutputErrorV2 = { - name: "StructuredOutputError" - data: { - message: string - retries: number - } -} - -export type ContextOverflowErrorV2 = { - name: "ContextOverflowError" - data: { - message: string - responseBody?: string | null - } -} - -export type ApiErrorV2 = { - name: "APIError" - data: { - message: string - statusCode?: number | null - isRetryable: boolean - responseHeaders?: { - [key: string]: string - } | null - responseBody?: string | null - metadata?: { - [key: string]: string - } | null - } -} - -export type AssistantMessageV2 = { - id: string - sessionID: string - role: "assistant" - time: { - created: number - completed?: number | null - } - error?: - | ProviderAuthError - | UnknownError1V2 - | MessageOutputLengthErrorV2 - | MessageAbortedError - | StructuredOutputErrorV2 - | ContextOverflowErrorV2 - | ContentFilterError - | ApiErrorV2 - | null - parentID: string - modelID: string - providerID: string - mode: string - agent: string - path: { - cwd: string - root: string - } - summary?: boolean | null - cost: number - tokens: { - total?: number | null - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } - structured?: unknown | null - variant?: string | null - finish?: string | null -} - -export type TextPartV2 = { - id: string - sessionID: string - messageID: string - type: "text" - text: string - synthetic?: boolean | null - ignored?: boolean | null - time?: { - start: number - end?: number | null - } | null - metadata?: { - [key: string]: unknown - } | null -} - -export type SubtaskPartV2 = { - id: string - sessionID: string - messageID: string - type: "subtask" - prompt: string - description: string - agent: string - model?: { - providerID: string - modelID: string - } | null - command?: string | null -} - -export type ReasoningPartV2 = { - id: string - sessionID: string - messageID: string - type: "reasoning" - text: string - metadata?: { - [key: string]: unknown - } | null - time: { - start: number - end?: number | null - } -} - -export type RangeV2 = { - start: { - line: number - character: number - } - end: { - line: number - character: number - } -} - -export type SymbolSourceV2 = { - text: FilePartSourceText - type: "symbol" - path: string - range: RangeV2 - name: string - kind: number -} - -export type FilePartV2 = { - id: string - sessionID: string - messageID: string - type: "file" - mime: string - filename?: string | null - url: string - source?: FilePartSource | null -} - -export type ToolStateRunningV2 = { - status: "running" - input: { - [key: string]: unknown - } - title?: string | null - metadata?: { - [key: string]: unknown - } | null - time: { - start: number - } -} - -export type ToolStateCompletedV2 = { - status: "completed" - input: { - [key: string]: unknown - } - output: string - title: string - metadata: { - [key: string]: unknown - } - time: { - start: number - end: number - compacted?: number | null - } - attachments?: Array | null -} - -export type ToolStateErrorV2 = { - status: "error" - input: { - [key: string]: unknown - } - error: string - metadata?: { - [key: string]: unknown - } | null - time: { - start: number - end: number - } -} - -export type ToolPartV2 = { - id: string - sessionID: string - messageID: string - type: "tool" - callID: string - tool: string - state: ToolState - metadata?: { - [key: string]: unknown - } | null -} - -export type StepStartPartV2 = { - id: string - sessionID: string - messageID: string - type: "step-start" - snapshot?: string | null -} - -export type StepFinishPartV2 = { - id: string - sessionID: string - messageID: string - type: "step-finish" - reason: string - snapshot?: string | null - cost: number - tokens: { - total?: number | null - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } -} - -export type SnapshotPartV2 = { - id: string - sessionID: string - messageID: string - type: "snapshot" - snapshot: string -} - -export type PatchPartV2 = { - id: string - sessionID: string - messageID: string - type: "patch" - hash: string - files: Array -} - -export type AgentPartV2 = { - id: string - sessionID: string - messageID: string - type: "agent" - name: string - source?: { - value: string - start: number - end: number - } | null -} - -export type RetryPartV2 = { - id: string - sessionID: string - messageID: string - type: "retry" - attempt: number - error: ApiErrorV2 - time: { - created: number - } -} - -export type CompactionPartV2 = { - id: string - sessionID: string - messageID: string - type: "compaction" - auto: boolean - overflow?: boolean | null - tail_start_id?: string | null -} - -export type PtyV2 = { - id: string - title: string - command: string - args: Array - cwd: string - status: "running" | "exited" - pid: number - exitCode?: number -} - -export type SessionStatusV2 = - | { - type: "idle" - } - | { - type: "retry" - attempt: number - message: string - action?: { - reason: string - provider: string - title: string - message: string - label: string - link?: string - } - next: number - } - | { - type: "busy" - } - -export type SessionStatusV22 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.status" - location?: LocationRefV2 - data: { - sessionID: string - status: SessionStatusV2 - } -} - -export type QuestionInfoV2 = { - /** - * Complete question - */ - question: string - /** - * Very short label (max 30 chars) - */ - header: string - /** - * Available choices - */ - options: Array - /** - * Allow selecting multiple choices - */ - multiple?: boolean | null - /** - * Allow typing a custom answer (default: true) - */ - custom?: boolean | null -} - -export type QuestionToolV2 = { - messageID: string - callID: string -} - -export type V2EventV2 = - | ModelsDevRefreshedV2 - | IntegrationUpdatedV2 - | IntegrationConnectionUpdatedV2 - | CatalogUpdatedV2 - | AgentUpdatedV2 - | SessionCreatedV2 - | SessionUpdatedV2 - | SessionDeleted1 - | MessageUpdatedV2 - | MessageRemovedV2 - | MessagePartUpdatedV2 - | MessagePartRemovedV2 - | SessionAgentSelectedV2 - | SessionModelSelectedV2 - | SessionMovedV2 - | SessionRenamedV2 - | SessionUsageUpdatedV2 - | SessionDeletedV2 - | SessionForkedV2 - | SessionInputPromotedV2 - | SessionInputAdmittedV2 - | SessionExecutionStartedV2 - | SessionExecutionSucceededV2 - | SessionExecutionFailedV2 - | SessionExecutionInterruptedV2 - | SessionInstructionsUpdatedV2 - | SessionSyntheticV2 - | SessionSkillActivatedV2 - | SessionShellStartedV2 - | SessionShellEndedV2 - | SessionStepStartedV2 - | SessionStepEndedV2 - | SessionStepFailedV2 - | SessionTextStartedV2 - | SessionTextDeltaV2 - | SessionTextEndedV2 - | SessionReasoningStartedV2 - | SessionReasoningDeltaV2 - | SessionReasoningEndedV2 - | SessionToolInputStartedV2 - | SessionToolInputDeltaV2 - | SessionToolInputEndedV2 - | SessionToolCalledV2 - | SessionToolProgressV2 - | SessionToolSuccessV2 - | SessionToolFailedV2 - | SessionRetryScheduledV2 - | SessionCompactionAdmittedV2 - | SessionCompactionStartedV2 - | SessionCompactionDeltaV2 - | SessionCompactionEndedV2 - | SessionCompactionFailedV2 - | SessionRevertStagedV2 - | SessionRevertClearedV2 - | SessionRevertCommittedV2 - | FilesystemChangedV2 - | ReferenceUpdatedV2 - | PermissionV2AskedV2 - | PermissionV2RepliedV2 - | PluginAddedV2 - | PluginUpdatedV2 - | ProjectDirectoriesUpdatedV2 - | CommandUpdatedV2 - | ConfigUpdatedV2 - | SkillUpdatedV2 - | PtyCreatedV2 - | PtyUpdatedV2 - | PtyExitedV2 - | PtyDeletedV2 - | ShellCreatedV2 - | ShellExitedV2 - | ShellDeletedV2 - | QuestionV2AskedV2 - | QuestionV2RepliedV2 - | QuestionV2RejectedV2 - | FormCreatedV2 - | FormRepliedV2 - | FormCancelledV2 - | SessionStatusV22 - | SessionIdleV2 - | TuiPromptAppendV2 - | TuiCommandExecuteV2 - | TuiToastShowV2 - | TuiSessionSelectV2 - | InstallationUpdatedV2 - | InstallationUpdateAvailableV2 - | VcsBranchUpdatedV2 - | McpStatusChangedV2 - | McpResourcesChangedV2 - | PermissionAskedV2 - | PermissionRepliedV2 - | QuestionAskedV2 - | QuestionRepliedV2 - | QuestionRejectedV2 - | SessionErrorV2 - | V2EventServerConnected - -export type ProjectCopyErrorV2 = { - name: "ProjectCopyError" - data: { - message: string - forceRequired?: boolean | null - } -} - -export type LocationInfoV2 = { - directory: string - workspaceID?: string - project: { - id: string - directory: string - } -} - -export type AgentColorV2 = string | "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info" - -export type AgentInfoV2 = { - id: string - name: string - model?: ModelRef - request: ProviderRequest - system?: string - description?: string - mode: "subagent" | "primary" | "all" - hidden: boolean - color?: AgentColorV2 - steps?: number - permissions: PermissionV2Ruleset -} - -export type LocationRefV2 = { - directory: string - workspaceID?: string -} - -export type FileDiffInfoV2 = { - file: string - patch: string - additions: number - deletions: number - status: "added" | "deleted" | "modified" -} - -export type SessionRevertV2 = { - messageID: string - partID?: string - snapshot?: string - files?: Array -} - -export type SessionInfoV2 = { - id: string - parentID?: string - fork?: { - sessionID: string - messageID?: string - } - projectID: string - agent?: string - model?: ModelRef - cost: MoneyUsd - tokens: TokenUsageInfo - time: { - created: number - updated: number - archived?: number - } - title: string - location: LocationRefV2 - subpath?: string - revert?: SessionRevertV2 -} - -export type PromptBase64V2 = string - -export type SessionPendingUserV2 = { - admittedSeq: number - id: string - sessionID: string - timeCreated: number - type: "user" - data: SessionPendingUserData - delivery: "steer" | "queue" -} - -export type SessionPendingSyntheticV2 = { - admittedSeq: number - id: string - sessionID: string - timeCreated: number - type: "synthetic" - data: SessionPendingSyntheticData - delivery: "steer" | "queue" -} - -export type SessionPendingCompactionV2 = { - admittedSeq: number - id: string - sessionID: string - timeCreated: number - type: "compaction" -} - -export type SessionMessageAgentSelectedV2 = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - type: "agent-switched" - agent: string -} - -export type SessionMessageModelSelectedV2 = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - type: "model-switched" - model: ModelRef - previous?: ModelRef -} - -export type SessionMessageUserV2 = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - text: string - files?: Array - agents?: Array - type: "user" -} - -export type SessionMessageSyntheticV2 = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - text: string - description?: string - type: "synthetic" -} - -export type SessionMessageSystemV2 = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - type: "system" - text: string -} - -export type SessionMessageSkillV2 = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - type: "skill" - skill: string - name: string - text: string -} - -export type SessionMessageShellV2 = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - completed?: number - } - type: "shell" - shellID: string - command: string - status: "running" | "exited" | "timeout" | "killed" - exit?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - output?: { - output: string - cursor: number - size: number - truncated: boolean - } -} - -export type SessionMessageAssistantRetryV2 = { - attempt: number - at: number - error: SessionStructuredError -} - -export type SessionMessageAssistantV2 = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - completed?: number - } - type: "assistant" - agent: string - model: ModelRef - content: Array - snapshot?: { - start?: string - end?: string - files?: Array - } - finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" - cost?: MoneyUsd - tokens?: TokenUsageInfo - error?: SessionStructuredError - retry?: SessionMessageAssistantRetryV2 -} - -export type SessionMessageCompactionRunningV2 = { - type: "compaction" - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - status: "running" - reason: "auto" | "manual" - summary: string - recent: string -} - -export type SessionMessageCompactionCompletedV2 = { - type: "compaction" - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - status: "completed" - reason: "auto" | "manual" - summary: string - recent: string -} - -export type SessionMessageCompactionFailedV2 = { - type: "compaction" - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - status: "failed" - reason: "auto" | "manual" - error: SessionStructuredError -} - -/** - * Instruction entry key (lowercase alphanumerics plus . _ -) - */ -export type InstructionEntryKeyV2 = string - -export type SessionAgentSelectedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.agent.selected" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - agent: string - } -} - -export type SessionModelSelectedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.model.selected" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - model: ModelRef - } -} - -export type SessionMovedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.moved" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - location: LocationRefV2 - projectID?: string - subpath?: string - } -} - -export type SessionRenamedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.renamed" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - title: string - } -} - -export type SessionDeletedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.deleted" - durable: { - aggregateID: string - seq: number - version: 2 - } - location?: LocationRefV2 - data: { - sessionID: string - } -} - -export type SessionForkedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.forked" - durable: { - aggregateID: string - seq: number - version: 2 - } - location?: LocationRefV2 - data: { - sessionID: string - parentID: string - parentSeq: number - from?: string - } -} - -export type SessionInputPromotedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.input.promoted" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - inputID: string - } -} - -export type SessionPendingUserData1 = { - text: string - files?: Array - agents?: Array - metadata?: { - [key: string]: unknown - } -} - -export type SessionPendingUserMessageV2 = { - type: "user" - data: SessionPendingUserData1 - delivery: "steer" | "queue" -} - -export type SessionPendingSyntheticData1 = { - text: string - description?: string - metadata?: { - [key: string]: unknown - } -} - -export type SessionPendingSyntheticMessageV2 = { - type: "synthetic" - data: SessionPendingSyntheticData1 - delivery: "steer" | "queue" -} - -export type SessionInputAdmittedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.input.admitted" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - inputID: string - input: SessionPendingMessage - } -} - -export type SessionExecutionStartedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.execution.started" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - } -} - -export type SessionExecutionSucceededV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.execution.succeeded" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - } -} - -export type SessionExecutionFailedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.execution.failed" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - error: SessionStructuredError - } -} - -export type SessionExecutionInterruptedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.execution.interrupted" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - reason: "user" | "shutdown" | "superseded" - } -} - -export type SessionInstructionsUpdatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.instructions.updated" - durable: { - aggregateID: string - seq: number - version: 2 - } - location?: LocationRefV2 - data: { - sessionID: string - delta: { - [key: string]: string | "removed" - } - } -} - -export type SessionSyntheticV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.synthetic" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - text: string - description?: string - metadata?: { - [key: string]: unknown - } - } -} - -export type SessionSkillActivatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.skill.activated" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - id: string - name: string - text: string - } -} - -export type ShellInfoV2 = { - id: string - status: "running" | "exited" | "timeout" | "killed" - command: string - cwd: string - shell: string - file: string - pid?: number - exit?: number - metadata: { - [key: string]: unknown - } - time: { - started: number - completed?: number - } -} - -export type SessionShellStartedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.shell.started" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - shell: ShellInfoV2 - } -} - -export type SessionShellEndedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.shell.ended" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - shell: ShellInfoV2 - output: { - output: string - cursor: number - size: number - truncated: boolean - } - } -} - -export type SessionStepStartedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.step.started" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - assistantMessageID: string - agent: string - model: ModelRef - snapshot?: string - } -} - -export type SessionStepEndedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.step.ended" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - assistantMessageID: string - finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" - cost: MoneyUsd - tokens: TokenUsageInfo - snapshot?: string - files?: Array - } -} - -export type SessionStepFailedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.step.failed" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - assistantMessageID: string - error: SessionStructuredError - cost?: MoneyUsd - tokens?: TokenUsageInfo - } -} - -export type SessionTextStartedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.text.started" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - assistantMessageID: string - ordinal: number - } -} - -export type SessionTextEndedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.text.ended" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - assistantMessageID: string - ordinal: number - text: string - } -} - -export type SessionMessageProviderState3 = { - [key: string]: unknown -} - -export type SessionReasoningStartedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.reasoning.started" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - assistantMessageID: string - ordinal: number - state?: SessionMessageProviderState3 - } -} - -export type SessionMessageProviderState4 = { - [key: string]: unknown -} - -export type SessionReasoningEndedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.reasoning.ended" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - assistantMessageID: string - ordinal: number - text: string - state?: SessionMessageProviderState4 - } -} - -export type SessionToolInputStartedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.tool.input.started" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - assistantMessageID: string - callID: string - name: string - } -} - -export type SessionToolInputEndedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.tool.input.ended" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - assistantMessageID: string - callID: string - text: string - } -} - -export type SessionMessageProviderState5 = { - [key: string]: unknown -} - -export type SessionToolCalledV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.tool.called" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - assistantMessageID: string - callID: string - input: { - [key: string]: unknown - } - executed: boolean - state?: SessionMessageProviderState5 - } -} - -export type SessionToolProgressV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.tool.progress" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - assistantMessageID: string - callID: string - structured: { - [key: string]: unknown - } - content: Array - } -} - -export type SessionMessageProviderState6 = { - [key: string]: unknown -} - -export type SessionToolSuccessV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.tool.success" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - assistantMessageID: string - callID: string - structured: { - [key: string]: unknown - } - content: Array - result?: unknown - executed: boolean - resultState?: SessionMessageProviderState6 - } -} - -export type SessionMessageProviderState7 = { - [key: string]: unknown -} - -export type SessionToolFailedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.tool.failed" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - assistantMessageID: string - callID: string - error: SessionStructuredError - result?: unknown - executed: boolean - resultState?: SessionMessageProviderState7 - } -} - -export type SessionRetryScheduledV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.retry.scheduled" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - assistantMessageID: string - attempt: number - at: number - error: SessionStructuredError - } -} - -export type SessionCompactionAdmittedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.compaction.admitted" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - inputID: string - } -} - -export type SessionCompactionStartedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.compaction.started" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - reason: "auto" | "manual" - recent: string - inputID?: string - } -} - -export type SessionCompactionEndedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.compaction.ended" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - reason: "auto" | "manual" - text: string - recent: string - } -} - -export type SessionCompactionFailedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.compaction.failed" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - reason: "auto" | "manual" - error: SessionStructuredError - inputID?: string - } -} - -export type SessionRevertStagedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.revert.staged" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - revert: SessionRevertV2 - } -} - -export type SessionRevertClearedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.revert.cleared" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - } -} - -export type SessionRevertCommittedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.revert.committed" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - to: string - } -} - -export type SessionUsageRecordedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.usage.recorded" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - source: "title" | "compaction" - cost: MoneyUsd - tokens: TokenUsageInfo - } -} - -/** - * Marker emitted once when a log read reaches its captured watermark. The reader holds every event committed at or below seq. - */ -export type EventLogSyncedV2 = { - type: "log.synced" - aggregateID: string - seq?: number -} - -export type McpResourceV2 = { - server: string - name: string - uri: string - description?: string - mimeType?: string -} - -export type McpResourceCatalogV2 = { - resources: Array - templates: Array -} - -export type ProjectTimeV2 = { - created: number - updated: number - initialized?: number -} - -export type FormWhenV2 = { - key: string - op: "eq" | "neq" - value: string | number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" | boolean -} - -export type FormStringFieldV2 = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "string" - format?: "email" | "uri" | "date" | "date-time" - minLength?: number - maxLength?: number - pattern?: string - placeholder?: string - default?: string - options?: Array - custom?: boolean -} - -export type FormMultiselectFieldV2 = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "multiselect" - options: Array - minItems?: number - maxItems?: number - custom?: boolean - default?: Array -} - -export type FormFieldsV2 = [FormField, FormField] - -export type FormInfoV2 = { - id: string - sessionID: string - title: string - metadata?: FormMetadata - fields: FormFieldsV2 -} - -export type FormCreatePayloadV2 = { - id?: string | null - title: string - metadata?: FormMetadata - fields: FormFieldsV2 -} - -export type FormValueV2 = - | string - | number - | "NaN" - | "Infinity" - | "-Infinity" - | "Infinity" - | "-Infinity" - | "NaN" - | boolean - | Array - -export type PermissionV2SourceV2 = { - type: "tool" - messageID: string - callID: string -} - -export type PermissionV2RequestV2 = { - id: string - sessionID: string - action: string - resources: Array - save?: Array - metadata?: { - [key: string]: unknown - } - source?: PermissionV2SourceV2 -} - -export type ModelsDevRefreshedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "models-dev.refreshed" - location?: LocationRefV2 - data: - | { - [key: string]: unknown - } - | Array -} - -export type IntegrationUpdatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "integration.updated" - location?: LocationRefV2 - data: - | { - [key: string]: unknown - } - | Array -} - -export type IntegrationConnectionUpdatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "integration.connection.updated" - location?: LocationRefV2 - data: { - integrationID: string - } -} - -export type CatalogUpdatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "catalog.updated" - location?: LocationRefV2 - data: - | { - [key: string]: unknown - } - | Array -} - -export type AgentUpdatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "agent.updated" - location?: LocationRefV2 - data: - | { - [key: string]: unknown - } - | Array -} - -export type SessionV1InfoV2 = { - id: string - slug: string - projectID: string - workspaceID?: string - directory: string - path?: string - parentID?: string - summary?: { - additions: number - deletions: number - files: number - diffs?: Array - } - cost?: number - tokens?: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } - share?: { - url: string - } - title: string - agent?: string - model?: { - id: string - providerID: string - variant?: string - } - version: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - updated: number - compacting?: number - archived?: number - } - permission?: PermissionRuleset - revert?: { - messageID: string - partID?: string - snapshot?: string - diff?: string - } -} - -export type SessionCreatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.created" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - info: SessionV1InfoV2 - } -} - -export type SessionUpdatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.updated" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - info: SessionV1InfoV2 - } -} - -export type SessionDeleted1 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.deleted" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - info: SessionV1InfoV2 - } -} - -export type MessageUpdatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "message.updated" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - info: Message - } -} - -export type MessageRemovedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "message.removed" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - messageID: string - } -} - -export type MessagePartUpdatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "message.part.updated" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - part: Part - time: number - } -} - -export type MessagePartRemovedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "message.part.removed" - durable: { - aggregateID: string - seq: number - version: 1 - } - location?: LocationRefV2 - data: { - sessionID: string - messageID: string - partID: string - } -} - -export type SessionUsageUpdatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.usage.updated" - location?: LocationRefV2 - data: { - sessionID: string - cost: MoneyUsd - tokens: TokenUsageInfo - } -} - -export type SessionTextDeltaV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.text.delta" - location?: LocationRefV2 - data: { - sessionID: string - assistantMessageID: string - ordinal: number - delta: string - } -} - -export type SessionReasoningDeltaV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.reasoning.delta" - location?: LocationRefV2 - data: { - sessionID: string - assistantMessageID: string - ordinal: number - delta: string - } -} - -export type SessionToolInputDeltaV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.tool.input.delta" - location?: LocationRefV2 - data: { - sessionID: string - assistantMessageID: string - callID: string - delta: string - } -} - -export type SessionCompactionDeltaV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.compaction.delta" - location?: LocationRefV2 - data: { - sessionID: string - text: string - } -} - -export type FilesystemChangedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "filesystem.changed" - location?: LocationRefV2 - data: { - file: string - event: "add" | "change" | "unlink" - } -} - -export type ReferenceUpdatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "reference.updated" - location?: LocationRefV2 - data: - | { - [key: string]: unknown - } - | Array -} - -export type PermissionV2AskedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "permission.v2.asked" - location?: LocationRefV2 - data: { - id: string - sessionID: string - action: string - resources: Array - save?: Array - metadata?: { - [key: string]: unknown - } - source?: PermissionV2SourceV2 - } -} - -export type PermissionV2RepliedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "permission.v2.replied" - location?: LocationRefV2 - data: { - sessionID: string - requestID: string - reply: PermissionV2Reply - } -} - -export type PluginAddedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "plugin.added" - location?: LocationRefV2 - data: { - id: string - } -} - -export type PluginUpdatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "plugin.updated" - location?: LocationRefV2 - data: - | { - [key: string]: unknown - } - | Array -} - -export type ProjectDirectoriesUpdatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "project.directories.updated" - location?: LocationRefV2 - data: { - projectID: string - } -} - -export type CommandUpdatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "command.updated" - location?: LocationRefV2 - data: - | { - [key: string]: unknown - } - | Array -} - -export type ConfigUpdatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "config.updated" - location?: LocationRefV2 - data: - | { - [key: string]: unknown - } - | Array -} - -export type SkillUpdatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "skill.updated" - location?: LocationRefV2 - data: - | { - [key: string]: unknown - } - | Array -} - -export type PtyCreatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "pty.created" - location?: LocationRefV2 - data: { - info: PtyV2 - } -} - -export type PtyUpdatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "pty.updated" - location?: LocationRefV2 - data: { - info: PtyV2 - } -} - -export type PtyExitedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "pty.exited" - location?: LocationRefV2 - data: { - id: string - exitCode: number - } -} - -export type PtyDeletedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "pty.deleted" - location?: LocationRefV2 - data: { - id: string - } -} - -export type ShellCreatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "shell.created" - location?: LocationRefV2 - data: { - info: ShellInfoV2 - } -} - -export type ShellExitedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "shell.exited" - location?: LocationRefV2 - data: { - id: string - exit?: number - status: "running" | "exited" | "timeout" | "killed" - } -} - -export type ShellDeletedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "shell.deleted" - location?: LocationRefV2 - data: { - id: string - } -} - -export type QuestionV2AskedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "question.v2.asked" - location?: LocationRefV2 - data: { - id: string - sessionID: string - /** - * Questions to ask - */ - questions: Array - tool?: QuestionV2Tool - } -} - -export type QuestionV2RepliedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "question.v2.replied" - location?: LocationRefV2 - data: { - sessionID: string - requestID: string - answers: Array - } -} - -export type QuestionV2RejectedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "question.v2.rejected" - location?: LocationRefV2 - data: { - sessionID: string - requestID: string - } -} - -export type FormMetadata1 = { - [key: string]: unknown -} - -export type FormStringField1 = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "string" - format?: "email" | "uri" | "date" | "date-time" - minLength?: number - maxLength?: number - pattern?: string - placeholder?: string - default?: string - options?: Array - custom?: boolean -} - -export type FormBooleanField1 = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "boolean" - default?: boolean -} - -export type FormMultiselectField1 = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "multiselect" - options: Array - minItems?: number - maxItems?: number - custom?: boolean - default?: Array -} - -export type FormField1 = - | FormStringField1 - | FormNumberField1 - | FormIntegerField1 - | FormBooleanField1 - | FormMultiselectField1 - | FormExternalField - -export type FormFields1 = [FormField1, FormField1] - -export type FormInfo1 = { - id: string - sessionID: string - title: string - metadata?: FormMetadata1 - fields: FormFields1 -} - -export type FormCreatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "form.created" - location?: LocationRefV2 - data: { - form: FormInfo1 - } -} - -export type FormAnswer1 = { - [key: string]: FormValue1 -} - -export type FormRepliedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "form.replied" - location?: LocationRefV2 - data: { - id: string - sessionID: string - answer: FormAnswer1 - } -} - -export type FormCancelledV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "form.cancelled" - location?: LocationRefV2 - data: { - id: string - sessionID: string - } -} - -export type SessionIdleV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.idle" - location?: LocationRefV2 - data: { - sessionID: string - } -} - -export type TuiPromptAppendV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "tui.prompt.append" - location?: LocationRefV2 - data: { - text: string - } -} - -export type TuiCommandExecuteV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "tui.command.execute" - location?: LocationRefV2 - data: { - command: - | "session.list" - | "session.new" - | "session.share" - | "session.interrupt" - | "session.background" - | "session.compact" - | "session.page.up" - | "session.page.down" - | "session.line.up" - | "session.line.down" - | "session.half.page.up" - | "session.half.page.down" - | "session.first" - | "session.last" - | "prompt.clear" - | "prompt.submit" - | "agent.cycle" - | string - } -} - -export type TuiToastShowV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "tui.toast.show" - location?: LocationRefV2 - data: { - title?: string - message: string - variant: "info" | "success" | "warning" | "error" - duration?: number | null - } -} - -export type TuiSessionSelectV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "tui.session.select" - location?: LocationRefV2 - data: { - /** - * Session ID to navigate to - */ - sessionID: string - } -} - -export type InstallationUpdatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "installation.updated" - location?: LocationRefV2 - data: { - version: string - } -} - -export type InstallationUpdateAvailableV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "installation.update-available" - location?: LocationRefV2 - data: { - version: string - } -} - -export type VcsBranchUpdatedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "vcs.branch.updated" - location?: LocationRefV2 - data: { - branch?: string - } -} - -export type McpStatusChangedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "mcp.status.changed" - location?: LocationRefV2 - data: { - server: string - } -} - -export type McpResourcesChangedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "mcp.resources.changed" - location?: LocationRefV2 - data: { - server: string - } -} - -export type PermissionAskedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "permission.asked" - location?: LocationRefV2 - data: { - id: string - sessionID: string - permission: string - patterns: Array - metadata: { - [key: string]: unknown - } - always: Array - tool?: { - messageID: string - callID: string - } | null - } -} - -export type PermissionRepliedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "permission.replied" - location?: LocationRefV2 - data: { - sessionID: string - requestID: string - reply: "once" | "always" | "reject" - } -} - -export type QuestionAskedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "question.asked" - location?: LocationRefV2 - data: { - id: string - sessionID: string - /** - * Questions to ask - */ - questions: Array - tool?: QuestionToolV2 | null - } -} - -export type QuestionRepliedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "question.replied" - location?: LocationRefV2 - data: { - sessionID: string - requestID: string - answers: Array - } -} - -export type QuestionRejectedV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "question.rejected" - location?: LocationRefV2 - data: { - sessionID: string - requestID: string - } -} - -export type SessionErrorV2 = { - id: string - created: number - metadata?: { - [key: string]: unknown - } - type: "session.error" - location?: LocationRefV2 - data: { - sessionID?: string | null - error?: - | ProviderAuthError - | UnknownError1V2 - | MessageOutputLengthErrorV2 - | MessageAbortedError - | StructuredOutputErrorV2 - | ContextOverflowErrorV2 - | ContentFilterError - | ApiErrorV2 - | null - } -} - -export type V2EventServerConnected = { - id: string - metadata?: { - [key: string]: unknown - } | null - location?: LocationRefV2 | null - type: "server.connected" - data: - | { - [key: string]: unknown - } - | Array -} - -export type PtyTicketConnectTokenV2 = { - ticket: string - expires_in: number -} - -export type ShellInfo1 = { - id: string - status: "running" | "exited" | "timeout" | "killed" - command: string - cwd: string - shell: string - file: string - pid?: number - exit?: number - metadata: { - [key: string]: unknown - } - time: { - started: number - completed?: number - } -} - -export type QuestionV2RequestV2 = { - id: string - sessionID: string - /** - * Questions to ask - */ - questions: Array - tool?: QuestionV2Tool -} - -export type VcsFileStatusV2 = { - file: string - additions: number - deletions: number - status: "added" | "deleted" | "modified" -} - -export type AuthRemoveData = { - body?: never - path: { - providerID: string - } - query?: never - url: "/auth/{providerID}" -} - -export type AuthRemoveErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type AuthRemoveError = AuthRemoveErrors[keyof AuthRemoveErrors] - -export type AuthRemoveResponses = { - /** - * Successfully removed authentication credentials - */ - 200: boolean -} - -export type AuthRemoveResponse = AuthRemoveResponses[keyof AuthRemoveResponses] - -export type AuthSetData = { - body?: Auth - path: { - providerID: string - } - query?: never - url: "/auth/{providerID}" -} - -export type AuthSetErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type AuthSetError = AuthSetErrors[keyof AuthSetErrors] - -export type AuthSetResponses = { - /** - * Successfully set authentication credentials - */ - 200: boolean -} - -export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses] - -export type AppLogData = { - body?: { - /** - * Service name for the log entry - */ - service: string - /** - * Log level - */ - level: "debug" | "info" | "error" | "warn" - /** - * Log message - */ - message: string - extra?: { - [key: string]: unknown - } - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/log" -} - -export type AppLogErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type AppLogError = AppLogErrors[keyof AppLogErrors] - -export type AppLogResponses = { - /** - * Log entry written successfully - */ - 200: boolean -} - -export type AppLogResponse = AppLogResponses[keyof AppLogResponses] - -export type ExperimentalControlPlaneMoveSessionData = { - body?: { - sessionID: string - destination: MoveSessionDestination - moveChanges?: boolean - } - path?: never - query?: never - url: "/experimental/control-plane/move-session" -} - -export type ExperimentalControlPlaneMoveSessionErrors = { - /** - * MoveSessionError | InvalidRequestError - */ - 400: MoveSessionError | InvalidRequestError -} - -export type ExperimentalControlPlaneMoveSessionError = - ExperimentalControlPlaneMoveSessionErrors[keyof ExperimentalControlPlaneMoveSessionErrors] - -export type ExperimentalControlPlaneMoveSessionResponses = { - /** - * Session moved - */ - 204: void -} - -export type ExperimentalControlPlaneMoveSessionResponse = - ExperimentalControlPlaneMoveSessionResponses[keyof ExperimentalControlPlaneMoveSessionResponses] - -export type GlobalHealthData = { - body?: never - path?: never - query?: never - url: "/global/health" -} - -export type GlobalHealthErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type GlobalHealthError = GlobalHealthErrors[keyof GlobalHealthErrors] - -export type GlobalHealthResponses = { - /** - * Health information - */ - 200: { - healthy: true - version: string - } -} - -export type GlobalHealthResponse = GlobalHealthResponses[keyof GlobalHealthResponses] - -export type GlobalEventData = { - body?: never - path?: never - query?: never - url: "/global/event" -} - -export type GlobalEventErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type GlobalEventError = GlobalEventErrors[keyof GlobalEventErrors] - -export type GlobalEventResponses = { - /** - * Event stream - */ - 200: GlobalEvent -} - -export type GlobalEventResponse = GlobalEventResponses[keyof GlobalEventResponses] - -export type GlobalConfigGetData = { - body?: never - path?: never - query?: never - url: "/global/config" -} - -export type GlobalConfigGetErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type GlobalConfigGetError = GlobalConfigGetErrors[keyof GlobalConfigGetErrors] - -export type GlobalConfigGetResponses = { - /** - * Get global config info - */ - 200: Config -} - -export type GlobalConfigGetResponse = GlobalConfigGetResponses[keyof GlobalConfigGetResponses] - -export type GlobalConfigUpdateData = { - body?: Config - path?: never - query?: never - url: "/global/config" -} - -export type GlobalConfigUpdateErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type GlobalConfigUpdateError = GlobalConfigUpdateErrors[keyof GlobalConfigUpdateErrors] - -export type GlobalConfigUpdateResponses = { - /** - * Successfully updated global config - */ - 200: Config -} - -export type GlobalConfigUpdateResponse = GlobalConfigUpdateResponses[keyof GlobalConfigUpdateResponses] - -export type GlobalDisposeData = { - body?: never - path?: never - query?: never - url: "/global/dispose" -} - -export type GlobalDisposeErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type GlobalDisposeError = GlobalDisposeErrors[keyof GlobalDisposeErrors] - -export type GlobalDisposeResponses = { - /** - * Global disposed - */ - 200: boolean -} - -export type GlobalDisposeResponse = GlobalDisposeResponses[keyof GlobalDisposeResponses] - -export type GlobalUpgradeData = { - body?: { - target?: string - } - path?: never - query?: never - url: "/global/upgrade" -} - -export type GlobalUpgradeErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type GlobalUpgradeError = GlobalUpgradeErrors[keyof GlobalUpgradeErrors] - -export type GlobalUpgradeResponses = { - /** - * Upgrade result - */ - 200: - | { - success: true - version: string - } - | { - success: false - error: string - } -} - -export type GlobalUpgradeResponse = GlobalUpgradeResponses[keyof GlobalUpgradeResponses] - -export type EventSubscribeData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/event" -} - -export type EventSubscribeResponses = { - /** - * Event stream - */ - 200: Event -} - -export type EventSubscribeResponse = EventSubscribeResponses[keyof EventSubscribeResponses] - -export type ConfigGetData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/config" -} - -export type ConfigGetErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ConfigGetError = ConfigGetErrors[keyof ConfigGetErrors] - -export type ConfigGetResponses = { - /** - * Get config info - */ - 200: Config -} - -export type ConfigGetResponse = ConfigGetResponses[keyof ConfigGetResponses] - -export type ConfigUpdateData = { - body?: Config - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/config" -} - -export type ConfigUpdateErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type ConfigUpdateError = ConfigUpdateErrors[keyof ConfigUpdateErrors] - -export type ConfigUpdateResponses = { - /** - * Successfully updated config - */ - 200: Config -} - -export type ConfigUpdateResponse = ConfigUpdateResponses[keyof ConfigUpdateResponses] - -export type ConfigProvidersData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/config/providers" -} - -export type ConfigProvidersErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ConfigProvidersError = ConfigProvidersErrors[keyof ConfigProvidersErrors] - -export type ConfigProvidersResponses = { - /** - * List of providers - */ - 200: { - providers: Array - default: { - [key: string]: string - } - } -} - -export type ConfigProvidersResponse = ConfigProvidersResponses[keyof ConfigProvidersResponses] - -export type ExperimentalCapabilitiesGetData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/capabilities" -} - -export type ExperimentalCapabilitiesGetErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ExperimentalCapabilitiesGetError = - ExperimentalCapabilitiesGetErrors[keyof ExperimentalCapabilitiesGetErrors] - -export type ExperimentalCapabilitiesGetResponses = { - /** - * Experimental capabilities - */ - 200: ExperimentalCapabilities -} - -export type ExperimentalCapabilitiesGetResponse = - ExperimentalCapabilitiesGetResponses[keyof ExperimentalCapabilitiesGetResponses] - -export type ExperimentalConsoleGetData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/console" -} - -export type ExperimentalConsoleGetErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * InternalServerError - */ - 500: EffectHttpApiErrorInternalServerError -} - -export type ExperimentalConsoleGetError = ExperimentalConsoleGetErrors[keyof ExperimentalConsoleGetErrors] - -export type ExperimentalConsoleGetResponses = { - /** - * Active Console provider metadata - */ - 200: ConsoleState -} - -export type ExperimentalConsoleGetResponse = ExperimentalConsoleGetResponses[keyof ExperimentalConsoleGetResponses] - -export type ExperimentalConsoleListOrgsData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/console/orgs" -} - -export type ExperimentalConsoleListOrgsErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * InternalServerError - */ - 500: EffectHttpApiErrorInternalServerError -} - -export type ExperimentalConsoleListOrgsError = - ExperimentalConsoleListOrgsErrors[keyof ExperimentalConsoleListOrgsErrors] - -export type ExperimentalConsoleListOrgsResponses = { - /** - * Switchable Console orgs - */ - 200: { - orgs: Array<{ - accountID: string - accountEmail: string - accountUrl: string - orgID: string - orgName: string - active: boolean - }> - } -} - -export type ExperimentalConsoleListOrgsResponse = - ExperimentalConsoleListOrgsResponses[keyof ExperimentalConsoleListOrgsResponses] - -export type ExperimentalConsoleSwitchOrgData = { - body?: { - accountID: string - orgID: string - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/console/switch" -} - -export type ExperimentalConsoleSwitchOrgResponses = { - /** - * Switch success - */ - 200: boolean -} - -export type ExperimentalConsoleSwitchOrgResponse = - ExperimentalConsoleSwitchOrgResponses[keyof ExperimentalConsoleSwitchOrgResponses] - -export type ToolListData = { - body?: never - path?: never - query: { - directory?: string - workspace?: string - provider: string - model: string - } - url: "/experimental/tool" -} - -export type ToolListErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type ToolListError = ToolListErrors[keyof ToolListErrors] - -export type ToolListResponses = { - /** - * Tools - */ - 200: ToolList -} - -export type ToolListResponse = ToolListResponses[keyof ToolListResponses] - -export type ToolIdsData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/tool/ids" -} - -export type ToolIdsErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type ToolIdsError = ToolIdsErrors[keyof ToolIdsErrors] - -export type ToolIdsResponses = { - /** - * Tool IDs - */ - 200: ToolIds -} - -export type ToolIdsResponse = ToolIdsResponses[keyof ToolIdsResponses] - -export type WorktreeRemoveData = { - body?: WorktreeRemoveInput - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/worktree" -} - -export type WorktreeRemoveErrors = { - /** - * WorktreeError | InvalidRequestError - */ - 400: WorktreeError | InvalidRequestError -} - -export type WorktreeRemoveError = WorktreeRemoveErrors[keyof WorktreeRemoveErrors] - -export type WorktreeRemoveResponses = { - /** - * Worktree removed - */ - 200: boolean -} - -export type WorktreeRemoveResponse = WorktreeRemoveResponses[keyof WorktreeRemoveResponses] - -export type WorktreeListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/worktree" -} - -export type WorktreeListErrors = { - /** - * WorktreeError | InvalidRequestError - */ - 400: WorktreeError | InvalidRequestError -} - -export type WorktreeListError = WorktreeListErrors[keyof WorktreeListErrors] - -export type WorktreeListResponses = { - /** - * List of worktree directories - */ - 200: Array -} - -export type WorktreeListResponse = WorktreeListResponses[keyof WorktreeListResponses] - -export type WorktreeCreateData = { - body?: WorktreeCreateInput - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/worktree" -} - -export type WorktreeCreateErrors = { - /** - * WorktreeError | InvalidRequestError - */ - 400: WorktreeError | InvalidRequestError -} - -export type WorktreeCreateError = WorktreeCreateErrors[keyof WorktreeCreateErrors] - -export type WorktreeCreateResponses = { - /** - * Worktree created - */ - 200: Worktree -} - -export type WorktreeCreateResponse = WorktreeCreateResponses[keyof WorktreeCreateResponses] - -export type WorktreeResetData = { - body?: WorktreeResetInput - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/worktree/reset" -} - -export type WorktreeResetErrors = { - /** - * WorktreeError | InvalidRequestError - */ - 400: WorktreeError | InvalidRequestError -} - -export type WorktreeResetError = WorktreeResetErrors[keyof WorktreeResetErrors] - -export type WorktreeResetResponses = { - /** - * Worktree reset - */ - 200: boolean -} - -export type WorktreeResetResponse = WorktreeResetResponses[keyof WorktreeResetResponses] - -export type ExperimentalSessionListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - roots?: boolean | "true" | "false" - start?: number - cursor?: number - search?: string - limit?: number - archived?: boolean | "true" | "false" - } - url: "/experimental/session" -} - -export type ExperimentalSessionListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ExperimentalSessionListError = ExperimentalSessionListErrors[keyof ExperimentalSessionListErrors] - -export type ExperimentalSessionListResponses = { - /** - * List of sessions - */ - 200: Array -} - -export type ExperimentalSessionListResponse = ExperimentalSessionListResponses[keyof ExperimentalSessionListResponses] - -export type ExperimentalSessionBackgroundData = { - body?: never - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/experimental/session/{sessionID}/background" -} - -export type ExperimentalSessionBackgroundErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type ExperimentalSessionBackgroundError = - ExperimentalSessionBackgroundErrors[keyof ExperimentalSessionBackgroundErrors] - -export type ExperimentalSessionBackgroundResponses = { - /** - * Backgrounded subagents - */ - 200: boolean -} - -export type ExperimentalSessionBackgroundResponse = - ExperimentalSessionBackgroundResponses[keyof ExperimentalSessionBackgroundResponses] - -export type ExperimentalResourceListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/resource" -} - -export type ExperimentalResourceListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ExperimentalResourceListError = ExperimentalResourceListErrors[keyof ExperimentalResourceListErrors] - -export type ExperimentalResourceListResponses = { - /** - * MCP resources - */ - 200: { - [key: string]: McpResource - } -} - -export type ExperimentalResourceListResponse = - ExperimentalResourceListResponses[keyof ExperimentalResourceListResponses] - -export type FindTextData = { - body?: never - path?: never - query: { - directory?: string - workspace?: string - pattern: string - } - url: "/find" -} - -export type FindTextErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type FindTextError = FindTextErrors[keyof FindTextErrors] - -export type FindTextResponses = { - /** - * Matches - */ - 200: Array<{ - path: { - text: string - } - lines: { - text: string - } - line_number: number - absolute_offset: number - submatches: Array<{ - match: { - text: string - } - start: number - end: number - }> - }> -} - -export type FindTextResponse = FindTextResponses[keyof FindTextResponses] - -export type FindFilesData = { - body?: never - path?: never - query: { - directory?: string - workspace?: string - query: string - dirs?: "true" | "false" - type?: "file" | "directory" - limit?: number - } - url: "/find/file" -} - -export type FindFilesErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type FindFilesError = FindFilesErrors[keyof FindFilesErrors] - -export type FindFilesResponses = { - /** - * File paths - */ - 200: Array -} - -export type FindFilesResponse = FindFilesResponses[keyof FindFilesResponses] - -export type FindSymbolsData = { - body?: never - path?: never - query: { - directory?: string - workspace?: string - query: string - } - url: "/find/symbol" -} - -export type FindSymbolsErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type FindSymbolsError = FindSymbolsErrors[keyof FindSymbolsErrors] - -export type FindSymbolsResponses = { - /** - * Symbols - */ - 200: Array -} - -export type FindSymbolsResponse = FindSymbolsResponses[keyof FindSymbolsResponses] - -export type FileListData = { - body?: never - path?: never - query: { - directory?: string - workspace?: string - path: string - } - url: "/file" -} - -export type FileListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type FileListError = FileListErrors[keyof FileListErrors] - -export type FileListResponses = { - /** - * Files and directories - */ - 200: Array -} - -export type FileListResponse = FileListResponses[keyof FileListResponses] - -export type FileReadData = { - body?: never - path?: never - query: { - directory?: string - workspace?: string - path: string - } - url: "/file/content" -} - -export type FileReadErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type FileReadError = FileReadErrors[keyof FileReadErrors] - -export type FileReadResponses = { - /** - * File content - */ - 200: FileContent -} - -export type FileReadResponse = FileReadResponses[keyof FileReadResponses] - -export type FileStatusData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/file/status" -} - -export type FileStatusErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type FileStatusError = FileStatusErrors[keyof FileStatusErrors] - -export type FileStatusResponses = { - /** - * File status - */ - 200: Array -} - -export type FileStatusResponse = FileStatusResponses[keyof FileStatusResponses] - -export type InstanceDisposeData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/instance/dispose" -} - -export type InstanceDisposeErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type InstanceDisposeError = InstanceDisposeErrors[keyof InstanceDisposeErrors] - -export type InstanceDisposeResponses = { - /** - * Instance disposed - */ - 200: boolean -} - -export type InstanceDisposeResponse = InstanceDisposeResponses[keyof InstanceDisposeResponses] - -export type PathGetData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/path" -} - -export type PathGetErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type PathGetError = PathGetErrors[keyof PathGetErrors] - -export type PathGetResponses = { - /** - * Path - */ - 200: Path -} - -export type PathGetResponse = PathGetResponses[keyof PathGetResponses] - -export type VcsGetData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/vcs" -} - -export type VcsGetErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type VcsGetError = VcsGetErrors[keyof VcsGetErrors] - -export type VcsGetResponses = { - /** - * VCS info - */ - 200: VcsInfo -} - -export type VcsGetResponse = VcsGetResponses[keyof VcsGetResponses] - -export type VcsStatusData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/vcs/status" -} - -export type VcsStatusErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type VcsStatusError = VcsStatusErrors[keyof VcsStatusErrors] - -export type VcsStatusResponses = { - /** - * VCS status - */ - 200: Array -} - -export type VcsStatusResponse = VcsStatusResponses[keyof VcsStatusResponses] - -export type VcsDiffData = { - body?: never - path?: never - query: { - directory?: string - workspace?: string - mode: "git" | "branch" - context?: number - } - url: "/vcs/diff" -} - -export type VcsDiffErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type VcsDiffError = VcsDiffErrors[keyof VcsDiffErrors] - -export type VcsDiffResponses = { - /** - * VCS diff - */ - 200: Array -} - -export type VcsDiffResponse = VcsDiffResponses[keyof VcsDiffResponses] - -export type VcsDiffRawData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/vcs/diff/raw" -} - -export type VcsDiffRawErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type VcsDiffRawError = VcsDiffRawErrors[keyof VcsDiffRawErrors] - -export type VcsDiffRawResponses = { - /** - * Raw VCS diff - */ - 200: string -} - -export type VcsDiffRawResponse = VcsDiffRawResponses[keyof VcsDiffRawResponses] - -export type VcsApplyData = { - body?: { - patch: string - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/vcs/apply" -} - -export type VcsApplyErrors = { - /** - * VcsApplyError | InvalidRequestError - */ - 400: VcsApplyError | InvalidRequestError -} - -export type VcsApplyError2 = VcsApplyErrors[keyof VcsApplyErrors] - -export type VcsApplyResponses = { - /** - * VCS patch applied - */ - 200: { - applied: boolean - } -} - -export type VcsApplyResponse = VcsApplyResponses[keyof VcsApplyResponses] - -export type CommandListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/command" -} - -export type CommandListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type CommandListError = CommandListErrors[keyof CommandListErrors] - -export type CommandListResponses = { - /** - * List of commands - */ - 200: Array -} - -export type CommandListResponse = CommandListResponses[keyof CommandListResponses] - -export type AppAgentsData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/agent" -} - -export type AppAgentsErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type AppAgentsError = AppAgentsErrors[keyof AppAgentsErrors] - -export type AppAgentsResponses = { - /** - * List of agents - */ - 200: Array -} - -export type AppAgentsResponse = AppAgentsResponses[keyof AppAgentsResponses] - -export type AppSkillsData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/skill" -} - -export type AppSkillsErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type AppSkillsError = AppSkillsErrors[keyof AppSkillsErrors] - -export type AppSkillsResponses = { - /** - * List of skills - */ - 200: Array<{ - name: string - description?: string - location: string - content: string - }> -} - -export type AppSkillsResponse = AppSkillsResponses[keyof AppSkillsResponses] - -export type LspStatusData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/lsp" -} - -export type LspStatusErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type LspStatusError = LspStatusErrors[keyof LspStatusErrors] - -export type LspStatusResponses = { - /** - * LSP server status - */ - 200: Array -} - -export type LspStatusResponse = LspStatusResponses[keyof LspStatusResponses] - -export type FormatterStatusData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/formatter" -} - -export type FormatterStatusErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type FormatterStatusError = FormatterStatusErrors[keyof FormatterStatusErrors] - -export type FormatterStatusResponses = { - /** - * Formatter status - */ - 200: Array -} - -export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses] - -export type McpStatusData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/mcp" -} - -export type McpStatusErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type McpStatusError = McpStatusErrors[keyof McpStatusErrors] - -export type McpStatusResponses = { - /** - * MCP server status - */ - 200: { - [key: string]: McpStatus - } -} - -export type McpStatusResponse = McpStatusResponses[keyof McpStatusResponses] - -export type McpAddData = { - body?: { - name: string - config: McpLocalConfig | McpRemoteConfig - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/mcp" -} - -export type McpAddErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type McpAddError = McpAddErrors[keyof McpAddErrors] - -export type McpAddResponses = { - /** - * MCP server added successfully - */ - 200: { - [key: string]: McpStatus - } -} - -export type McpAddResponse = McpAddResponses[keyof McpAddResponses] - -export type McpAuthRemoveData = { - body?: never - path: { - name: string - } - query?: { - directory?: string - workspace?: string - } - url: "/mcp/{name}/auth" -} - -export type McpAuthRemoveErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * McpServerNotFoundError - */ - 404: McpServerNotFoundError -} - -export type McpAuthRemoveError = McpAuthRemoveErrors[keyof McpAuthRemoveErrors] - -export type McpAuthRemoveResponses = { - /** - * OAuth credentials removed - */ - 200: { - success: true - } -} - -export type McpAuthRemoveResponse = McpAuthRemoveResponses[keyof McpAuthRemoveResponses] - -export type McpAuthStartData = { - body?: never - path: { - name: string - } - query?: { - directory?: string - workspace?: string - } - url: "/mcp/{name}/auth" -} - -export type McpAuthStartErrors = { - /** - * McpUnsupportedOAuthError | InvalidRequestError - */ - 400: McpUnsupportedOAuthError | InvalidRequestError - /** - * McpServerNotFoundError - */ - 404: McpServerNotFoundError -} - -export type McpAuthStartError = McpAuthStartErrors[keyof McpAuthStartErrors] - -export type McpAuthStartResponses = { - /** - * OAuth flow started - */ - 200: { - authorizationUrl: string - oauthState: string - } -} - -export type McpAuthStartResponse = McpAuthStartResponses[keyof McpAuthStartResponses] - -export type McpAuthCallbackData = { - body?: { - code: string - } - path: { - name: string - } - query?: { - directory?: string - workspace?: string - } - url: "/mcp/{name}/auth/callback" -} - -export type McpAuthCallbackErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * McpServerNotFoundError - */ - 404: McpServerNotFoundError -} - -export type McpAuthCallbackError = McpAuthCallbackErrors[keyof McpAuthCallbackErrors] - -export type McpAuthCallbackResponses = { - /** - * OAuth authentication completed - */ - 200: McpStatus -} - -export type McpAuthCallbackResponse = McpAuthCallbackResponses[keyof McpAuthCallbackResponses] - -export type McpAuthAuthenticateData = { - body?: never - path: { - name: string - } - query?: { - directory?: string - workspace?: string - } - url: "/mcp/{name}/auth/authenticate" -} - -export type McpAuthAuthenticateErrors = { - /** - * McpUnsupportedOAuthError | InvalidRequestError - */ - 400: McpUnsupportedOAuthError | InvalidRequestError - /** - * McpServerNotFoundError - */ - 404: McpServerNotFoundError -} - -export type McpAuthAuthenticateError = McpAuthAuthenticateErrors[keyof McpAuthAuthenticateErrors] - -export type McpAuthAuthenticateResponses = { - /** - * OAuth authentication completed - */ - 200: McpStatus -} - -export type McpAuthAuthenticateResponse = McpAuthAuthenticateResponses[keyof McpAuthAuthenticateResponses] - -export type McpConnectData = { - body?: never - path: { - name: string - } - query?: { - directory?: string - workspace?: string - } - url: "/mcp/{name}/connect" -} - -export type McpConnectErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * McpServerNotFoundError - */ - 404: McpServerNotFoundError -} - -export type McpConnectError = McpConnectErrors[keyof McpConnectErrors] - -export type McpConnectResponses = { - /** - * MCP server connected successfully - */ - 200: boolean -} - -export type McpConnectResponse = McpConnectResponses[keyof McpConnectResponses] - -export type McpDisconnectData = { - body?: never - path: { - name: string - } - query?: { - directory?: string - workspace?: string - } - url: "/mcp/{name}/disconnect" -} - -export type McpDisconnectErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * McpServerNotFoundError - */ - 404: McpServerNotFoundError -} - -export type McpDisconnectError = McpDisconnectErrors[keyof McpDisconnectErrors] - -export type McpDisconnectResponses = { - /** - * MCP server disconnected successfully - */ - 200: boolean -} - -export type McpDisconnectResponse = McpDisconnectResponses[keyof McpDisconnectResponses] - -export type ProjectListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/project" -} - -export type ProjectListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ProjectListError = ProjectListErrors[keyof ProjectListErrors] - -export type ProjectListResponses = { - /** - * List of projects - */ - 200: Array -} - -export type ProjectListResponse = ProjectListResponses[keyof ProjectListResponses] - -export type ProjectCurrentData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/project/current" -} - -export type ProjectCurrentErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ProjectCurrentError = ProjectCurrentErrors[keyof ProjectCurrentErrors] - -export type ProjectCurrentResponses = { - /** - * Current project information - */ - 200: Project -} - -export type ProjectCurrentResponse = ProjectCurrentResponses[keyof ProjectCurrentResponses] - -export type ProjectInitGitData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/project/git/init" -} - -export type ProjectInitGitErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ProjectInitGitError = ProjectInitGitErrors[keyof ProjectInitGitErrors] - -export type ProjectInitGitResponses = { - /** - * Project information after git initialization - */ - 200: Project -} - -export type ProjectInitGitResponse = ProjectInitGitResponses[keyof ProjectInitGitResponses] - -export type ProjectUpdateData = { - body?: { - name?: string - icon?: ProjectIcon - commands?: ProjectCommands - } - path: { - projectID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/project/{projectID}" -} - -export type ProjectUpdateErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * ProjectNotFoundError - */ - 404: ProjectNotFoundError -} - -export type ProjectUpdateError = ProjectUpdateErrors[keyof ProjectUpdateErrors] - -export type ProjectUpdateResponses = { - /** - * Updated project information - */ - 200: Project -} - -export type ProjectUpdateResponse = ProjectUpdateResponses[keyof ProjectUpdateResponses] - -export type ProjectDirectoriesData = { - body?: never - path: { - projectID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/project/{projectID}/directories" -} - -export type ProjectDirectoriesErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ProjectDirectoriesError = ProjectDirectoriesErrors[keyof ProjectDirectoriesErrors] - -export type ProjectDirectoriesResponses = { - /** - * Project directories - */ - 200: ProjectDirectories -} - -export type ProjectDirectoriesResponse = ProjectDirectoriesResponses[keyof ProjectDirectoriesResponses] - -export type ExperimentalProjectCopyGenerateNameData = { - body?: { - context?: string - } - path: { - projectID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/experimental/project/{projectID}/copy/generate-name" -} - -export type ExperimentalProjectCopyGenerateNameErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ExperimentalProjectCopyGenerateNameError = - ExperimentalProjectCopyGenerateNameErrors[keyof ExperimentalProjectCopyGenerateNameErrors] - -export type ExperimentalProjectCopyGenerateNameResponses = { - /** - * Success - */ - 200: { - name: string - } -} - -export type ExperimentalProjectCopyGenerateNameResponse = - ExperimentalProjectCopyGenerateNameResponses[keyof ExperimentalProjectCopyGenerateNameResponses] - -export type PtyShellsData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/pty/shells" -} - -export type PtyShellsErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type PtyShellsError = PtyShellsErrors[keyof PtyShellsErrors] - -export type PtyShellsResponses = { - /** - * List of shells - */ - 200: Array<{ - path: string - name: string - acceptable: boolean - }> -} - -export type PtyShellsResponse = PtyShellsResponses[keyof PtyShellsResponses] - -export type PtyListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/pty" -} - -export type PtyListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type PtyListError = PtyListErrors[keyof PtyListErrors] - -export type PtyListResponses = { - /** - * List of sessions - */ - 200: Array -} - -export type PtyListResponse = PtyListResponses[keyof PtyListResponses] - -export type PtyCreateData = { - body?: { - command?: string - args?: Array - cwd?: string - title?: string - env?: { - [key: string]: string - } - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/pty" -} - -export type PtyCreateErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type PtyCreateError = PtyCreateErrors[keyof PtyCreateErrors] - -export type PtyCreateResponses = { - /** - * Created session - */ - 200: Pty -} - -export type PtyCreateResponse = PtyCreateResponses[keyof PtyCreateResponses] - -export type PtyRemoveData = { - body?: never - path: { - ptyID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/pty/{ptyID}" -} - -export type PtyRemoveErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * PtyNotFoundError - */ - 404: PtyNotFoundError -} - -export type PtyRemoveError = PtyRemoveErrors[keyof PtyRemoveErrors] - -export type PtyRemoveResponses = { - /** - * Session removed - */ - 200: boolean -} - -export type PtyRemoveResponse = PtyRemoveResponses[keyof PtyRemoveResponses] - -export type PtyGetData = { - body?: never - path: { - ptyID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/pty/{ptyID}" -} - -export type PtyGetErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * PtyNotFoundError - */ - 404: PtyNotFoundError -} - -export type PtyGetError = PtyGetErrors[keyof PtyGetErrors] - -export type PtyGetResponses = { - /** - * Session info - */ - 200: Pty -} - -export type PtyGetResponse = PtyGetResponses[keyof PtyGetResponses] - -export type PtyUpdateData = { - body?: { - title?: string - size?: { - rows: number - cols: number - } - } - path: { - ptyID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/pty/{ptyID}" -} - -export type PtyUpdateErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * PtyNotFoundError - */ - 404: PtyNotFoundError -} - -export type PtyUpdateError = PtyUpdateErrors[keyof PtyUpdateErrors] - -export type PtyUpdateResponses = { - /** - * Updated session - */ - 200: Pty -} - -export type PtyUpdateResponse = PtyUpdateResponses[keyof PtyUpdateResponses] - -export type PtyConnectTokenData = { - body?: never - path: { - ptyID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/pty/{ptyID}/connect-token" -} - -export type PtyConnectTokenErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * PtyForbiddenError - */ - 403: PtyForbiddenError - /** - * PtyNotFoundError - */ - 404: PtyNotFoundError -} - -export type PtyConnectTokenError = PtyConnectTokenErrors[keyof PtyConnectTokenErrors] - -export type PtyConnectTokenResponses = { - /** - * WebSocket connect token - */ - 200: PtyTicketConnectToken -} - -export type PtyConnectTokenResponse = PtyConnectTokenResponses[keyof PtyConnectTokenResponses] - -export type QuestionListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/question" -} - -export type QuestionListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type QuestionListError = QuestionListErrors[keyof QuestionListErrors] - -export type QuestionListResponses = { - /** - * List of pending questions - */ - 200: Array -} - -export type QuestionListResponse = QuestionListResponses[keyof QuestionListResponses] - -export type QuestionReplyData = { - body?: { - /** - * User answers in order of questions (each answer is an array of selected labels) - */ - answers: Array - } - path: { - requestID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/question/{requestID}/reply" -} - -export type QuestionReplyErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * QuestionNotFoundError - */ - 404: QuestionNotFoundError -} - -export type QuestionReplyError = QuestionReplyErrors[keyof QuestionReplyErrors] - -export type QuestionReplyResponses = { - /** - * Question answered successfully - */ - 200: boolean -} - -export type QuestionReplyResponse = QuestionReplyResponses[keyof QuestionReplyResponses] - -export type QuestionRejectData = { - body?: never - path: { - requestID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/question/{requestID}/reject" -} - -export type QuestionRejectErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * QuestionNotFoundError - */ - 404: QuestionNotFoundError -} - -export type QuestionRejectError = QuestionRejectErrors[keyof QuestionRejectErrors] - -export type QuestionRejectResponses = { - /** - * Question rejected successfully - */ - 200: boolean -} - -export type QuestionRejectResponse = QuestionRejectResponses[keyof QuestionRejectResponses] - -export type PermissionListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/permission" -} - -export type PermissionListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type PermissionListError = PermissionListErrors[keyof PermissionListErrors] - -export type PermissionListResponses = { - /** - * List of pending permissions - */ - 200: Array -} - -export type PermissionListResponse = PermissionListResponses[keyof PermissionListResponses] - -export type PermissionReplyData = { - body?: { - reply: "once" | "always" | "reject" - message?: string - } - path: { - requestID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/permission/{requestID}/reply" -} - -export type PermissionReplyErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * PermissionNotFoundError - */ - 404: PermissionNotFoundError -} - -export type PermissionReplyError = PermissionReplyErrors[keyof PermissionReplyErrors] - -export type PermissionReplyResponses = { - /** - * Permission processed successfully - */ - 200: boolean -} - -export type PermissionReplyResponse = PermissionReplyResponses[keyof PermissionReplyResponses] - -export type ProviderListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/provider" -} - -export type ProviderListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ProviderListError = ProviderListErrors[keyof ProviderListErrors] - -export type ProviderListResponses = { - /** - * List of providers - */ - 200: { - all: Array - default: { - [key: string]: string - } - connected: Array - } -} - -export type ProviderListResponse = ProviderListResponses[keyof ProviderListResponses] - -export type ProviderAuthData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/provider/auth" -} - -export type ProviderAuthErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ProviderAuthError2 = ProviderAuthErrors[keyof ProviderAuthErrors] - -export type ProviderAuthResponses = { - /** - * Provider auth methods - */ - 200: { - [key: string]: Array - } -} - -export type ProviderAuthResponse = ProviderAuthResponses[keyof ProviderAuthResponses] - -export type ProviderOauthAuthorizeData = { - body?: { - /** - * Auth method index - */ - method: number - inputs?: { - [key: string]: string - } - } - path: { - providerID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/provider/{providerID}/oauth/authorize" -} - -export type ProviderOauthAuthorizeErrors = { - /** - * ProviderAuthError | InvalidRequestError - */ - 400: ProviderAuthError1 | InvalidRequestError -} - -export type ProviderOauthAuthorizeError = ProviderOauthAuthorizeErrors[keyof ProviderOauthAuthorizeErrors] - -export type ProviderOauthAuthorizeResponses = { - /** - * Authorization URL and method - */ - 200: ProviderAuthAuthorization -} - -export type ProviderOauthAuthorizeResponse = ProviderOauthAuthorizeResponses[keyof ProviderOauthAuthorizeResponses] - -export type ProviderOauthCallbackData = { - body?: { - /** - * Auth method index - */ - method: number - code?: string - } - path: { - providerID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/provider/{providerID}/oauth/callback" -} - -export type ProviderOauthCallbackErrors = { - /** - * ProviderAuthError | InvalidRequestError - */ - 400: ProviderAuthError1 | InvalidRequestError -} - -export type ProviderOauthCallbackError = ProviderOauthCallbackErrors[keyof ProviderOauthCallbackErrors] - -export type ProviderOauthCallbackResponses = { - /** - * OAuth callback processed successfully - */ - 200: boolean -} - -export type ProviderOauthCallbackResponse = ProviderOauthCallbackResponses[keyof ProviderOauthCallbackResponses] - -export type SessionListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - scope?: "project" - path?: string - roots?: boolean | "true" | "false" - start?: number - search?: string - limit?: number - } - url: "/session" -} - -export type SessionListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type SessionListError = SessionListErrors[keyof SessionListErrors] - -export type SessionListResponses = { - /** - * List of sessions - */ - 200: Array -} - -export type SessionListResponse = SessionListResponses[keyof SessionListResponses] - -export type SessionCreateData = { - body?: { - parentID?: string - title?: string - agent?: string - model?: { - id: string - providerID: string - variant?: string - } - metadata?: { - [key: string]: unknown - } - permission?: PermissionRuleset - workspaceID?: string - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/session" -} - -export type SessionCreateErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type SessionCreateError = SessionCreateErrors[keyof SessionCreateErrors] - -export type SessionCreateResponses = { - /** - * Successfully created session - */ - 200: Session -} - -export type SessionCreateResponse = SessionCreateResponses[keyof SessionCreateResponses] - -export type SessionStatusData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/session/status" -} - -export type SessionStatusErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type SessionStatusError = SessionStatusErrors[keyof SessionStatusErrors] - -export type SessionStatusResponses = { - /** - * Get session status - */ - 200: { - [key: string]: SessionStatus - } -} - -export type SessionStatusResponse = SessionStatusResponses[keyof SessionStatusResponses] - -export type SessionDeleteData = { - body?: never - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}" -} - -export type SessionDeleteErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * NotFoundError - */ - 404: NotFoundError -} - -export type SessionDeleteError = SessionDeleteErrors[keyof SessionDeleteErrors] - -export type SessionDeleteResponses = { - /** - * Successfully deleted session - */ - 200: boolean -} - -export type SessionDeleteResponse = SessionDeleteResponses[keyof SessionDeleteResponses] - -export type SessionGetData = { - body?: never - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}" -} - -export type SessionGetErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * NotFoundError - */ - 404: NotFoundError -} - -export type SessionGetError = SessionGetErrors[keyof SessionGetErrors] - -export type SessionGetResponses = { - /** - * Get session - */ - 200: Session -} - -export type SessionGetResponse = SessionGetResponses[keyof SessionGetResponses] - -export type SessionUpdateData = { - body?: { - title?: string - metadata?: { - [key: string]: unknown - } - permission?: PermissionRuleset - time?: { - archived?: number - } - } - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}" -} - -export type SessionUpdateErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * NotFoundError - */ - 404: NotFoundError -} - -export type SessionUpdateError = SessionUpdateErrors[keyof SessionUpdateErrors] - -export type SessionUpdateResponses = { - /** - * Successfully updated session - */ - 200: Session -} - -export type SessionUpdateResponse = SessionUpdateResponses[keyof SessionUpdateResponses] - -export type SessionChildrenData = { - body?: never - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}/children" -} - -export type SessionChildrenErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * NotFoundError - */ - 404: NotFoundError -} - -export type SessionChildrenError = SessionChildrenErrors[keyof SessionChildrenErrors] - -export type SessionChildrenResponses = { - /** - * List of children - */ - 200: Array -} - -export type SessionChildrenResponse = SessionChildrenResponses[keyof SessionChildrenResponses] - -export type SessionDiffData = { - body?: never - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - messageID?: string - } - url: "/session/{sessionID}/diff" -} - -export type SessionDiffErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type SessionDiffError = SessionDiffErrors[keyof SessionDiffErrors] - -export type SessionDiffResponses = { - /** - * Successfully retrieved diff - */ - 200: Array -} - -export type SessionDiffResponse = SessionDiffResponses[keyof SessionDiffResponses] - -export type SessionMessagesData = { - body?: never - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - limit?: number - before?: string - } - url: "/session/{sessionID}/message" -} - -export type SessionMessagesErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * NotFoundError - */ - 404: NotFoundError -} - -export type SessionMessagesError = SessionMessagesErrors[keyof SessionMessagesErrors] - -export type SessionMessagesResponses = { - /** - * List of messages - */ - 200: Array<{ - info: Message - parts: Array - }> -} - -export type SessionMessagesResponse2 = SessionMessagesResponses[keyof SessionMessagesResponses] - -export type SessionPromptData = { - body?: { - messageID?: string - model?: { - providerID: string - modelID: string - } - agent?: string - noReply?: boolean - tools?: { - [key: string]: boolean - } - format?: OutputFormat - system?: string - variant?: string - parts: Array - } - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}/message" -} - -export type SessionPromptErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * NotFoundError - */ - 404: NotFoundError -} - -export type SessionPromptError = SessionPromptErrors[keyof SessionPromptErrors] - -export type SessionPromptResponses = { - /** - * Created message - */ - 200: { - info: AssistantMessage - parts: Array - } -} - -export type SessionPromptResponse = SessionPromptResponses[keyof SessionPromptResponses] - -export type SessionDeleteMessageData = { - body?: never - path: { - sessionID: string - messageID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}/message/{messageID}" -} - -export type SessionDeleteMessageErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * NotFoundError - */ - 404: NotFoundError - /** - * SessionBusyError - */ - 409: SessionBusyError -} - -export type SessionDeleteMessageError = SessionDeleteMessageErrors[keyof SessionDeleteMessageErrors] - -export type SessionDeleteMessageResponses = { - /** - * Successfully deleted message - */ - 200: boolean -} - -export type SessionDeleteMessageResponse = SessionDeleteMessageResponses[keyof SessionDeleteMessageResponses] - -export type SessionMessageData = { - body?: never - path: { - sessionID: string - messageID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}/message/{messageID}" -} - -export type SessionMessageErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * NotFoundError - */ - 404: NotFoundError -} - -export type SessionMessageError = SessionMessageErrors[keyof SessionMessageErrors] - -export type SessionMessageResponses = { - /** - * Message - */ - 200: { - info: Message - parts: Array - } -} - -export type SessionMessageResponse = SessionMessageResponses[keyof SessionMessageResponses] - -export type SessionForkData = { - body?: { - messageID?: string - } - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}/fork" -} - -export type SessionForkErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * NotFoundError - */ - 404: NotFoundError -} - -export type SessionForkError = SessionForkErrors[keyof SessionForkErrors] - -export type SessionForkResponses = { - /** - * 200 - */ - 200: Session -} - -export type SessionForkResponse = SessionForkResponses[keyof SessionForkResponses] - -export type SessionAbortData = { - body?: never - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}/abort" -} - -export type SessionAbortErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type SessionAbortError = SessionAbortErrors[keyof SessionAbortErrors] - -export type SessionAbortResponses = { - /** - * Aborted session - */ - 200: boolean -} - -export type SessionAbortResponse = SessionAbortResponses[keyof SessionAbortResponses] - -export type SessionInitData = { - body?: { - modelID: string - providerID: string - messageID: string - } - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}/init" -} - -export type SessionInitErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * NotFoundError - */ - 404: NotFoundError -} - -export type SessionInitError = SessionInitErrors[keyof SessionInitErrors] - -export type SessionInitResponses = { - /** - * 200 - */ - 200: boolean -} - -export type SessionInitResponse = SessionInitResponses[keyof SessionInitResponses] - -export type SessionUnshareData = { - body?: never - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}/share" -} - -export type SessionUnshareErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * NotFoundError - */ - 404: NotFoundError - /** - * InternalServerError - */ - 500: EffectHttpApiErrorInternalServerError -} - -export type SessionUnshareError = SessionUnshareErrors[keyof SessionUnshareErrors] - -export type SessionUnshareResponses = { - /** - * Successfully unshared session - */ - 200: Session -} - -export type SessionUnshareResponse = SessionUnshareResponses[keyof SessionUnshareResponses] - -export type SessionShareData = { - body?: never - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}/share" -} - -export type SessionShareErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * NotFoundError - */ - 404: NotFoundError - /** - * InternalServerError - */ - 500: EffectHttpApiErrorInternalServerError -} - -export type SessionShareError = SessionShareErrors[keyof SessionShareErrors] - -export type SessionShareResponses = { - /** - * Successfully shared session - */ - 200: Session -} - -export type SessionShareResponse = SessionShareResponses[keyof SessionShareResponses] - -export type SessionSummarizeData = { - body?: { - providerID: string - modelID: string - auto?: boolean - } - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}/summarize" -} - -export type SessionSummarizeErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * NotFoundError - */ - 404: NotFoundError -} - -export type SessionSummarizeError = SessionSummarizeErrors[keyof SessionSummarizeErrors] - -export type SessionSummarizeResponses = { - /** - * Summarized session - */ - 200: boolean -} - -export type SessionSummarizeResponse = SessionSummarizeResponses[keyof SessionSummarizeResponses] - -export type SessionPromptAsyncData = { - body?: { - messageID?: string - model?: { - providerID: string - modelID: string - } - agent?: string - noReply?: boolean - tools?: { - [key: string]: boolean - } - format?: OutputFormat - system?: string - variant?: string - parts: Array - } - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}/prompt_async" -} - -export type SessionPromptAsyncErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * NotFoundError - */ - 404: NotFoundError -} - -export type SessionPromptAsyncError = SessionPromptAsyncErrors[keyof SessionPromptAsyncErrors] - -export type SessionPromptAsyncResponses = { - /** - * Prompt accepted - */ - 204: void -} - -export type SessionPromptAsyncResponse = SessionPromptAsyncResponses[keyof SessionPromptAsyncResponses] - -export type SessionCommandData = { - body?: { - messageID?: string - agent?: string - model?: string - arguments: string - command: string - variant?: string - parts?: Array<{ - id?: string - type: "file" - mime: string - filename?: string - url: string - source?: FilePartSource - }> - } - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}/command" -} - -export type SessionCommandErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * NotFoundError - */ - 404: NotFoundError -} - -export type SessionCommandError = SessionCommandErrors[keyof SessionCommandErrors] - -export type SessionCommandResponses = { - /** - * Created message - */ - 200: { - info: AssistantMessage - parts: Array - } -} - -export type SessionCommandResponse = SessionCommandResponses[keyof SessionCommandResponses] - -export type SessionShellData = { - body?: { - messageID?: string - agent: string - model?: { - providerID: string - modelID: string - } - command: string - } - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}/shell" -} - -export type SessionShellErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * NotFoundError - */ - 404: NotFoundError - /** - * SessionBusyError - */ - 409: SessionBusyError -} - -export type SessionShellError = SessionShellErrors[keyof SessionShellErrors] - -export type SessionShellResponses = { - /** - * Created message - */ - 200: { - info: Message - parts: Array - } -} - -export type SessionShellResponse = SessionShellResponses[keyof SessionShellResponses] - -export type SessionRevertData = { - body?: { - messageID: string - partID?: string - } - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}/revert" -} - -export type SessionRevertErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * NotFoundError - */ - 404: NotFoundError - /** - * SessionBusyError - */ - 409: SessionBusyError -} - -export type SessionRevertError = SessionRevertErrors[keyof SessionRevertErrors] - -export type SessionRevertResponses = { - /** - * Updated session - */ - 200: Session -} - -export type SessionRevertResponse = SessionRevertResponses[keyof SessionRevertResponses] - -export type SessionUnrevertData = { - body?: never - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}/unrevert" -} - -export type SessionUnrevertErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * NotFoundError - */ - 404: NotFoundError - /** - * SessionBusyError - */ - 409: SessionBusyError -} - -export type SessionUnrevertError = SessionUnrevertErrors[keyof SessionUnrevertErrors] - -export type SessionUnrevertResponses = { - /** - * Updated session - */ - 200: Session -} - -export type SessionUnrevertResponse = SessionUnrevertResponses[keyof SessionUnrevertResponses] - -export type PermissionRespondData = { - body?: { - response: "once" | "always" | "reject" - } - path: { - sessionID: string - permissionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}/permissions/{permissionID}" -} - -export type PermissionRespondErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * NotFoundError | PermissionNotFoundError - */ - 404: NotFoundError | PermissionNotFoundError -} - -export type PermissionRespondError = PermissionRespondErrors[keyof PermissionRespondErrors] - -export type PermissionRespondResponses = { - /** - * Permission processed successfully - */ - 200: boolean -} - -export type PermissionRespondResponse = PermissionRespondResponses[keyof PermissionRespondResponses] - -export type PartDeleteData = { - body?: never - path: { - sessionID: string - messageID: string - partID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}/message/{messageID}/part/{partID}" -} - -export type PartDeleteErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * NotFoundError - */ - 404: NotFoundError -} - -export type PartDeleteError = PartDeleteErrors[keyof PartDeleteErrors] - -export type PartDeleteResponses = { - /** - * Successfully deleted part - */ - 200: boolean -} - -export type PartDeleteResponse = PartDeleteResponses[keyof PartDeleteResponses] - -export type PartUpdateData = { - body?: Part - path: { - sessionID: string - messageID: string - partID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/session/{sessionID}/message/{messageID}/part/{partID}" -} - -export type PartUpdateErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * NotFoundError - */ - 404: NotFoundError -} - -export type PartUpdateError = PartUpdateErrors[keyof PartUpdateErrors] - -export type PartUpdateResponses = { - /** - * Successfully updated part - */ - 200: Part -} - -export type PartUpdateResponse = PartUpdateResponses[keyof PartUpdateResponses] - -export type SyncStartData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/sync/start" -} - -export type SyncStartErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type SyncStartError = SyncStartErrors[keyof SyncStartErrors] - -export type SyncStartResponses = { - /** - * Workspace sync started - */ - 200: boolean -} - -export type SyncStartResponse = SyncStartResponses[keyof SyncStartResponses] - -export type SyncReplayData = { - body?: { - directory: string - events: Array<{ - id: string - aggregateID: string - seq: number - type: string - data: { - [key: string]: unknown - } - }> - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/sync/replay" -} - -export type SyncReplayErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type SyncReplayError = SyncReplayErrors[keyof SyncReplayErrors] - -export type SyncReplayResponses = { - /** - * Replayed sync events - */ - 200: { - sessionID: string - } -} - -export type SyncReplayResponse = SyncReplayResponses[keyof SyncReplayResponses] - -export type SyncStealData = { - body?: { - sessionID: string - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/sync/steal" -} - -export type SyncStealErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type SyncStealError = SyncStealErrors[keyof SyncStealErrors] - -export type SyncStealResponses = { - /** - * Session stolen into workspace - */ - 200: { - sessionID: string - } -} - -export type SyncStealResponse = SyncStealResponses[keyof SyncStealResponses] - -export type SyncHistoryListData = { - body?: { - [key: string]: number - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/sync/history" -} - -export type SyncHistoryListErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type SyncHistoryListError = SyncHistoryListErrors[keyof SyncHistoryListErrors] - -export type SyncHistoryListResponses = { - /** - * Sync events - */ - 200: Array<{ - id: string - aggregate_id: string - seq: number - type: string - data: { - [key: string]: unknown - } - }> -} - -export type SyncHistoryListResponse = SyncHistoryListResponses[keyof SyncHistoryListResponses] - -export type TuiAppendPromptData = { - body?: { - text: string - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/tui/append-prompt" -} - -export type TuiAppendPromptErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type TuiAppendPromptError = TuiAppendPromptErrors[keyof TuiAppendPromptErrors] - -export type TuiAppendPromptResponses = { - /** - * Prompt processed successfully - */ - 200: boolean -} - -export type TuiAppendPromptResponse = TuiAppendPromptResponses[keyof TuiAppendPromptResponses] - -export type TuiOpenHelpData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/tui/open-help" -} - -export type TuiOpenHelpErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiOpenHelpError = TuiOpenHelpErrors[keyof TuiOpenHelpErrors] - -export type TuiOpenHelpResponses = { - /** - * Help dialog opened successfully - */ - 200: boolean -} - -export type TuiOpenHelpResponse = TuiOpenHelpResponses[keyof TuiOpenHelpResponses] - -export type TuiOpenSessionsData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/tui/open-sessions" -} - -export type TuiOpenSessionsErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiOpenSessionsError = TuiOpenSessionsErrors[keyof TuiOpenSessionsErrors] - -export type TuiOpenSessionsResponses = { - /** - * Session dialog opened successfully - */ - 200: boolean -} - -export type TuiOpenSessionsResponse = TuiOpenSessionsResponses[keyof TuiOpenSessionsResponses] - -export type TuiOpenThemesData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/tui/open-themes" -} - -export type TuiOpenThemesErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiOpenThemesError = TuiOpenThemesErrors[keyof TuiOpenThemesErrors] - -export type TuiOpenThemesResponses = { - /** - * Theme dialog opened successfully - */ - 200: boolean -} - -export type TuiOpenThemesResponse = TuiOpenThemesResponses[keyof TuiOpenThemesResponses] - -export type TuiOpenModelsData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/tui/open-models" -} - -export type TuiOpenModelsErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiOpenModelsError = TuiOpenModelsErrors[keyof TuiOpenModelsErrors] - -export type TuiOpenModelsResponses = { - /** - * Model dialog opened successfully - */ - 200: boolean -} - -export type TuiOpenModelsResponse = TuiOpenModelsResponses[keyof TuiOpenModelsResponses] - -export type TuiSubmitPromptData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/tui/submit-prompt" -} - -export type TuiSubmitPromptErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiSubmitPromptError = TuiSubmitPromptErrors[keyof TuiSubmitPromptErrors] - -export type TuiSubmitPromptResponses = { - /** - * Prompt submitted successfully - */ - 200: boolean -} - -export type TuiSubmitPromptResponse = TuiSubmitPromptResponses[keyof TuiSubmitPromptResponses] - -export type TuiClearPromptData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/tui/clear-prompt" -} - -export type TuiClearPromptErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiClearPromptError = TuiClearPromptErrors[keyof TuiClearPromptErrors] - -export type TuiClearPromptResponses = { - /** - * Prompt cleared successfully - */ - 200: boolean -} - -export type TuiClearPromptResponse = TuiClearPromptResponses[keyof TuiClearPromptResponses] - -export type TuiExecuteCommandData = { - body?: { - command: string - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/tui/execute-command" -} - -export type TuiExecuteCommandErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type TuiExecuteCommandError = TuiExecuteCommandErrors[keyof TuiExecuteCommandErrors] - -export type TuiExecuteCommandResponses = { - /** - * Command executed successfully - */ - 200: boolean -} - -export type TuiExecuteCommandResponse = TuiExecuteCommandResponses[keyof TuiExecuteCommandResponses] - -export type TuiShowToastData = { - body?: { - title?: string - message: string - variant: "info" | "success" | "warning" | "error" - duration?: number - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/tui/show-toast" -} - -export type TuiShowToastErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiShowToastError = TuiShowToastErrors[keyof TuiShowToastErrors] - -export type TuiShowToastResponses = { - /** - * Toast notification shown successfully - */ - 200: boolean -} - -export type TuiShowToastResponse = TuiShowToastResponses[keyof TuiShowToastResponses] - -export type TuiPublishData = { - body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/tui/publish" -} - -export type TuiPublishErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type TuiPublishError = TuiPublishErrors[keyof TuiPublishErrors] - -export type TuiPublishResponses = { - /** - * Event published successfully - */ - 200: boolean -} - -export type TuiPublishResponse = TuiPublishResponses[keyof TuiPublishResponses] - -export type TuiSelectSessionData = { - body?: { - /** - * Session ID to navigate to - */ - sessionID: string - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/tui/select-session" -} - -export type TuiSelectSessionErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * NotFoundError - */ - 404: NotFoundError -} - -export type TuiSelectSessionError = TuiSelectSessionErrors[keyof TuiSelectSessionErrors] - -export type TuiSelectSessionResponses = { - /** - * Session selected successfully - */ - 200: boolean -} - -export type TuiSelectSessionResponse = TuiSelectSessionResponses[keyof TuiSelectSessionResponses] - -export type TuiControlNextData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/tui/control/next" -} - -export type TuiControlNextErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiControlNextError = TuiControlNextErrors[keyof TuiControlNextErrors] - -export type TuiControlNextResponses = { - /** - * Next TUI request - */ - 200: { - path: string - body: unknown - } -} - -export type TuiControlNextResponse = TuiControlNextResponses[keyof TuiControlNextResponses] - -export type TuiControlResponseData = { - body?: unknown - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/tui/control/response" -} - -export type TuiControlResponseErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type TuiControlResponseError = TuiControlResponseErrors[keyof TuiControlResponseErrors] - -export type TuiControlResponseResponses = { - /** - * Response submitted successfully - */ - 200: boolean -} - -export type TuiControlResponseResponse = TuiControlResponseResponses[keyof TuiControlResponseResponses] - -export type ExperimentalWorkspaceAdapterListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/workspace/adapter" -} - -export type ExperimentalWorkspaceAdapterListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ExperimentalWorkspaceAdapterListError = - ExperimentalWorkspaceAdapterListErrors[keyof ExperimentalWorkspaceAdapterListErrors] - -export type ExperimentalWorkspaceAdapterListResponses = { - /** - * Workspace adapters - */ - 200: Array<{ - type: string - name: string - description: string - }> -} - -export type ExperimentalWorkspaceAdapterListResponse = - ExperimentalWorkspaceAdapterListResponses[keyof ExperimentalWorkspaceAdapterListResponses] - -export type ExperimentalWorkspaceListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/workspace" -} - -export type ExperimentalWorkspaceListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ExperimentalWorkspaceListError = ExperimentalWorkspaceListErrors[keyof ExperimentalWorkspaceListErrors] - -export type ExperimentalWorkspaceListResponses = { - /** - * Workspaces - */ - 200: Array -} - -export type ExperimentalWorkspaceListResponse = - ExperimentalWorkspaceListResponses[keyof ExperimentalWorkspaceListResponses] - -export type ExperimentalWorkspaceCreateData = { - body?: { - id?: string - type: string - branch?: string | null - extra?: unknown | null - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/workspace" -} - -export type ExperimentalWorkspaceCreateErrors = { - /** - * WorkspaceCreateError | BadRequest | InvalidRequestError - */ - 400: WorkspaceCreateError | EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type ExperimentalWorkspaceCreateError = - ExperimentalWorkspaceCreateErrors[keyof ExperimentalWorkspaceCreateErrors] - -export type ExperimentalWorkspaceCreateResponses = { - /** - * Workspace created - */ - 200: Workspace -} - -export type ExperimentalWorkspaceCreateResponse = - ExperimentalWorkspaceCreateResponses[keyof ExperimentalWorkspaceCreateResponses] - -export type ExperimentalWorkspaceSyncListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/workspace/sync-list" -} - -export type ExperimentalWorkspaceSyncListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ExperimentalWorkspaceSyncListError = - ExperimentalWorkspaceSyncListErrors[keyof ExperimentalWorkspaceSyncListErrors] - -export type ExperimentalWorkspaceSyncListResponses = { - /** - * Workspace list synced - */ - 204: void -} - -export type ExperimentalWorkspaceSyncListResponse = - ExperimentalWorkspaceSyncListResponses[keyof ExperimentalWorkspaceSyncListResponses] - -export type ExperimentalWorkspaceStatusData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/workspace/status" -} - -export type ExperimentalWorkspaceStatusErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ExperimentalWorkspaceStatusError = - ExperimentalWorkspaceStatusErrors[keyof ExperimentalWorkspaceStatusErrors] - -export type ExperimentalWorkspaceStatusResponses = { - /** - * Workspace status - */ - 200: Array -} - -export type ExperimentalWorkspaceStatusResponse = - ExperimentalWorkspaceStatusResponses[keyof ExperimentalWorkspaceStatusResponses] - -export type ExperimentalWorkspaceRemoveData = { - body?: never - path: { - id: string - } - query?: { - directory?: string - workspace?: string - } - url: "/experimental/workspace/{id}" -} - -export type ExperimentalWorkspaceRemoveErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError -} - -export type ExperimentalWorkspaceRemoveError = - ExperimentalWorkspaceRemoveErrors[keyof ExperimentalWorkspaceRemoveErrors] - -export type ExperimentalWorkspaceRemoveResponses = { - /** - * Workspace removed - */ - 200: Workspace -} - -export type ExperimentalWorkspaceRemoveResponse = - ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses] - -export type ExperimentalWorkspaceWarpData = { - body?: { - id: string | null - sessionID: string - copyChanges?: boolean - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/workspace/warp" -} - -export type ExperimentalWorkspaceWarpErrors = { - /** - * WorkspaceWarpError | VcsApplyError | InvalidRequestError - */ - 400: WorkspaceWarpError | VcsApplyError | InvalidRequestError - /** - * NotFoundError - */ - 404: NotFoundError -} - -export type ExperimentalWorkspaceWarpError = ExperimentalWorkspaceWarpErrors[keyof ExperimentalWorkspaceWarpErrors] - -export type ExperimentalWorkspaceWarpResponses = { - /** - * Session warped - */ - 204: void -} - -export type ExperimentalWorkspaceWarpResponse = - ExperimentalWorkspaceWarpResponses[keyof ExperimentalWorkspaceWarpResponses] - -export type V2HealthGetData = { - body?: never - path?: never - query?: never - url: "/api/health" -} - -export type V2HealthGetErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2HealthGetError = V2HealthGetErrors[keyof V2HealthGetErrors] - -export type V2HealthGetResponses = { - /** - * ServiceHealth - */ - 200: ServiceHealthV2 -} - -export type V2HealthGetResponse = V2HealthGetResponses[keyof V2HealthGetResponses] - -export type V2HealthStopData = { - body: ServiceStopRequest - path?: never - query?: never - url: "/api/service/stop" -} - -export type V2HealthStopErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2HealthStopError = V2HealthStopErrors[keyof V2HealthStopErrors] - -export type V2HealthStopResponses = { - /** - * ServiceStopResponse - */ - 200: ServiceStopResponse -} - -export type V2HealthStopResponse = V2HealthStopResponses[keyof V2HealthStopResponses] - -export type V2ServerGetData = { - body?: never - path?: never - query?: never - url: "/api/server" -} - -export type V2ServerGetErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2ServerGetError = V2ServerGetErrors[keyof V2ServerGetErrors] - -export type V2ServerGetResponses = { - /** - * Success - */ - 200: { - urls: Array - } -} - -export type V2ServerGetResponse = V2ServerGetResponses[keyof V2ServerGetResponses] - -export type V2LocationGetData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/location" -} - -export type V2LocationGetErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2LocationGetError = V2LocationGetErrors[keyof V2LocationGetErrors] - -export type V2LocationGetResponses = { - /** - * Location.Info - */ - 200: LocationInfoV2 -} - -export type V2LocationGetResponse = V2LocationGetResponses[keyof V2LocationGetResponses] - -export type V2AgentListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/agent" -} - -export type V2AgentListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2AgentListError = V2AgentListErrors[keyof V2AgentListErrors] - -export type V2AgentListResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: Array - } -} - -export type V2AgentListResponse = V2AgentListResponses[keyof V2AgentListResponses] - -export type V2PluginListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/plugin" -} - -export type V2PluginListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2PluginListError = V2PluginListErrors[keyof V2PluginListErrors] - -export type V2PluginListResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: Array - } -} - -export type V2PluginListResponse = V2PluginListResponses[keyof V2PluginListResponses] - -export type V2SessionListData = { - body?: never - path?: never - query?: { - workspace?: string | null - /** - * Maximum number of sessions to return. Defaults to the newest 50 sessions. - */ - limit?: number | null - /** - * Session order for the first page. Use desc for newest first or asc for oldest first. - */ - order?: "asc" | "desc" | null - search?: string | null - parentID?: string | "null" | null - directory?: string | null - project?: string | null - subpath?: string | null - cursor?: string | null - } - url: "/api/session" -} - -export type V2SessionListErrors = { - /** - * InvalidCursorError | InvalidRequestError - */ - 400: InvalidCursorError | InvalidRequestError1 | InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2SessionListError = V2SessionListErrors[keyof V2SessionListErrors] - -export type V2SessionListResponses = { - /** - * SessionsResponse - */ - 200: SessionsResponseV2 -} - -export type V2SessionListResponse = V2SessionListResponses[keyof V2SessionListResponses] - -export type V2SessionCreateData = { - body: { - id?: string | null - agent?: string | null - model?: ModelRef | null - location?: LocationRefV2 | null - } - path?: never - query?: never - url: "/api/session" -} - -export type V2SessionCreateErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2SessionCreateError = V2SessionCreateErrors[keyof V2SessionCreateErrors] - -export type V2SessionCreateResponses = { - /** - * Success - */ - 200: { - data: SessionInfoV2 - } -} - -export type V2SessionCreateResponse = V2SessionCreateResponses[keyof V2SessionCreateResponses] - -export type V2SessionActiveData = { - body?: never - path?: never - query?: never - url: "/api/session/active" -} - -export type V2SessionActiveErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2SessionActiveError = V2SessionActiveErrors[keyof V2SessionActiveErrors] - -export type V2SessionActiveResponses = { - /** - * Success - */ - 200: { - data: { - [key: string]: unknown | SessionActive - } - } -} - -export type V2SessionActiveResponse = V2SessionActiveResponses[keyof V2SessionActiveResponses] - -export type V2SessionRemoveData = { - body?: never - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}" -} - -export type V2SessionRemoveErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError -} - -export type V2SessionRemoveError = V2SessionRemoveErrors[keyof V2SessionRemoveErrors] - -export type V2SessionRemoveResponses = { - /** - * - */ - 204: void -} - -export type V2SessionRemoveResponse = V2SessionRemoveResponses[keyof V2SessionRemoveResponses] - -export type V2SessionGetData = { - body?: never - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}" -} - -export type V2SessionGetErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError -} - -export type V2SessionGetError = V2SessionGetErrors[keyof V2SessionGetErrors] - -export type V2SessionGetResponses = { - /** - * Success - */ - 200: { - data: SessionInfoV2 - } -} - -export type V2SessionGetResponse = V2SessionGetResponses[keyof V2SessionGetResponses] - -export type V2SessionForkData = { - body: { - messageID?: string | null - } - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/fork" -} - -export type V2SessionForkErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError | MessageNotFoundError - */ - 404: MessageNotFoundError | SessionNotFoundError -} - -export type V2SessionForkError = V2SessionForkErrors[keyof V2SessionForkErrors] - -export type V2SessionForkResponses = { - /** - * Success - */ - 200: { - data: SessionInfoV2 - } -} - -export type V2SessionForkResponse = V2SessionForkResponses[keyof V2SessionForkResponses] - -export type V2SessionSwitchAgentData = { - body: { - agent: string - } - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/agent" -} - -export type V2SessionSwitchAgentErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError -} - -export type V2SessionSwitchAgentError = V2SessionSwitchAgentErrors[keyof V2SessionSwitchAgentErrors] - -export type V2SessionSwitchAgentResponses = { - /** - * - */ - 204: void -} - -export type V2SessionSwitchAgentResponse = V2SessionSwitchAgentResponses[keyof V2SessionSwitchAgentResponses] - -export type V2SessionSwitchModelData = { - body: { - model: ModelRef - } - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/model" -} - -export type V2SessionSwitchModelErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError -} - -export type V2SessionSwitchModelError = V2SessionSwitchModelErrors[keyof V2SessionSwitchModelErrors] - -export type V2SessionSwitchModelResponses = { - /** - * - */ - 204: void -} - -export type V2SessionSwitchModelResponse = V2SessionSwitchModelResponses[keyof V2SessionSwitchModelResponses] - -export type V2SessionRenameData = { - body: { - title: string - } - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/rename" -} - -export type V2SessionRenameErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError -} - -export type V2SessionRenameError = V2SessionRenameErrors[keyof V2SessionRenameErrors] - -export type V2SessionRenameResponses = { - /** - * - */ - 204: void -} - -export type V2SessionRenameResponse = V2SessionRenameResponses[keyof V2SessionRenameResponses] - -export type V2SessionMoveData = { - body: LocationRefV2 - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/move" -} - -export type V2SessionMoveErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError1 | InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError -} - -export type V2SessionMoveError = V2SessionMoveErrors[keyof V2SessionMoveErrors] - -export type V2SessionMoveResponses = { - /** - * - */ - 204: void -} - -export type V2SessionMoveResponse = V2SessionMoveResponses[keyof V2SessionMoveResponses] - -export type V2SessionPromptData = { - body: { - id?: string | null - text: string - files?: Array - agents?: Array - metadata?: { - [key: string]: unknown - } - delivery?: "steer" | "queue" | null - resume?: boolean | null - } - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/prompt" -} - -export type V2SessionPromptErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError1 | InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError - /** - * ConflictError - */ - 409: ConflictErrorV2 -} - -export type V2SessionPromptError = V2SessionPromptErrors[keyof V2SessionPromptErrors] - -export type V2SessionPromptResponses = { - /** - * Success - */ - 200: { - data: SessionPendingUserV2 - } -} - -export type V2SessionPromptResponse = V2SessionPromptResponses[keyof V2SessionPromptResponses] - -export type V2SessionCommandData = { - body: { - id?: string | null - command: string - arguments?: string | null - agent?: string | null - model?: ModelRef | null - files?: Array - agents?: Array - delivery?: "steer" | "queue" | null - resume?: boolean | null - } - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/command" -} - -export type V2SessionCommandErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError1 | InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError | CommandNotFoundError - */ - 404: CommandNotFoundError | SessionNotFoundError - /** - * ConflictError - */ - 409: ConflictErrorV2 - /** - * CommandEvaluationError - */ - 500: CommandEvaluationError -} - -export type V2SessionCommandError = V2SessionCommandErrors[keyof V2SessionCommandErrors] - -export type V2SessionCommandResponses = { - /** - * Success - */ - 200: { - data: SessionPendingUserV2 - } -} - -export type V2SessionCommandResponse = V2SessionCommandResponses[keyof V2SessionCommandResponses] - -export type V2SessionSkillData = { - body: { - id?: string | null - skill: string - resume?: boolean | null - } - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/skill" -} - -export type V2SessionSkillErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError | SkillNotFoundError - */ - 404: SkillNotFoundError | SessionNotFoundError -} - -export type V2SessionSkillError = V2SessionSkillErrors[keyof V2SessionSkillErrors] - -export type V2SessionSkillResponses = { - /** - * - */ - 204: void -} - -export type V2SessionSkillResponse = V2SessionSkillResponses[keyof V2SessionSkillResponses] - -export type V2SessionSyntheticData = { - body: { - id?: string | null - text: string - description?: string | null - metadata?: { - [key: string]: unknown - } - delivery?: "steer" | "queue" | null - resume?: boolean | null - } - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/synthetic" -} - -export type V2SessionSyntheticErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError - /** - * ConflictError - */ - 409: ConflictErrorV2 -} - -export type V2SessionSyntheticError = V2SessionSyntheticErrors[keyof V2SessionSyntheticErrors] - -export type V2SessionSyntheticResponses = { - /** - * Success - */ - 200: { - data: SessionPendingSyntheticV2 - } -} - -export type V2SessionSyntheticResponse = V2SessionSyntheticResponses[keyof V2SessionSyntheticResponses] - -export type V2SessionShellData = { - body: { - id?: string | null - command: string - } - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/shell" -} - -export type V2SessionShellErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError -} - -export type V2SessionShellError = V2SessionShellErrors[keyof V2SessionShellErrors] - -export type V2SessionShellResponses = { - /** - * - */ - 204: void -} - -export type V2SessionShellResponse = V2SessionShellResponses[keyof V2SessionShellResponses] - -export type V2SessionCompactData = { - body: { - id?: string | null - } - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/compact" -} - -export type V2SessionCompactErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError - /** - * ConflictError - */ - 409: ConflictErrorV2 -} - -export type V2SessionCompactError = V2SessionCompactErrors[keyof V2SessionCompactErrors] - -export type V2SessionCompactResponses = { - /** - * Success - */ - 200: { - data: SessionPendingCompactionV2 - } -} - -export type V2SessionCompactResponse = V2SessionCompactResponses[keyof V2SessionCompactResponses] - -export type V2SessionWaitData = { - body?: never - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/wait" -} - -export type V2SessionWaitErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError - /** - * ServiceUnavailableError - */ - 503: ServiceUnavailableErrorV2 -} - -export type V2SessionWaitError = V2SessionWaitErrors[keyof V2SessionWaitErrors] - -export type V2SessionWaitResponses = { - /** - * - */ - 204: void -} - -export type V2SessionWaitResponse = V2SessionWaitResponses[keyof V2SessionWaitResponses] - -export type V2SessionRevertStageData = { - body: { - messageID: string - files?: boolean | null - } - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/revert/stage" -} - -export type V2SessionRevertStageErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * MessageNotFoundError | SessionNotFoundError - */ - 404: MessageNotFoundError | SessionNotFoundError - /** - * SessionBusyError - */ - 409: SessionBusyError - /** - * UnknownError - */ - 500: UnknownErrorV2 -} - -export type V2SessionRevertStageError = V2SessionRevertStageErrors[keyof V2SessionRevertStageErrors] - -export type V2SessionRevertStageResponses = { - /** - * Success - */ - 200: { - data: SessionRevertV2 - } -} - -export type V2SessionRevertStageResponse = V2SessionRevertStageResponses[keyof V2SessionRevertStageResponses] - -export type V2SessionRevertClearData = { - body?: never - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/revert/clear" -} - -export type V2SessionRevertClearErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError - /** - * SessionBusyError - */ - 409: SessionBusyError - /** - * UnknownError - */ - 500: UnknownErrorV2 -} - -export type V2SessionRevertClearError = V2SessionRevertClearErrors[keyof V2SessionRevertClearErrors] - -export type V2SessionRevertClearResponses = { - /** - * - */ - 204: void -} - -export type V2SessionRevertClearResponse = V2SessionRevertClearResponses[keyof V2SessionRevertClearResponses] - -export type V2SessionRevertCommitData = { - body?: never - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/revert/commit" -} - -export type V2SessionRevertCommitErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError - /** - * SessionBusyError - */ - 409: SessionBusyError -} - -export type V2SessionRevertCommitError = V2SessionRevertCommitErrors[keyof V2SessionRevertCommitErrors] - -export type V2SessionRevertCommitResponses = { - /** - * - */ - 204: void -} - -export type V2SessionRevertCommitResponse = V2SessionRevertCommitResponses[keyof V2SessionRevertCommitResponses] - -export type V2SessionContextData = { - body?: never - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/context" -} - -export type V2SessionContextErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError - /** - * UnknownError - */ - 500: UnknownErrorV2 -} - -export type V2SessionContextError = V2SessionContextErrors[keyof V2SessionContextErrors] - -export type V2SessionContextResponses = { - /** - * Success - */ - 200: { - data: Array - } -} - -export type V2SessionContextResponse = V2SessionContextResponses[keyof V2SessionContextResponses] - -export type V2SessionPendingListData = { - body?: never - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/pending" -} - -export type V2SessionPendingListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError -} - -export type V2SessionPendingListError = V2SessionPendingListErrors[keyof V2SessionPendingListErrors] - -export type V2SessionPendingListResponses = { - /** - * Success - */ - 200: { - data: Array - } -} - -export type V2SessionPendingListResponse = V2SessionPendingListResponses[keyof V2SessionPendingListResponses] - -export type V2SessionInstructionsEntryListData = { - body?: never - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/instructions/entries" -} - -export type V2SessionInstructionsEntryListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError -} - -export type V2SessionInstructionsEntryListError = - V2SessionInstructionsEntryListErrors[keyof V2SessionInstructionsEntryListErrors] - -export type V2SessionInstructionsEntryListResponses = { - /** - * Success - */ - 200: { - data: Array - } -} - -export type V2SessionInstructionsEntryListResponse = - V2SessionInstructionsEntryListResponses[keyof V2SessionInstructionsEntryListResponses] - -export type V2SessionInstructionsEntryRemoveData = { - body?: never - path: { - sessionID: string - key: InstructionEntryKeyV2 - } - query?: never - url: "/api/session/{sessionID}/instructions/entries/{key}" -} - -export type V2SessionInstructionsEntryRemoveErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError -} - -export type V2SessionInstructionsEntryRemoveError = - V2SessionInstructionsEntryRemoveErrors[keyof V2SessionInstructionsEntryRemoveErrors] - -export type V2SessionInstructionsEntryRemoveResponses = { - /** - * - */ - 204: void -} - -export type V2SessionInstructionsEntryRemoveResponse = - V2SessionInstructionsEntryRemoveResponses[keyof V2SessionInstructionsEntryRemoveResponses] - -export type V2SessionInstructionsEntryPutData = { - body: { - value: unknown - } - path: { - sessionID: string - key: InstructionEntryKeyV2 - } - query?: never - url: "/api/session/{sessionID}/instructions/entries/{key}" -} - -export type V2SessionInstructionsEntryPutErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError - /** - * InstructionEntryValueTooLargeError - */ - 413: InstructionEntryValueTooLargeError -} - -export type V2SessionInstructionsEntryPutError = - V2SessionInstructionsEntryPutErrors[keyof V2SessionInstructionsEntryPutErrors] - -export type V2SessionInstructionsEntryPutResponses = { - /** - * - */ - 204: void -} - -export type V2SessionInstructionsEntryPutResponse = - V2SessionInstructionsEntryPutResponses[keyof V2SessionInstructionsEntryPutResponses] - -export type V2SessionGenerateData = { - body: { - prompt: string - } - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/generate" -} - -export type V2SessionGenerateErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError - /** - * ServiceUnavailableError - */ - 503: ServiceUnavailableErrorV2 -} - -export type V2SessionGenerateError = V2SessionGenerateErrors[keyof V2SessionGenerateErrors] - -export type V2SessionGenerateResponses = { - /** - * SessionGenerateResponse - */ - 200: SessionGenerateResponse -} - -export type V2SessionGenerateResponse = V2SessionGenerateResponses[keyof V2SessionGenerateResponses] - -export type V2SessionLogData = { - body?: never - path: { - sessionID: string - } - query?: { - after?: number | null - follow?: "true" | "false" | null - } - url: "/api/experimental/session/{sessionID}/log" -} - -export type V2SessionLogErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError -} - -export type V2SessionLogError = V2SessionLogErrors[keyof V2SessionLogErrors] - -export type V2SessionLogResponses = { - /** - * Success - */ - 200: { - id: string | null - event: string - data: SessionLogItemJsonString - } -} - -export type V2SessionLogResponse = V2SessionLogResponses[keyof V2SessionLogResponses] - -export type V2SessionInterruptData = { - body?: never - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/interrupt" -} - -export type V2SessionInterruptErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError -} - -export type V2SessionInterruptError = V2SessionInterruptErrors[keyof V2SessionInterruptErrors] - -export type V2SessionInterruptResponses = { - /** - * - */ - 204: void -} - -export type V2SessionInterruptResponse = V2SessionInterruptResponses[keyof V2SessionInterruptResponses] - -export type V2SessionBackgroundData = { - body?: never - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/background" -} - -export type V2SessionBackgroundErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError -} - -export type V2SessionBackgroundError = V2SessionBackgroundErrors[keyof V2SessionBackgroundErrors] - -export type V2SessionBackgroundResponses = { - /** - * - */ - 204: void -} - -export type V2SessionBackgroundResponse = V2SessionBackgroundResponses[keyof V2SessionBackgroundResponses] - -export type V2SessionMessageData = { - body?: never - path: { - sessionID: string - messageID: string - } - query?: never - url: "/api/session/{sessionID}/message/{messageID}" -} - -export type V2SessionMessageErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError | MessageNotFoundError - */ - 404: MessageNotFoundError | SessionNotFoundError -} - -export type V2SessionMessageError = V2SessionMessageErrors[keyof V2SessionMessageErrors] - -export type V2SessionMessageResponses = { - /** - * Success - */ - 200: { - data: SessionMessageInfo - } -} - -export type V2SessionMessageResponse = V2SessionMessageResponses[keyof V2SessionMessageResponses] - -export type V2MessageListData = { - body?: never - path: { - sessionID: string - } - query?: { - /** - * Maximum number of messages to return. When omitted, the endpoint returns its default page size. - */ - limit?: number | null - /** - * Message order for the first page. Use desc for newest first or asc for oldest first. - */ - order?: "asc" | "desc" | null - cursor?: string | null - } - url: "/api/session/{sessionID}/message" -} - -export type V2MessageListErrors = { - /** - * InvalidCursorError | InvalidRequestError - */ - 400: InvalidCursorError | InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError - /** - * UnknownError - */ - 500: UnknownErrorV2 -} - -export type V2MessageListError = V2MessageListErrors[keyof V2MessageListErrors] - -export type V2MessageListResponses = { - /** - * SessionMessagesResponse - */ - 200: SessionMessagesResponseV2 -} - -export type V2MessageListResponse = V2MessageListResponses[keyof V2MessageListResponses] - -export type V2ModelListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/model" -} - -export type V2ModelListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * ServiceUnavailableError - */ - 503: ServiceUnavailableErrorV2 -} - -export type V2ModelListError = V2ModelListErrors[keyof V2ModelListErrors] - -export type V2ModelListResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: Array - } -} - -export type V2ModelListResponse = V2ModelListResponses[keyof V2ModelListResponses] - -export type V2ModelDefaultData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/model/default" -} - -export type V2ModelDefaultErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * ServiceUnavailableError - */ - 503: ServiceUnavailableErrorV2 -} - -export type V2ModelDefaultError = V2ModelDefaultErrors[keyof V2ModelDefaultErrors] - -export type V2ModelDefaultResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: ModelInfo | null - } -} - -export type V2ModelDefaultResponse = V2ModelDefaultResponses[keyof V2ModelDefaultResponses] - -export type V2GenerateTextData = { - body: { - prompt: string - model?: ModelRef | null - } - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/generate" -} - -export type V2GenerateTextErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError1 | InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * ServiceUnavailableError - */ - 503: ServiceUnavailableErrorV2 -} - -export type V2GenerateTextError = V2GenerateTextErrors[keyof V2GenerateTextErrors] - -export type V2GenerateTextResponses = { - /** - * GenerateTextResponse - */ - 200: GenerateTextResponse -} - -export type V2GenerateTextResponse = V2GenerateTextResponses[keyof V2GenerateTextResponses] - -export type V2ProviderListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/provider" -} - -export type V2ProviderListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * ServiceUnavailableError - */ - 503: ServiceUnavailableErrorV2 -} - -export type V2ProviderListError = V2ProviderListErrors[keyof V2ProviderListErrors] - -export type V2ProviderListResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: Array - } -} - -export type V2ProviderListResponse = V2ProviderListResponses[keyof V2ProviderListResponses] - -export type V2ProviderGetData = { - body?: never - path: { - providerID: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/provider/{providerID}" -} - -export type V2ProviderGetErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * ProviderNotFoundError - */ - 404: ProviderNotFoundError - /** - * ServiceUnavailableError - */ - 503: ServiceUnavailableErrorV2 -} - -export type V2ProviderGetError = V2ProviderGetErrors[keyof V2ProviderGetErrors] - -export type V2ProviderGetResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: ProviderV2Info - } -} - -export type V2ProviderGetResponse = V2ProviderGetResponses[keyof V2ProviderGetResponses] - -export type V2IntegrationListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/integration" -} - -export type V2IntegrationListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2IntegrationListError = V2IntegrationListErrors[keyof V2IntegrationListErrors] - -export type V2IntegrationListResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: Array - } -} - -export type V2IntegrationListResponse = V2IntegrationListResponses[keyof V2IntegrationListResponses] - -export type V2IntegrationGetData = { - body?: never - path: { - integrationID: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/integration/{integrationID}" -} - -export type V2IntegrationGetErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2IntegrationGetError = V2IntegrationGetErrors[keyof V2IntegrationGetErrors] - -export type V2IntegrationGetResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: IntegrationInfo | null - } -} - -export type V2IntegrationGetResponse = V2IntegrationGetResponses[keyof V2IntegrationGetResponses] - -export type V2ExperimentalIntegrationWellknownAddData = { - body: { - url: string - } - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/experimental/integration/wellknown" -} - -export type V2ExperimentalIntegrationWellknownAddErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError1 | InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2ExperimentalIntegrationWellknownAddError = - V2ExperimentalIntegrationWellknownAddErrors[keyof V2ExperimentalIntegrationWellknownAddErrors] - -export type V2ExperimentalIntegrationWellknownAddResponses = { - /** - * - */ - 204: void -} - -export type V2ExperimentalIntegrationWellknownAddResponse = - V2ExperimentalIntegrationWellknownAddResponses[keyof V2ExperimentalIntegrationWellknownAddResponses] - -export type V2IntegrationConnectKeyData = { - body: { - key: string - label?: string | null - } - path: { - integrationID: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/integration/{integrationID}/connect/key" -} - -export type V2IntegrationConnectKeyErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError1 | InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2IntegrationConnectKeyError = V2IntegrationConnectKeyErrors[keyof V2IntegrationConnectKeyErrors] - -export type V2IntegrationConnectKeyResponses = { - /** - * - */ - 204: void -} - -export type V2IntegrationConnectKeyResponse = V2IntegrationConnectKeyResponses[keyof V2IntegrationConnectKeyResponses] - -export type V2IntegrationOauthConnectData = { - body: { - methodID: string - inputs: { - [key: string]: string - } - label?: string | null - } - path: { - integrationID: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/integration/{integrationID}/connect/oauth" -} - -export type V2IntegrationOauthConnectErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError1 | InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2IntegrationOauthConnectError = V2IntegrationOauthConnectErrors[keyof V2IntegrationOauthConnectErrors] - -export type V2IntegrationOauthConnectResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: IntegrationAttempt - } -} - -export type V2IntegrationOauthConnectResponse = - V2IntegrationOauthConnectResponses[keyof V2IntegrationOauthConnectResponses] - -export type V2IntegrationOauthCancelData = { - body?: never - path: { - integrationID: string - attemptID: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/integration/{integrationID}/connect/oauth/{attemptID}" -} - -export type V2IntegrationOauthCancelErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2IntegrationOauthCancelError = V2IntegrationOauthCancelErrors[keyof V2IntegrationOauthCancelErrors] - -export type V2IntegrationOauthCancelResponses = { - /** - * - */ - 204: void -} - -export type V2IntegrationOauthCancelResponse = - V2IntegrationOauthCancelResponses[keyof V2IntegrationOauthCancelResponses] - -export type V2IntegrationOauthStatusData = { - body?: never - path: { - integrationID: string - attemptID: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/integration/{integrationID}/connect/oauth/{attemptID}" -} - -export type V2IntegrationOauthStatusErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2IntegrationOauthStatusError = V2IntegrationOauthStatusErrors[keyof V2IntegrationOauthStatusErrors] - -export type V2IntegrationOauthStatusResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: IntegrationAttemptStatus - } -} - -export type V2IntegrationOauthStatusResponse = - V2IntegrationOauthStatusResponses[keyof V2IntegrationOauthStatusResponses] - -export type V2IntegrationOauthCompleteData = { - body: { - code?: string | null - } - path: { - integrationID: string - attemptID: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/integration/{integrationID}/connect/oauth/{attemptID}/complete" -} - -export type V2IntegrationOauthCompleteErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError1 | InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2IntegrationOauthCompleteError = V2IntegrationOauthCompleteErrors[keyof V2IntegrationOauthCompleteErrors] - -export type V2IntegrationOauthCompleteResponses = { - /** - * - */ - 204: void -} - -export type V2IntegrationOauthCompleteResponse = - V2IntegrationOauthCompleteResponses[keyof V2IntegrationOauthCompleteResponses] - -export type V2IntegrationCommandConnectData = { - body: { - methodID: string - label?: string | null - } - path: { - integrationID: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/integration/{integrationID}/connect/command" -} - -export type V2IntegrationCommandConnectErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError1 | InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2IntegrationCommandConnectError = - V2IntegrationCommandConnectErrors[keyof V2IntegrationCommandConnectErrors] - -export type V2IntegrationCommandConnectResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: IntegrationCommandAttempt - } -} - -export type V2IntegrationCommandConnectResponse = - V2IntegrationCommandConnectResponses[keyof V2IntegrationCommandConnectResponses] - -export type V2IntegrationCommandCancelData = { - body?: never - path: { - integrationID: string - attemptID: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/integration/{integrationID}/connect/command/{attemptID}" -} - -export type V2IntegrationCommandCancelErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2IntegrationCommandCancelError = V2IntegrationCommandCancelErrors[keyof V2IntegrationCommandCancelErrors] - -export type V2IntegrationCommandCancelResponses = { - /** - * - */ - 204: void -} - -export type V2IntegrationCommandCancelResponse = - V2IntegrationCommandCancelResponses[keyof V2IntegrationCommandCancelResponses] - -export type V2IntegrationCommandStatusData = { - body?: never - path: { - integrationID: string - attemptID: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/integration/{integrationID}/connect/command/{attemptID}" -} - -export type V2IntegrationCommandStatusErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2IntegrationCommandStatusError = V2IntegrationCommandStatusErrors[keyof V2IntegrationCommandStatusErrors] - -export type V2IntegrationCommandStatusResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: IntegrationCommandAttemptStatus - } -} - -export type V2IntegrationCommandStatusResponse = - V2IntegrationCommandStatusResponses[keyof V2IntegrationCommandStatusResponses] - -export type V2McpListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/mcp" -} - -export type V2McpListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2McpListError = V2McpListErrors[keyof V2McpListErrors] - -export type V2McpListResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: Array - } -} - -export type V2McpListResponse = V2McpListResponses[keyof V2McpListResponses] - -export type V2McpResourceCatalogData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/mcp/resource" -} - -export type V2McpResourceCatalogErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2McpResourceCatalogError = V2McpResourceCatalogErrors[keyof V2McpResourceCatalogErrors] - -export type V2McpResourceCatalogResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: McpResourceCatalogV2 - } -} - -export type V2McpResourceCatalogResponse = V2McpResourceCatalogResponses[keyof V2McpResourceCatalogResponses] - -export type V2CredentialRemoveData = { - body?: never - path: { - credentialID: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/credential/{credentialID}" -} - -export type V2CredentialRemoveErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2CredentialRemoveError = V2CredentialRemoveErrors[keyof V2CredentialRemoveErrors] - -export type V2CredentialRemoveResponses = { - /** - * - */ - 204: void -} - -export type V2CredentialRemoveResponse = V2CredentialRemoveResponses[keyof V2CredentialRemoveResponses] - -export type V2CredentialUpdateData = { - body: { - label: string - } - path: { - credentialID: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/credential/{credentialID}" -} - -export type V2CredentialUpdateErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2CredentialUpdateError = V2CredentialUpdateErrors[keyof V2CredentialUpdateErrors] - -export type V2CredentialUpdateResponses = { - /** - * - */ - 204: void -} - -export type V2CredentialUpdateResponse = V2CredentialUpdateResponses[keyof V2CredentialUpdateResponses] - -export type V2ProjectListData = { - body?: never - path?: never - query?: never - url: "/api/project" -} - -export type V2ProjectListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2ProjectListError = V2ProjectListErrors[keyof V2ProjectListErrors] - -export type V2ProjectListResponses = { - /** - * Success - */ - 200: Array -} - -export type V2ProjectListResponse = V2ProjectListResponses[keyof V2ProjectListResponses] - -export type V2ProjectCurrentData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/project/current" -} - -export type V2ProjectCurrentErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2ProjectCurrentError = V2ProjectCurrentErrors[keyof V2ProjectCurrentErrors] - -export type V2ProjectCurrentResponses = { - /** - * Project.Current - */ - 200: ProjectCurrent -} - -export type V2ProjectCurrentResponse = V2ProjectCurrentResponses[keyof V2ProjectCurrentResponses] - -export type V2ProjectDirectoriesData = { - body?: never - path: { - projectID: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/project/{projectID}/directories" -} - -export type V2ProjectDirectoriesErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2ProjectDirectoriesError = V2ProjectDirectoriesErrors[keyof V2ProjectDirectoriesErrors] - -export type V2ProjectDirectoriesResponses = { - /** - * Project.Directories - */ - 200: ProjectDirectories -} - -export type V2ProjectDirectoriesResponse = V2ProjectDirectoriesResponses[keyof V2ProjectDirectoriesResponses] - -export type V2FormRequestListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/form/request" -} - -export type V2FormRequestListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2FormRequestListError = V2FormRequestListErrors[keyof V2FormRequestListErrors] - -export type V2FormRequestListResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: Array - } -} - -export type V2FormRequestListResponse = V2FormRequestListResponses[keyof V2FormRequestListResponses] - -export type V2SessionFormListData = { - body?: never - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/form" -} - -export type V2SessionFormListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError -} - -export type V2SessionFormListError = V2SessionFormListErrors[keyof V2SessionFormListErrors] - -export type V2SessionFormListResponses = { - /** - * Success - */ - 200: { - data: Array - } -} - -export type V2SessionFormListResponse = V2SessionFormListResponses[keyof V2SessionFormListResponses] - -export type V2SessionFormCreateData = { - body: FormCreatePayloadV2 - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/form" -} - -export type V2SessionFormCreateErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError1 | InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError - /** - * ConflictError - */ - 409: ConflictErrorV2 -} - -export type V2SessionFormCreateError = V2SessionFormCreateErrors[keyof V2SessionFormCreateErrors] - -export type V2SessionFormCreateResponses = { - /** - * Success - */ - 200: { - data: FormInfoV2 - } -} - -export type V2SessionFormCreateResponse = V2SessionFormCreateResponses[keyof V2SessionFormCreateResponses] - -export type V2SessionFormGetData = { - body?: never - path: { - sessionID: string - formID: string - } - query?: never - url: "/api/session/{sessionID}/form/{formID}" -} - -export type V2SessionFormGetErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError | FormNotFoundError - */ - 404: FormNotFoundError | SessionNotFoundError -} - -export type V2SessionFormGetError = V2SessionFormGetErrors[keyof V2SessionFormGetErrors] - -export type V2SessionFormGetResponses = { - /** - * Success - */ - 200: { - data: FormInfoV2 - } -} - -export type V2SessionFormGetResponse = V2SessionFormGetResponses[keyof V2SessionFormGetResponses] - -export type V2SessionFormStateData = { - body?: never - path: { - sessionID: string - formID: string - } - query?: never - url: "/api/session/{sessionID}/form/{formID}/state" -} - -export type V2SessionFormStateErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError | FormNotFoundError - */ - 404: FormNotFoundError | SessionNotFoundError -} - -export type V2SessionFormStateError = V2SessionFormStateErrors[keyof V2SessionFormStateErrors] - -export type V2SessionFormStateResponses = { - /** - * Success - */ - 200: { - data: FormState - } -} - -export type V2SessionFormStateResponse = V2SessionFormStateResponses[keyof V2SessionFormStateResponses] - -export type V2SessionFormReplyData = { - body: FormReply - path: { - sessionID: string - formID: string - } - query?: never - url: "/api/session/{sessionID}/form/{formID}/reply" -} - -export type V2SessionFormReplyErrors = { - /** - * FormInvalidAnswerError | InvalidRequestError - */ - 400: FormInvalidAnswerError | InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError | FormNotFoundError - */ - 404: FormNotFoundError | SessionNotFoundError - /** - * FormAlreadySettledError - */ - 409: FormAlreadySettledError -} - -export type V2SessionFormReplyError = V2SessionFormReplyErrors[keyof V2SessionFormReplyErrors] - -export type V2SessionFormReplyResponses = { - /** - * - */ - 204: void -} - -export type V2SessionFormReplyResponse = V2SessionFormReplyResponses[keyof V2SessionFormReplyResponses] - -export type V2SessionFormCancelData = { - body?: never - path: { - sessionID: string - formID: string - } - query?: never - url: "/api/session/{sessionID}/form/{formID}/cancel" -} - -export type V2SessionFormCancelErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError | FormNotFoundError - */ - 404: FormNotFoundError | SessionNotFoundError - /** - * FormAlreadySettledError - */ - 409: FormAlreadySettledError -} - -export type V2SessionFormCancelError = V2SessionFormCancelErrors[keyof V2SessionFormCancelErrors] - -export type V2SessionFormCancelResponses = { - /** - * - */ - 204: void -} - -export type V2SessionFormCancelResponse = V2SessionFormCancelResponses[keyof V2SessionFormCancelResponses] - -export type V2PermissionRequestListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/permission/request" -} - -export type V2PermissionRequestListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2PermissionRequestListError = V2PermissionRequestListErrors[keyof V2PermissionRequestListErrors] - -export type V2PermissionRequestListResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: Array - } -} - -export type V2PermissionRequestListResponse = V2PermissionRequestListResponses[keyof V2PermissionRequestListResponses] - -export type V2PermissionSavedListData = { - body?: never - path?: never - query?: { - projectID?: string | null - } - url: "/api/permission/saved" -} - -export type V2PermissionSavedListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2PermissionSavedListError = V2PermissionSavedListErrors[keyof V2PermissionSavedListErrors] - -export type V2PermissionSavedListResponses = { - /** - * Success - */ - 200: { - data: Array - } -} - -export type V2PermissionSavedListResponse = V2PermissionSavedListResponses[keyof V2PermissionSavedListResponses] - -export type V2PermissionSavedRemoveData = { - body?: never - path: { - id: string - } - query?: never - url: "/api/permission/saved/{id}" -} - -export type V2PermissionSavedRemoveErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2PermissionSavedRemoveError = V2PermissionSavedRemoveErrors[keyof V2PermissionSavedRemoveErrors] - -export type V2PermissionSavedRemoveResponses = { - /** - * - */ - 204: void -} - -export type V2PermissionSavedRemoveResponse = V2PermissionSavedRemoveResponses[keyof V2PermissionSavedRemoveResponses] - -export type V2SessionPermissionListData = { - body?: never - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/permission" -} - -export type V2SessionPermissionListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError -} - -export type V2SessionPermissionListError = V2SessionPermissionListErrors[keyof V2SessionPermissionListErrors] - -export type V2SessionPermissionListResponses = { - /** - * Success - */ - 200: { - data: Array - } -} - -export type V2SessionPermissionListResponse = V2SessionPermissionListResponses[keyof V2SessionPermissionListResponses] - -export type V2SessionPermissionCreateData = { - body: { - id?: string | null - action: string - resources: Array - save?: Array - metadata?: { - [key: string]: unknown - } - source?: PermissionV2SourceV2 - agent?: string | null - } - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/permission" -} - -export type V2SessionPermissionCreateErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError -} - -export type V2SessionPermissionCreateError = V2SessionPermissionCreateErrors[keyof V2SessionPermissionCreateErrors] - -export type V2SessionPermissionCreateResponses = { - /** - * Success - */ - 200: { - data: { - id: string - effect: PermissionV2Effect - } - } -} - -export type V2SessionPermissionCreateResponse = - V2SessionPermissionCreateResponses[keyof V2SessionPermissionCreateResponses] - -export type V2SessionPermissionGetData = { - body?: never - path: { - sessionID: string - requestID: string - } - query?: never - url: "/api/session/{sessionID}/permission/{requestID}" -} - -export type V2SessionPermissionGetErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError | PermissionNotFoundError - */ - 404: PermissionNotFoundError | SessionNotFoundError -} - -export type V2SessionPermissionGetError = V2SessionPermissionGetErrors[keyof V2SessionPermissionGetErrors] - -export type V2SessionPermissionGetResponses = { - /** - * Success - */ - 200: { - data: PermissionV2RequestV2 - } -} - -export type V2SessionPermissionGetResponse = V2SessionPermissionGetResponses[keyof V2SessionPermissionGetResponses] - -export type V2SessionPermissionReplyData = { - body: { - reply: PermissionV2Reply - message?: string | null - } - path: { - sessionID: string - requestID: string - } - query?: never - url: "/api/session/{sessionID}/permission/{requestID}/reply" -} - -export type V2SessionPermissionReplyErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError | PermissionNotFoundError - */ - 404: PermissionNotFoundError | SessionNotFoundError -} - -export type V2SessionPermissionReplyError = V2SessionPermissionReplyErrors[keyof V2SessionPermissionReplyErrors] - -export type V2SessionPermissionReplyResponses = { - /** - * - */ - 204: void -} - -export type V2SessionPermissionReplyResponse = - V2SessionPermissionReplyResponses[keyof V2SessionPermissionReplyResponses] - -export type V2FsReadData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/fs/read/*" -} - -export type V2FsReadErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2FsReadError = V2FsReadErrors[keyof V2FsReadErrors] - -export type V2FsReadResponses = { - /** - * Success - */ - 200: Blob | File -} - -export type V2FsReadResponse = V2FsReadResponses[keyof V2FsReadResponses] - -export type V2FsListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - path?: string | null - } - url: "/api/fs/list" -} - -export type V2FsListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2FsListError = V2FsListErrors[keyof V2FsListErrors] - -export type V2FsListResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: Array - } -} - -export type V2FsListResponse = V2FsListResponses[keyof V2FsListResponses] - -export type V2FsFindData = { - body?: never - path?: never - query: { - location?: { - directory?: string | null - workspace?: string | null - } | null - query: string - type?: "file" | "directory" - limit?: string | null - } - url: "/api/fs/find" -} - -export type V2FsFindErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2FsFindError = V2FsFindErrors[keyof V2FsFindErrors] - -export type V2FsFindResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: Array - } -} - -export type V2FsFindResponse = V2FsFindResponses[keyof V2FsFindResponses] - -export type V2CommandListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/command" -} - -export type V2CommandListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2CommandListError = V2CommandListErrors[keyof V2CommandListErrors] - -export type V2CommandListResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: Array - } -} - -export type V2CommandListResponse = V2CommandListResponses[keyof V2CommandListResponses] - -export type V2SkillListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/skill" -} - -export type V2SkillListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2SkillListError = V2SkillListErrors[keyof V2SkillListErrors] - -export type V2SkillListResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: Array - } -} - -export type V2SkillListResponse = V2SkillListResponses[keyof V2SkillListResponses] - -export type V2EventSubscribeData = { - body?: never - path?: never - query?: never - url: "/api/event" -} - -export type V2EventSubscribeErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2EventSubscribeError = V2EventSubscribeErrors[keyof V2EventSubscribeErrors] - -export type V2EventSubscribeResponses = { - /** - * Success - */ - 200: V2Event -} - -export type V2EventSubscribeResponse = V2EventSubscribeResponses[keyof V2EventSubscribeResponses] - -export type V2PtyListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/pty" -} - -export type V2PtyListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2PtyListError = V2PtyListErrors[keyof V2PtyListErrors] - -export type V2PtyListResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: Array - } -} - -export type V2PtyListResponse = V2PtyListResponses[keyof V2PtyListResponses] - -export type V2PtyCreateData = { - body: { - command?: string - args?: Array - cwd?: string - title?: string - env?: { - [key: string]: string - } - } - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/pty" -} - -export type V2PtyCreateErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2PtyCreateError = V2PtyCreateErrors[keyof V2PtyCreateErrors] - -export type V2PtyCreateResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: PtyV2 - } -} - -export type V2PtyCreateResponse = V2PtyCreateResponses[keyof V2PtyCreateResponses] - -export type V2PtyRemoveData = { - body?: never - path: { - ptyID: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/pty/{ptyID}" -} - -export type V2PtyRemoveErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * PtyNotFoundError - */ - 404: PtyNotFoundError -} - -export type V2PtyRemoveError = V2PtyRemoveErrors[keyof V2PtyRemoveErrors] - -export type V2PtyRemoveResponses = { - /** - * - */ - 204: void -} - -export type V2PtyRemoveResponse = V2PtyRemoveResponses[keyof V2PtyRemoveResponses] - -export type V2PtyGetData = { - body?: never - path: { - ptyID: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/pty/{ptyID}" -} - -export type V2PtyGetErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * PtyNotFoundError - */ - 404: PtyNotFoundError -} - -export type V2PtyGetError = V2PtyGetErrors[keyof V2PtyGetErrors] - -export type V2PtyGetResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: PtyV2 - } -} - -export type V2PtyGetResponse = V2PtyGetResponses[keyof V2PtyGetResponses] - -export type V2PtyUpdateData = { - body: { - title?: string - size?: { - rows: number - cols: number - } - } - path: { - ptyID: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/pty/{ptyID}" -} - -export type V2PtyUpdateErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * PtyNotFoundError - */ - 404: PtyNotFoundError -} - -export type V2PtyUpdateError = V2PtyUpdateErrors[keyof V2PtyUpdateErrors] - -export type V2PtyUpdateResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: PtyV2 - } -} - -export type V2PtyUpdateResponse = V2PtyUpdateResponses[keyof V2PtyUpdateResponses] - -export type V2PtyConnectTokenData = { - body?: never - path: { - ptyID: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/pty/{ptyID}/connect-token" -} - -export type V2PtyConnectTokenErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * ForbiddenError - */ - 403: ForbiddenError - /** - * PtyNotFoundError - */ - 404: PtyNotFoundError -} - -export type V2PtyConnectTokenError = V2PtyConnectTokenErrors[keyof V2PtyConnectTokenErrors] - -export type V2PtyConnectTokenResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: PtyTicketConnectTokenV2 - } -} - -export type V2PtyConnectTokenResponse = V2PtyConnectTokenResponses[keyof V2PtyConnectTokenResponses] - -export type V2PtyConnectData = { - body?: never - path: { - ptyID: string - } - query?: { - "location[directory]"?: string - "location[workspace]"?: string - cursor?: string - ticket?: string - } - url: "/api/pty/{ptyID}/connect" -} - -export type V2PtyConnectErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * ForbiddenError - */ - 403: ForbiddenError - /** - * PtyNotFoundError - */ - 404: PtyNotFoundError -} - -export type V2PtyConnectError = V2PtyConnectErrors[keyof V2PtyConnectErrors] - -export type V2PtyConnectResponses = { - /** - * Success - */ - 200: boolean -} - -export type V2PtyConnectResponse = V2PtyConnectResponses[keyof V2PtyConnectResponses] - -export type V2ShellListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/shell" -} - -export type V2ShellListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2ShellListError = V2ShellListErrors[keyof V2ShellListErrors] - -export type V2ShellListResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: Array - } -} - -export type V2ShellListResponse = V2ShellListResponses[keyof V2ShellListResponses] - -export type V2ShellCreateData = { - body: { - command: string - cwd?: string - timeout: number - metadata?: { - [key: string]: unknown - } - } - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/shell" -} - -export type V2ShellCreateErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2ShellCreateError = V2ShellCreateErrors[keyof V2ShellCreateErrors] - -export type V2ShellCreateResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: ShellInfo1 - } -} - -export type V2ShellCreateResponse = V2ShellCreateResponses[keyof V2ShellCreateResponses] - -export type V2ShellRemoveData = { - body?: never - path: { - id: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/shell/{id}" -} - -export type V2ShellRemoveErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * ShellNotFoundError - */ - 404: ShellNotFoundError -} - -export type V2ShellRemoveError = V2ShellRemoveErrors[keyof V2ShellRemoveErrors] - -export type V2ShellRemoveResponses = { - /** - * - */ - 204: void -} - -export type V2ShellRemoveResponse = V2ShellRemoveResponses[keyof V2ShellRemoveResponses] - -export type V2ShellGetData = { - body?: never - path: { - id: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/shell/{id}" -} - -export type V2ShellGetErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * ShellNotFoundError - */ - 404: ShellNotFoundError -} - -export type V2ShellGetError = V2ShellGetErrors[keyof V2ShellGetErrors] - -export type V2ShellGetResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: ShellInfo1 - } -} - -export type V2ShellGetResponse = V2ShellGetResponses[keyof V2ShellGetResponses] - -export type V2ShellTimeoutData = { - body: { - timeout: number - } - path: { - id: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/shell/{id}/timeout" -} - -export type V2ShellTimeoutErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * ShellNotFoundError - */ - 404: ShellNotFoundError -} - -export type V2ShellTimeoutError = V2ShellTimeoutErrors[keyof V2ShellTimeoutErrors] - -export type V2ShellTimeoutResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: ShellInfo1 - } -} - -export type V2ShellTimeoutResponse = V2ShellTimeoutResponses[keyof V2ShellTimeoutResponses] - -export type V2ShellOutputData = { - body?: never - path: { - id: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - cursor?: string - limit?: string - } - url: "/api/shell/{id}/output" -} - -export type V2ShellOutputErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * ShellNotFoundError - */ - 404: ShellNotFoundError -} - -export type V2ShellOutputError = V2ShellOutputErrors[keyof V2ShellOutputErrors] - -export type V2ShellOutputResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: { - output: string - cursor: number - size: number - truncated: boolean - } - } -} - -export type V2ShellOutputResponse = V2ShellOutputResponses[keyof V2ShellOutputResponses] - -export type V2QuestionRequestListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/question/request" -} - -export type V2QuestionRequestListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2QuestionRequestListError = V2QuestionRequestListErrors[keyof V2QuestionRequestListErrors] - -export type V2QuestionRequestListResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: Array - } -} - -export type V2QuestionRequestListResponse = V2QuestionRequestListResponses[keyof V2QuestionRequestListResponses] - -export type V2SessionQuestionListData = { - body?: never - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/question" -} - -export type V2SessionQuestionListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError -} - -export type V2SessionQuestionListError = V2SessionQuestionListErrors[keyof V2SessionQuestionListErrors] - -export type V2SessionQuestionListResponses = { - /** - * Success - */ - 200: { - data: Array - } -} - -export type V2SessionQuestionListResponse = V2SessionQuestionListResponses[keyof V2SessionQuestionListResponses] - -export type V2SessionQuestionReplyData = { - body: QuestionV2Reply - path: { - sessionID: string - requestID: string - } - query?: never - url: "/api/session/{sessionID}/question/{requestID}/reply" -} - -export type V2SessionQuestionReplyErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError | QuestionNotFoundError - */ - 404: QuestionNotFoundError | SessionNotFoundError -} - -export type V2SessionQuestionReplyError = V2SessionQuestionReplyErrors[keyof V2SessionQuestionReplyErrors] - -export type V2SessionQuestionReplyResponses = { - /** - * - */ - 204: void -} - -export type V2SessionQuestionReplyResponse = V2SessionQuestionReplyResponses[keyof V2SessionQuestionReplyResponses] - -export type V2SessionQuestionRejectData = { - body?: never - path: { - sessionID: string - requestID: string - } - query?: never - url: "/api/session/{sessionID}/question/{requestID}/reject" -} - -export type V2SessionQuestionRejectErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError | QuestionNotFoundError - */ - 404: QuestionNotFoundError | SessionNotFoundError -} - -export type V2SessionQuestionRejectError = V2SessionQuestionRejectErrors[keyof V2SessionQuestionRejectErrors] - -export type V2SessionQuestionRejectResponses = { - /** - * - */ - 204: void -} - -export type V2SessionQuestionRejectResponse = V2SessionQuestionRejectResponses[keyof V2SessionQuestionRejectResponses] - -export type V2ReferenceListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/reference" -} - -export type V2ReferenceListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2ReferenceListError = V2ReferenceListErrors[keyof V2ReferenceListErrors] - -export type V2ReferenceListResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: Array - } -} - -export type V2ReferenceListResponse = V2ReferenceListResponses[keyof V2ReferenceListResponses] - -export type V2ProjectCopyRemoveData = { - body: { - directory: string - force: boolean - } - path: { - projectID: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/experimental/project/{projectID}/copy" -} - -export type V2ProjectCopyRemoveErrors = { - /** - * ProjectCopyError | InvalidRequestError - */ - 400: ProjectCopyErrorV2 | InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2ProjectCopyRemoveError = V2ProjectCopyRemoveErrors[keyof V2ProjectCopyRemoveErrors] - -export type V2ProjectCopyRemoveResponses = { - /** - * - */ - 204: void -} - -export type V2ProjectCopyRemoveResponse = V2ProjectCopyRemoveResponses[keyof V2ProjectCopyRemoveResponses] - -export type V2ProjectCopyCreateData = { - body: { - strategy: string - directory: string - name?: string - } - path: { - projectID: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/experimental/project/{projectID}/copy" -} - -export type V2ProjectCopyCreateErrors = { - /** - * ProjectCopyError | InvalidRequestError - */ - 400: ProjectCopyErrorV2 | InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2ProjectCopyCreateError = V2ProjectCopyCreateErrors[keyof V2ProjectCopyCreateErrors] - -export type V2ProjectCopyCreateResponses = { - /** - * ProjectCopy.Copy - */ - 200: ProjectCopyCopy -} - -export type V2ProjectCopyCreateResponse = V2ProjectCopyCreateResponses[keyof V2ProjectCopyCreateResponses] - -export type V2ProjectCopyRefreshData = { - body?: never - path: { - projectID: string - } - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/experimental/project/{projectID}/copy/refresh" -} - -export type V2ProjectCopyRefreshErrors = { - /** - * ProjectCopyError | InvalidRequestError - */ - 400: ProjectCopyErrorV2 | InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2ProjectCopyRefreshError = V2ProjectCopyRefreshErrors[keyof V2ProjectCopyRefreshErrors] - -export type V2ProjectCopyRefreshResponses = { - /** - * - */ - 204: void -} - -export type V2ProjectCopyRefreshResponse = V2ProjectCopyRefreshResponses[keyof V2ProjectCopyRefreshResponses] - -export type V2VcsStatusData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/vcs/status" -} - -export type V2VcsStatusErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2VcsStatusError = V2VcsStatusErrors[keyof V2VcsStatusErrors] - -export type V2VcsStatusResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: Array - } -} - -export type V2VcsStatusResponse = V2VcsStatusResponses[keyof V2VcsStatusResponses] - -export type V2VcsDiffData = { - body?: never - path?: never - query: { - location?: { - directory?: string | null - workspace?: string | null - } | null - mode: VcsMode - context?: string | null - } - url: "/api/vcs/diff" -} - -export type V2VcsDiffErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2VcsDiffError = V2VcsDiffErrors[keyof V2VcsDiffErrors] - -export type V2VcsDiffResponses = { - /** - * Success - */ - 200: { - location: LocationInfoV2 - data: Array - } -} - -export type V2VcsDiffResponse = V2VcsDiffResponses[keyof V2VcsDiffResponses] - -export type V2DebugLocationEvictData = { - body?: never - path?: never - query?: { - location?: { - directory?: string | null - workspace?: string | null - } | null - } - url: "/api/debug/location" -} - -export type V2DebugLocationEvictErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2DebugLocationEvictError = V2DebugLocationEvictErrors[keyof V2DebugLocationEvictErrors] - -export type V2DebugLocationEvictResponses = { - /** - * - */ - 204: void -} - -export type V2DebugLocationEvictResponse = V2DebugLocationEvictResponses[keyof V2DebugLocationEvictResponses] - -export type V2DebugLocationListData = { - body?: never - path?: never - query?: never - url: "/api/debug/location" -} - -export type V2DebugLocationListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestErrorV2 - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2DebugLocationListError = V2DebugLocationListErrors[keyof V2DebugLocationListErrors] - -export type V2DebugLocationListResponses = { - /** - * Success - */ - 200: Array -} - -export type V2DebugLocationListResponse = V2DebugLocationListResponses[keyof V2DebugLocationListResponses] - -export type PtyConnectData = { - body?: never - path: { - ptyID: string - } - query?: { - directory?: string - workspace?: string - cursor?: string - ticket?: string - } - url: "/pty/{ptyID}/connect" -} - -export type PtyConnectErrors = { - /** - * Forbidden - */ - 403: EffectHttpApiErrorForbidden - /** - * Not found - */ - 404: NotFoundError -} - -export type PtyConnectError = PtyConnectErrors[keyof PtyConnectErrors] - -export type PtyConnectResponses = { - /** - * Connected session - */ - 200: boolean -} - -export type PtyConnectResponse = PtyConnectResponses[keyof PtyConnectResponses] diff --git a/packages/sdk/js/src/v2/index.ts b/packages/sdk/js/src/v2/index.ts deleted file mode 100644 index 9615eacc7abe..000000000000 --- a/packages/sdk/js/src/v2/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -export * from "./client.js" -export * from "./server.js" - -import { createOpencodeClient } from "./client.js" -import { createOpencodeServer } from "./server.js" -import type { ServerOptions } from "./server.js" - -export * as data from "./data.js" - -export async function createOpencode(options?: ServerOptions) { - const server = await createOpencodeServer({ - ...options, - }) - - const client = createOpencodeClient({ - baseUrl: server.url, - }) - - return { - client, - server, - } -} diff --git a/packages/sdk/js/src/v2/server.ts b/packages/sdk/js/src/v2/server.ts deleted file mode 100644 index 48f1a253da8d..000000000000 --- a/packages/sdk/js/src/v2/server.ts +++ /dev/null @@ -1,134 +0,0 @@ -import launch from "cross-spawn" -import { type Config } from "./gen/types.gen.js" -import { stop, bindAbort } from "../process.js" - -export type ServerOptions = { - hostname?: string - port?: number - signal?: AbortSignal - timeout?: number - config?: Config -} - -export type TuiOptions = { - project?: string - model?: string - session?: string - agent?: string - signal?: AbortSignal - config?: Config -} - -export async function createOpencodeServer(options?: ServerOptions) { - options = Object.assign( - { - hostname: "127.0.0.1", - port: 4096, - timeout: 5000, - }, - options ?? {}, - ) - - const args = [`serve`, `--hostname=${options.hostname}`, `--port=${options.port}`] - if (options.config?.logLevel) args.push(`--log-level=${options.config.logLevel}`) - - const proc = launch(`opencode`, args, { - env: { - ...process.env, - OPENCODE_CONFIG_CONTENT: JSON.stringify(options.config ?? {}), - }, - }) - let clear = () => {} - - const url = await new Promise((resolve, reject) => { - const id = setTimeout(() => { - clear() - stop(proc) - reject(new Error(`Timeout waiting for server to start after ${options.timeout}ms`)) - }, options.timeout) - let output = "" - let resolved = false - proc.stdout?.on("data", (chunk) => { - if (resolved) return - output += chunk.toString() - const lines = output.split("\n") - for (const line of lines) { - if (line.startsWith("opencode server listening")) { - const match = line.match(/on\s+(https?:\/\/[^\s]+)/) - if (!match) { - clear() - stop(proc) - clearTimeout(id) - reject(new Error(`Failed to parse server url from output: ${line}`)) - return - } - clearTimeout(id) - resolved = true - resolve(match[1]!) - return - } - } - }) - proc.stderr?.on("data", (chunk) => { - output += chunk.toString() - }) - proc.on("exit", (code) => { - clearTimeout(id) - let msg = `Server exited with code ${code}` - if (output.trim()) { - msg += `\nServer output: ${output}` - } - reject(new Error(msg)) - }) - proc.on("error", (error) => { - clearTimeout(id) - reject(error) - }) - clear = bindAbort(proc, options.signal, () => { - clearTimeout(id) - reject(options.signal?.reason) - }) - }) - - return { - url, - close() { - clear() - stop(proc) - }, - } -} - -export function createOpencodeTui(options?: TuiOptions) { - const args = [] - - if (options?.project) { - args.push(`--project=${options.project}`) - } - if (options?.model) { - args.push(`--model=${options.model}`) - } - if (options?.session) { - args.push(`--session=${options.session}`) - } - if (options?.agent) { - args.push(`--agent=${options.agent}`) - } - - const proc = launch(`opencode`, args, { - stdio: "inherit", - env: { - ...process.env, - OPENCODE_CONFIG_CONTENT: JSON.stringify(options?.config ?? {}), - }, - }) - - const clear = bindAbort(proc, options?.signal) - - return { - close() { - clear() - stop(proc) - }, - } -} diff --git a/packages/sdk/js/sst-env.d.ts b/packages/sdk/js/sst-env.d.ts deleted file mode 100644 index 301538ccb214..000000000000 --- a/packages/sdk/js/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/packages/sdk/js/test/session-history.test.ts b/packages/sdk/js/test/session-history.test.ts deleted file mode 100644 index 44a974333168..000000000000 --- a/packages/sdk/js/test/session-history.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { expect, test } from "bun:test" -import type { V2SessionHistoryData } from "../src/v2/gen/types.gen" - -test("uses numeric Session history positions", () => { - const input = { - path: { sessionID: "ses_test" }, - query: { after: 1, limit: 50 }, - url: "/api/session/{sessionID}/history", - } satisfies V2SessionHistoryData - - expect(input.query.after).toBe(1) -}) diff --git a/packages/sdk/js/tsconfig.json b/packages/sdk/js/tsconfig.json deleted file mode 100644 index 3ab5fcb76890..000000000000 --- a/packages/sdk/js/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig.json", - "extends": "@tsconfig/node22/tsconfig.json", - "compilerOptions": { - "outDir": "dist", - "module": "nodenext", - "declaration": true, - "moduleResolution": "nodenext", - "lib": ["es2022", "dom", "dom.iterable"], - "composite": true, - "rootDir": "src" - }, - "include": ["src"] -} diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json deleted file mode 100644 index 04ff287bf80b..000000000000 --- a/packages/sdk/openapi.json +++ /dev/null @@ -1,36699 +0,0 @@ -{ - "openapi": "3.1.0", - "info": { - "title": "opencode", - "version": "1.0.0", - "description": "opencode api" - }, - "paths": { - "/auth/{providerID}": { - "put": { - "tags": ["control"], - "operationId": "auth.set", - "parameters": [ - { - "name": "providerID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "responses": { - "200": { - "description": "Successfully set authentication credentials", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Successfully set authentication credentials" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Set authentication credentials", - "summary": "Set auth credentials", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Auth" - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.set({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["control"], - "operationId": "auth.remove", - "parameters": [ - { - "name": "providerID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "responses": { - "200": { - "description": "Successfully removed authentication credentials", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Successfully removed authentication credentials" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Remove authentication credentials", - "summary": "Remove auth credentials", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.remove({\n ...\n})" - } - ] - } - }, - "/log": { - "post": { - "tags": ["control"], - "operationId": "app.log", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Log entry written successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Log entry written successfully" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Write a log entry to the server logs with specified level and metadata.", - "summary": "Write log", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "service": { - "type": "string", - "description": "Service name for the log entry" - }, - "level": { - "type": "string", - "enum": ["debug", "info", "error", "warn"], - "description": "Log level" - }, - "message": { - "type": "string", - "description": "Log message" - }, - "extra": { - "type": "object" - } - }, - "required": ["service", "level", "message"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.app.log({\n ...\n})" - } - ] - } - }, - "/experimental/control-plane/move-session": { - "post": { - "tags": ["controlPlane"], - "operationId": "experimental.controlPlane.moveSession", - "parameters": [], - "responses": { - "204": { - "description": "Session moved" - }, - "400": { - "description": "MoveSessionError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/MoveSessionError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Move a session to another project directory, optionally transferring local changes.", - "summary": "Move session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "destination": { - "$ref": "#/components/schemas/MoveSessionDestination" - }, - "moveChanges": { - "type": "boolean" - } - }, - "required": ["sessionID", "destination"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.controlPlane.moveSession({\n ...\n})" - } - ] - } - }, - "/global/health": { - "get": { - "tags": ["global"], - "operationId": "global.health", - "parameters": [], - "responses": { - "200": { - "description": "Health information", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "healthy": { - "type": "boolean", - "enum": [true] - }, - "version": { - "type": "string" - } - }, - "required": ["healthy", "version"], - "additionalProperties": false, - "description": "Health information" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get health information about the OpenCode server.", - "summary": "Get health", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.health({\n ...\n})" - } - ] - } - }, - "/global/event": { - "get": { - "tags": ["global"], - "operationId": "global.event", - "parameters": [], - "responses": { - "200": { - "description": "Event stream", - "content": { - "text/event-stream": { - "schema": { - "$ref": "#/components/schemas/GlobalEvent" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Subscribe to global events from the OpenCode system using server-sent events.", - "summary": "Get global events", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.event({\n ...\n})" - } - ] - } - }, - "/global/config": { - "get": { - "tags": ["global"], - "operationId": "global.config.get", - "parameters": [], - "responses": { - "200": { - "description": "Get global config info", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Config" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Retrieve the current global OpenCode configuration settings and preferences.", - "summary": "Get global configuration", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.config.get({\n ...\n})" - } - ] - }, - "patch": { - "tags": ["global"], - "operationId": "global.config.update", - "parameters": [], - "responses": { - "200": { - "description": "Successfully updated global config", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Config" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Update global OpenCode configuration settings and preferences.", - "summary": "Update global configuration", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Config" - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.config.update({\n ...\n})" - } - ] - } - }, - "/global/dispose": { - "post": { - "tags": ["global"], - "operationId": "global.dispose", - "parameters": [], - "responses": { - "200": { - "description": "Global disposed", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Global disposed" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Clean up and dispose all OpenCode instances, releasing all resources.", - "summary": "Dispose instance", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.dispose({\n ...\n})" - } - ] - } - }, - "/global/upgrade": { - "post": { - "tags": ["global"], - "operationId": "global.upgrade", - "parameters": [], - "responses": { - "200": { - "description": "Upgrade result", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [true] - }, - "version": { - "type": "string" - } - }, - "required": ["success", "version"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [false] - }, - "error": { - "type": "string" - } - }, - "required": ["success", "error"], - "additionalProperties": false - } - ], - "description": "Upgrade result" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Upgrade opencode to the specified version or latest if not specified.", - "summary": "Upgrade opencode", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "target": { - "type": "string" - } - }, - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.upgrade({\n ...\n})" - } - ] - } - }, - "/event": { - "get": { - "tags": ["event"], - "operationId": "event.subscribe", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Event stream", - "content": { - "text/event-stream": { - "schema": { - "$ref": "#/components/schemas/Event" - } - } - } - } - }, - "description": "Get events", - "summary": "Subscribe to events", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.event.subscribe({\n ...\n})" - } - ] - } - }, - "/config": { - "get": { - "tags": ["config"], - "operationId": "config.get", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Get config info", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Config" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Retrieve the current OpenCode configuration settings and preferences.", - "summary": "Get configuration", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.config.get({\n ...\n})" - } - ] - }, - "patch": { - "tags": ["config"], - "operationId": "config.update", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Successfully updated config", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Config" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Update OpenCode configuration settings and preferences.", - "summary": "Update configuration", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Config" - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.config.update({\n ...\n})" - } - ] - } - }, - "/config/providers": { - "get": { - "tags": ["config"], - "operationId": "config.providers", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "List of providers", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "providers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Provider" - } - }, - "default": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "required": ["providers", "default"], - "additionalProperties": false, - "description": "List of providers" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get a list of all configured AI providers and their default models.", - "summary": "List config providers", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.config.providers({\n ...\n})" - } - ] - } - }, - "/experimental/capabilities": { - "get": { - "tags": ["experimental"], - "operationId": "experimental.capabilities.get", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Experimental capabilities", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExperimentalCapabilities" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get experimental features enabled on the OpenCode server.", - "summary": "Get experimental capabilities", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.capabilities.get({\n ...\n})" - } - ] - } - }, - "/experimental/console": { - "get": { - "tags": ["experimental"], - "operationId": "experimental.console.get", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Active Console provider metadata", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConsoleState" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "500": { - "description": "InternalServerError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/effect_HttpApiError_InternalServerError" - } - } - } - } - }, - "description": "Get the active Console org name and the set of provider IDs managed by that Console org.", - "summary": "Get active Console provider metadata", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.console.get({\n ...\n})" - } - ] - } - }, - "/experimental/console/orgs": { - "get": { - "tags": ["experimental"], - "operationId": "experimental.console.listOrgs", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Switchable Console orgs", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "orgs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "accountID": { - "type": "string" - }, - "accountEmail": { - "type": "string" - }, - "accountUrl": { - "type": "string" - }, - "orgID": { - "type": "string" - }, - "orgName": { - "type": "string" - }, - "active": { - "type": "boolean" - } - }, - "required": ["accountID", "accountEmail", "accountUrl", "orgID", "orgName", "active"], - "additionalProperties": false - } - } - }, - "required": ["orgs"], - "additionalProperties": false, - "description": "Switchable Console orgs" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "500": { - "description": "InternalServerError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/effect_HttpApiError_InternalServerError" - } - } - } - } - }, - "description": "Get the available Console orgs across logged-in accounts, including the current active org.", - "summary": "List switchable Console orgs", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.console.listOrgs({\n ...\n})" - } - ] - } - }, - "/experimental/console/switch": { - "post": { - "tags": ["experimental"], - "operationId": "experimental.console.switchOrg", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Switch success", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Switch success" - } - } - } - } - }, - "description": "Persist a new active Console account/org selection for the current local OpenCode state.", - "summary": "Switch active Console org", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "accountID": { - "type": "string" - }, - "orgID": { - "type": "string" - } - }, - "required": ["accountID", "orgID"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.console.switchOrg({\n ...\n})" - } - ] - } - }, - "/experimental/tool": { - "get": { - "tags": ["experimental"], - "operationId": "tool.list", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "provider", - "in": "query", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "model", - "in": "query", - "schema": { - "type": "string" - }, - "required": true - } - ], - "responses": { - "200": { - "description": "Tools", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ToolList" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Get a list of available tools with their JSON schema parameters for a specific provider and model combination.", - "summary": "List tools", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tool.list({\n ...\n})" - } - ] - } - }, - "/experimental/tool/ids": { - "get": { - "tags": ["experimental"], - "operationId": "tool.ids", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Tool IDs", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ToolIDs" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.", - "summary": "List tool IDs", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tool.ids({\n ...\n})" - } - ] - } - }, - "/experimental/worktree": { - "get": { - "tags": ["experimental"], - "operationId": "worktree.list", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "List of worktree directories", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - }, - "description": "List of worktree directories" - } - } - } - }, - "400": { - "description": "WorktreeError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/WorktreeError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "List all sandbox worktrees for the current project.", - "summary": "List worktrees", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.list({\n ...\n})" - } - ] - }, - "post": { - "tags": ["experimental"], - "operationId": "worktree.create", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Worktree created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Worktree" - } - } - } - }, - "400": { - "description": "WorktreeError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/WorktreeError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Create a new git worktree for the current project and run any configured startup scripts.", - "summary": "Create worktree", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorktreeCreateInput" - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.create({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["experimental"], - "operationId": "worktree.remove", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Worktree removed", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Worktree removed" - } - } - } - }, - "400": { - "description": "WorktreeError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/WorktreeError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Remove a git worktree and delete its branch.", - "summary": "Remove worktree", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorktreeRemoveInput" - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.remove({\n ...\n})" - } - ] - } - }, - "/experimental/worktree/reset": { - "post": { - "tags": ["experimental"], - "operationId": "worktree.reset", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Worktree reset", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Worktree reset" - } - } - } - }, - "400": { - "description": "WorktreeError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/WorktreeError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Reset a worktree branch to the primary default branch.", - "summary": "Reset worktree", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorktreeResetInput" - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.reset({\n ...\n})" - } - ] - } - }, - "/experimental/session": { - "get": { - "tags": ["experimental"], - "operationId": "experimental.session.list", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "roots", - "in": "query", - "schema": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "string", - "enum": ["true", "false"] - } - ] - }, - "required": false - }, - { - "name": "start", - "in": "query", - "schema": { - "type": "number" - }, - "required": false - }, - { - "name": "cursor", - "in": "query", - "schema": { - "type": "number" - }, - "required": false - }, - { - "name": "search", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "number" - }, - "required": false - }, - { - "name": "archived", - "in": "query", - "schema": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "string", - "enum": ["true", "false"] - } - ] - }, - "required": false - } - ], - "responses": { - "200": { - "description": "List of sessions", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/GlobalSession" - }, - "description": "List of sessions" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default.", - "summary": "List sessions", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.session.list({\n ...\n})" - } - ] - } - }, - "/experimental/session/{sessionID}/background": { - "post": { - "tags": ["experimental"], - "operationId": "experimental.session.background", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Backgrounded subagents", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Backgrounded subagents" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Detach any synchronous subagents currently blocking the session and continue them in the background.", - "summary": "Background subagents", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.session.background({\n ...\n})" - } - ] - } - }, - "/experimental/resource": { - "get": { - "tags": ["experimental"], - "operationId": "experimental.resource.list", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "MCP resources", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/McpResource" - }, - "description": "MCP resources" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get all available MCP resources from connected servers. Optionally filter by name.", - "summary": "Get MCP resources", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.resource.list({\n ...\n})" - } - ] - } - }, - "/find": { - "get": { - "tags": ["file"], - "operationId": "find.text", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "pattern", - "in": "query", - "schema": { - "type": "string" - }, - "required": true - } - ], - "responses": { - "200": { - "description": "Matches", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "path": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"], - "additionalProperties": false - }, - "lines": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"], - "additionalProperties": false - }, - "line_number": { - "type": "integer", - "minimum": 0 - }, - "absolute_offset": { - "type": "integer", - "minimum": 0 - }, - "submatches": { - "type": "array", - "items": { - "type": "object", - "properties": { - "match": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"], - "additionalProperties": false - }, - "start": { - "type": "integer", - "minimum": 0 - }, - "end": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["match", "start", "end"], - "additionalProperties": false - } - } - }, - "required": ["path", "lines", "line_number", "absolute_offset", "submatches"], - "additionalProperties": false - }, - "description": "Matches" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Search for text patterns across files in the project using ripgrep.", - "summary": "Find text", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.find.text({\n ...\n})" - } - ] - } - }, - "/find/file": { - "get": { - "tags": ["file"], - "operationId": "find.files", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "query", - "in": "query", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "dirs", - "in": "query", - "schema": { - "type": "string", - "enum": ["true", "false"] - }, - "required": false - }, - { - "name": "type", - "in": "query", - "schema": { - "type": "string", - "enum": ["file", "directory"] - }, - "required": false - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 200 - }, - "required": false - } - ], - "responses": { - "200": { - "description": "File paths", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - }, - "description": "File paths" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Search for files or directories by name or pattern in the project directory.", - "summary": "Find files", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.find.files({\n ...\n})" - } - ] - } - }, - "/find/symbol": { - "get": { - "tags": ["file"], - "operationId": "find.symbols", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "query", - "in": "query", - "schema": { - "type": "string" - }, - "required": true - } - ], - "responses": { - "200": { - "description": "Symbols", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Symbol" - }, - "description": "Symbols" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Search for workspace symbols like functions, classes, and variables using LSP.", - "summary": "Find symbols", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.find.symbols({\n ...\n})" - } - ] - } - }, - "/file": { - "get": { - "tags": ["file"], - "operationId": "file.list", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "path", - "in": "query", - "schema": { - "type": "string" - }, - "required": true - } - ], - "responses": { - "200": { - "description": "Files and directories", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileNode" - }, - "description": "Files and directories" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "List files and directories in a specified path.", - "summary": "List files", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.file.list({\n ...\n})" - } - ] - } - }, - "/file/content": { - "get": { - "tags": ["file"], - "operationId": "file.read", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "path", - "in": "query", - "schema": { - "type": "string" - }, - "required": true - } - ], - "responses": { - "200": { - "description": "File content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileContent" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Read the content of a specified file.", - "summary": "Read file", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.file.read({\n ...\n})" - } - ] - } - }, - "/file/status": { - "get": { - "tags": ["file"], - "operationId": "file.status", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "File status", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/File" - }, - "description": "File status" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get the git status of all files in the project.", - "summary": "Get file status", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.file.status({\n ...\n})" - } - ] - } - }, - "/instance/dispose": { - "post": { - "tags": ["instance"], - "operationId": "instance.dispose", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Instance disposed", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Instance disposed" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Clean up and dispose the current OpenCode instance, releasing all resources.", - "summary": "Dispose instance", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.instance.dispose({\n ...\n})" - } - ] - } - }, - "/path": { - "get": { - "tags": ["instance"], - "operationId": "path.get", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Path", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Path" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Retrieve the current working directory and related path information for the OpenCode instance.", - "summary": "Get paths", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.path.get({\n ...\n})" - } - ] - } - }, - "/vcs": { - "get": { - "tags": ["instance"], - "operationId": "vcs.get", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "VCS info", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VcsInfo" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Retrieve version control system (VCS) information for the current project, such as git branch.", - "summary": "Get VCS info", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.vcs.get({\n ...\n})" - } - ] - } - }, - "/vcs/status": { - "get": { - "tags": ["instance"], - "operationId": "vcs.status", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "VCS status", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/VcsFileStatus" - }, - "description": "VCS status" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Retrieve changed files in the current working tree without patches.", - "summary": "Get VCS status", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.vcs.status({\n ...\n})" - } - ] - } - }, - "/vcs/diff": { - "get": { - "tags": ["instance"], - "operationId": "vcs.diff", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "mode", - "in": "query", - "schema": { - "type": "string", - "enum": ["git", "branch"] - }, - "required": true - }, - { - "name": "context", - "in": "query", - "schema": { - "type": "integer", - "minimum": 0 - }, - "required": false - } - ], - "responses": { - "200": { - "description": "VCS diff", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/VcsFileDiff" - }, - "description": "VCS diff" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Retrieve the current git diff for the working tree or against the default branch.", - "summary": "Get VCS diff", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.vcs.diff({\n ...\n})" - } - ] - } - }, - "/vcs/diff/raw": { - "get": { - "tags": ["instance"], - "operationId": "vcs.diff.raw", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Raw VCS diff", - "content": { - "text/x-diff; charset=utf-8": { - "schema": { - "type": "string" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Retrieve a raw patch for current uncommitted changes.", - "summary": "Get raw VCS diff", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.vcs.diff.raw({\n ...\n})" - } - ] - } - }, - "/vcs/apply": { - "post": { - "tags": ["instance"], - "operationId": "vcs.apply", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "VCS patch applied", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "applied": { - "type": "boolean" - } - }, - "required": ["applied"], - "additionalProperties": false, - "description": "VCS patch applied" - } - } - } - }, - "400": { - "description": "VcsApplyError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/VcsApplyError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Apply a raw patch to the current working tree.", - "summary": "Apply VCS patch", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "patch": { - "type": "string" - } - }, - "required": ["patch"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.vcs.apply({\n ...\n})" - } - ] - } - }, - "/command": { - "get": { - "tags": ["instance"], - "operationId": "command.list", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "List of commands", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Command" - }, - "description": "List of commands" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get a list of all available commands in the OpenCode system.", - "summary": "List commands", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.command.list({\n ...\n})" - } - ] - } - }, - "/agent": { - "get": { - "tags": ["instance"], - "operationId": "app.agents", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "List of agents", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Agent" - }, - "description": "List of agents" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get a list of all available AI agents in the OpenCode system.", - "summary": "List agents", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.app.agents({\n ...\n})" - } - ] - } - }, - "/skill": { - "get": { - "tags": ["instance"], - "operationId": "app.skills", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "List of skills", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "location": { - "type": "string" - }, - "content": { - "type": "string" - } - }, - "required": ["name", "location", "content"], - "additionalProperties": false - }, - "description": "List of skills" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get a list of all available skills in the OpenCode system.", - "summary": "List skills", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.app.skills({\n ...\n})" - } - ] - } - }, - "/lsp": { - "get": { - "tags": ["instance"], - "operationId": "lsp.status", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "LSP server status", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LSPStatus" - }, - "description": "LSP server status" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get LSP server status", - "summary": "Get LSP status", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.lsp.status({\n ...\n})" - } - ] - } - }, - "/formatter": { - "get": { - "tags": ["instance"], - "operationId": "formatter.status", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Formatter status", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FormatterStatus" - }, - "description": "Formatter status" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get formatter status", - "summary": "Get formatter status", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.formatter.status({\n ...\n})" - } - ] - } - }, - "/mcp": { - "get": { - "tags": ["mcp"], - "operationId": "mcp.status", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "MCP server status", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/MCPStatus" - }, - "description": "MCP server status" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get the status of all Model Context Protocol (MCP) servers.", - "summary": "Get MCP status", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.status({\n ...\n})" - } - ] - }, - "post": { - "tags": ["mcp"], - "operationId": "mcp.add", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "MCP server added successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/MCPStatus" - }, - "description": "MCP server added successfully" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Dynamically add a new Model Context Protocol (MCP) server to the system.", - "summary": "Add MCP server", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "config": { - "anyOf": [ - { - "$ref": "#/components/schemas/McpLocalConfig" - }, - { - "$ref": "#/components/schemas/McpRemoteConfig" - } - ] - } - }, - "required": ["name", "config"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.add({\n ...\n})" - } - ] - } - }, - "/mcp/{name}/auth": { - "post": { - "tags": ["mcp"], - "operationId": "mcp.auth.start", - "parameters": [ - { - "name": "name", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "OAuth flow started", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "authorizationUrl": { - "type": "string" - }, - "oauthState": { - "type": "string" - } - }, - "required": ["authorizationUrl", "oauthState"], - "additionalProperties": false, - "description": "OAuth flow started" - } - } - } - }, - "400": { - "description": "McpUnsupportedOAuthError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/McpUnsupportedOAuthError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "McpServerNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/McpServerNotFoundError" - } - } - } - } - }, - "description": "Start OAuth authentication flow for a Model Context Protocol (MCP) server.", - "summary": "Start MCP OAuth", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.auth.start({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["mcp"], - "operationId": "mcp.auth.remove", - "parameters": [ - { - "name": "name", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "OAuth credentials removed", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [true] - } - }, - "required": ["success"], - "additionalProperties": false, - "description": "OAuth credentials removed" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "McpServerNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/McpServerNotFoundError" - } - } - } - } - }, - "description": "Remove OAuth credentials for an MCP server.", - "summary": "Remove MCP OAuth", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.auth.remove({\n ...\n})" - } - ] - } - }, - "/mcp/{name}/auth/callback": { - "post": { - "tags": ["mcp"], - "operationId": "mcp.auth.callback", - "parameters": [ - { - "name": "name", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "OAuth authentication completed", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MCPStatus" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "McpServerNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/McpServerNotFoundError" - } - } - } - } - }, - "description": "Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.", - "summary": "Complete MCP OAuth", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": { - "type": "string" - } - }, - "required": ["code"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.auth.callback({\n ...\n})" - } - ] - } - }, - "/mcp/{name}/auth/authenticate": { - "post": { - "tags": ["mcp"], - "operationId": "mcp.auth.authenticate", - "parameters": [ - { - "name": "name", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "OAuth authentication completed", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MCPStatus" - } - } - } - }, - "400": { - "description": "McpUnsupportedOAuthError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/McpUnsupportedOAuthError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "McpServerNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/McpServerNotFoundError" - } - } - } - } - }, - "description": "Start OAuth flow and wait for callback (opens browser).", - "summary": "Authenticate MCP OAuth", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.auth.authenticate({\n ...\n})" - } - ] - } - }, - "/mcp/{name}/connect": { - "post": { - "tags": ["mcp"], - "operationId": "mcp.connect", - "parameters": [ - { - "name": "name", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "MCP server connected successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "MCP server connected successfully" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "McpServerNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/McpServerNotFoundError" - } - } - } - } - }, - "description": "Connect an MCP server.", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.connect({\n ...\n})" - } - ] - } - }, - "/mcp/{name}/disconnect": { - "post": { - "tags": ["mcp"], - "operationId": "mcp.disconnect", - "parameters": [ - { - "name": "name", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "MCP server disconnected successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "MCP server disconnected successfully" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "McpServerNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/McpServerNotFoundError" - } - } - } - } - }, - "description": "Disconnect an MCP server.", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.disconnect({\n ...\n})" - } - ] - } - }, - "/project": { - "get": { - "tags": ["project"], - "operationId": "project.list", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "List of projects", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Project" - }, - "description": "List of projects" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get a list of projects that have been opened with OpenCode.", - "summary": "List all projects", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.project.list({\n ...\n})" - } - ] - } - }, - "/project/current": { - "get": { - "tags": ["project"], - "operationId": "project.current", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Current project information", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Project" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Retrieve the currently active project that OpenCode is working with.", - "summary": "Get current project", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.project.current({\n ...\n})" - } - ] - } - }, - "/project/git/init": { - "post": { - "tags": ["project"], - "operationId": "project.initGit", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Project information after git initialization", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Project" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Create a git repository for the current project and return the refreshed project info.", - "summary": "Initialize git repository", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.project.initGit({\n ...\n})" - } - ] - } - }, - "/project/{projectID}": { - "patch": { - "tags": ["project"], - "operationId": "project.update", - "parameters": [ - { - "name": "projectID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Updated project information", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Project" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "ProjectNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProjectNotFoundError" - } - } - } - } - }, - "description": "Update project properties such as name, icon, and commands.", - "summary": "Update project", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "icon": { - "$ref": "#/components/schemas/ProjectIcon" - }, - "commands": { - "$ref": "#/components/schemas/ProjectCommands" - } - }, - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.project.update({\n ...\n})" - } - ] - } - }, - "/project/{projectID}/directories": { - "get": { - "tags": ["project"], - "operationId": "project.directories", - "parameters": [ - { - "name": "projectID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Project directories", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProjectDirectories" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "List known local absolute directories for a project.", - "summary": "List project directories", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.project.directories({\n ...\n})" - } - ] - } - }, - "/experimental/project/{projectID}/copy/generate-name": { - "post": { - "tags": ["projectCopy"], - "operationId": "experimental.projectCopy.generateName", - "parameters": [ - { - "name": "projectID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": ["name"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Generate a short name for a project copy from task context.", - "summary": "Generate project copy name", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "context": { - "type": "string" - } - }, - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.projectCopy.generateName({\n ...\n})" - } - ] - } - }, - "/pty/shells": { - "get": { - "tags": ["pty"], - "operationId": "pty.shells", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "List of shells", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "path": { - "type": "string" - }, - "name": { - "type": "string" - }, - "acceptable": { - "type": "boolean" - } - }, - "required": ["path", "name", "acceptable"], - "additionalProperties": false - }, - "description": "List of shells" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get a list of available shells on the system.", - "summary": "List available shells", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.shells({\n ...\n})" - } - ] - } - }, - "/pty": { - "get": { - "tags": ["pty"], - "operationId": "pty.list", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "List of sessions", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pty" - }, - "description": "List of sessions" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.", - "summary": "List PTY sessions", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.list({\n ...\n})" - } - ] - }, - "post": { - "tags": ["pty"], - "operationId": "pty.create", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Created session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pty" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Create a new pseudo-terminal (PTY) session for running shell commands and processes.", - "summary": "Create PTY session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "command": { - "type": "string" - }, - "args": { - "type": "array", - "items": { - "type": "string" - } - }, - "cwd": { - "type": "string" - }, - "title": { - "type": "string" - }, - "env": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.create({\n ...\n})" - } - ] - } - }, - "/pty/{ptyID}": { - "get": { - "tags": ["pty"], - "operationId": "pty.get", - "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^pty" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Session info", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pty" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "PtyNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } - } - }, - "description": "Retrieve detailed information about a specific pseudo-terminal (PTY) session.", - "summary": "Get PTY session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.get({\n ...\n})" - } - ] - }, - "put": { - "tags": ["pty"], - "operationId": "pty.update", - "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^pty" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Updated session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pty" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "PtyNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } - } - }, - "description": "Update properties of an existing pseudo-terminal (PTY) session.", - "summary": "Update PTY session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "size": { - "type": "object", - "properties": { - "rows": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "cols": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["rows", "cols"], - "additionalProperties": false - } - }, - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.update({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["pty"], - "operationId": "pty.remove", - "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^pty" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Session removed", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Session removed" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "PtyNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } - } - }, - "description": "Remove and terminate a specific pseudo-terminal (PTY) session.", - "summary": "Remove PTY session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.remove({\n ...\n})" - } - ] - } - }, - "/pty/{ptyID}/connect-token": { - "post": { - "tags": ["pty"], - "operationId": "pty.connectToken", - "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^pty" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "WebSocket connect token", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyTicketConnectToken" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "403": { - "description": "PtyForbiddenError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyForbiddenError" - } - } - } - }, - "404": { - "description": "PtyNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } - } - }, - "description": "Create a short-lived ticket for opening a PTY WebSocket connection.", - "summary": "Create PTY WebSocket token", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.connectToken({\n ...\n})" - } - ] - } - }, - "/question": { - "get": { - "tags": ["question"], - "operationId": "question.list", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "List of pending questions", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionRequest" - }, - "description": "List of pending questions" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get all pending question requests across all sessions.", - "summary": "List pending questions", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.question.list({\n ...\n})" - } - ] - } - }, - "/question/{requestID}/reply": { - "post": { - "tags": ["question"], - "operationId": "question.reply", - "parameters": [ - { - "name": "requestID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^que" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Question answered successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Question answered successfully" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "QuestionNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/QuestionNotFoundError" - } - } - } - } - }, - "description": "Provide answers to a question request from the AI assistant.", - "summary": "Reply to question request", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - }, - "description": "User answers in order of questions (each answer is an array of selected labels)" - } - }, - "required": ["answers"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.question.reply({\n ...\n})" - } - ] - } - }, - "/question/{requestID}/reject": { - "post": { - "tags": ["question"], - "operationId": "question.reject", - "parameters": [ - { - "name": "requestID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^que" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Question rejected successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Question rejected successfully" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "QuestionNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/QuestionNotFoundError" - } - } - } - } - }, - "description": "Reject a question request from the AI assistant.", - "summary": "Reject question request", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.question.reject({\n ...\n})" - } - ] - } - }, - "/permission": { - "get": { - "tags": ["permission"], - "operationId": "permission.list", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "List of pending permissions", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionRequest" - }, - "description": "List of pending permissions" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get all pending permission requests across all sessions.", - "summary": "List pending permissions", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.permission.list({\n ...\n})" - } - ] - } - }, - "/permission/{requestID}/reply": { - "post": { - "tags": ["permission"], - "operationId": "permission.reply", - "parameters": [ - { - "name": "requestID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^per" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Permission processed successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Permission processed successfully" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "PermissionNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PermissionNotFoundError" - } - } - } - } - }, - "description": "Approve or deny a permission request from the AI assistant.", - "summary": "Respond to permission request", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "reply": { - "type": "string", - "enum": ["once", "always", "reject"] - }, - "message": { - "type": "string" - } - }, - "required": ["reply"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.permission.reply({\n ...\n})" - } - ] - } - }, - "/provider": { - "get": { - "tags": ["provider"], - "operationId": "provider.list", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "List of providers", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "all": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Provider" - } - }, - "default": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "connected": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["all", "default", "connected"], - "additionalProperties": false, - "description": "List of providers" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get a list of all available AI providers, including both available and connected ones.", - "summary": "List providers", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.provider.list({\n ...\n})" - } - ] - } - }, - "/provider/auth": { - "get": { - "tags": ["provider"], - "operationId": "provider.auth", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Provider auth methods", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProviderAuthMethod" - } - }, - "description": "Provider auth methods" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Retrieve available authentication methods for all AI providers.", - "summary": "Get provider auth methods", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.provider.auth({\n ...\n})" - } - ] - } - }, - "/provider/{providerID}/oauth/authorize": { - "post": { - "tags": ["provider"], - "operationId": "provider.oauth.authorize", - "parameters": [ - { - "name": "providerID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Authorization URL and method", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProviderAuthAuthorization" - } - } - } - }, - "400": { - "description": "ProviderAuthError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProviderAuthError1" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Start the OAuth authorization flow for a provider.", - "summary": "Start OAuth authorization", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "method": { - "type": "number", - "description": "Auth method index" - }, - "inputs": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "required": ["method"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.provider.oauth.authorize({\n ...\n})" - } - ] - } - }, - "/provider/{providerID}/oauth/callback": { - "post": { - "tags": ["provider"], - "operationId": "provider.oauth.callback", - "parameters": [ - { - "name": "providerID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "OAuth callback processed successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "OAuth callback processed successfully" - } - } - } - }, - "400": { - "description": "ProviderAuthError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProviderAuthError1" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Handle the OAuth callback from a provider after user authorization.", - "summary": "Handle OAuth callback", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "method": { - "type": "number", - "description": "Auth method index" - }, - "code": { - "type": "string" - } - }, - "required": ["method"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.provider.oauth.callback({\n ...\n})" - } - ] - } - }, - "/session": { - "get": { - "tags": ["session"], - "operationId": "session.list", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "scope", - "in": "query", - "schema": { - "type": "string", - "enum": ["project"] - }, - "required": false - }, - { - "name": "path", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "roots", - "in": "query", - "schema": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "string", - "enum": ["true", "false"] - } - ] - }, - "required": false - }, - { - "name": "start", - "in": "query", - "schema": { - "type": "number" - }, - "required": false - }, - { - "name": "search", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "number" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "List of sessions", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Session" - }, - "description": "List of sessions" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get a list of all OpenCode sessions, sorted by most recently updated.", - "summary": "List sessions", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.list({\n ...\n})" - } - ] - }, - "post": { - "tags": ["session"], - "operationId": "session.create", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Successfully created session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Session" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Create a new OpenCode session for interacting with AI assistants and managing conversations.", - "summary": "Create session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "parentID": { - "type": "string", - "pattern": "^ses" - }, - "title": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "variant": { - "type": "string" - } - }, - "required": ["id", "providerID"], - "additionalProperties": false - }, - "metadata": { - "type": "object" - }, - "permission": { - "$ref": "#/components/schemas/PermissionRuleset" - }, - "workspaceID": { - "type": "string", - "pattern": "^wrk" - } - }, - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.create({\n ...\n})" - } - ] - } - }, - "/session/status": { - "get": { - "tags": ["session"], - "operationId": "session.status", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Get session status", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/SessionStatus" - }, - "description": "Get session status" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Retrieve the current status of all sessions, including active, idle, and completed states.", - "summary": "Get session status", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.status({\n ...\n})" - } - ] - } - }, - "/session/{sessionID}": { - "get": { - "tags": ["session"], - "operationId": "session.get", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Get session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Session" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "description": "Retrieve detailed information about a specific OpenCode session.", - "summary": "Get session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.get({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["session"], - "operationId": "session.delete", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Successfully deleted session", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Successfully deleted session" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "description": "Delete a session and permanently remove all associated data, including messages and history.", - "summary": "Delete session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.delete({\n ...\n})" - } - ] - }, - "patch": { - "tags": ["session"], - "operationId": "session.update", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Successfully updated session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Session" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "description": "Update properties of an existing session, such as title or other metadata.", - "summary": "Update session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "metadata": { - "type": "object" - }, - "permission": { - "$ref": "#/components/schemas/PermissionRuleset" - }, - "time": { - "type": "object", - "properties": { - "archived": { - "type": "number" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.update({\n ...\n})" - } - ] - } - }, - "/session/{sessionID}/children": { - "get": { - "tags": ["session"], - "operationId": "session.children", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "List of children", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Session" - }, - "description": "List of children" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "description": "Retrieve all child sessions that were forked from the specified parent session.", - "summary": "Get session children", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.children({\n ...\n})" - } - ] - } - }, - "/session/{sessionID}/diff": { - "get": { - "tags": ["session"], - "operationId": "session.diff", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "messageID", - "in": "query", - "schema": { - "type": "string", - "pattern": "^msg" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Successfully retrieved diff", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" - }, - "description": "Successfully retrieved diff" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get the file changes (diff) that resulted from a specific user message in the session.", - "summary": "Get message diff", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.diff({\n ...\n})" - } - ] - } - }, - "/session/{sessionID}/message": { - "get": { - "tags": ["session"], - "operationId": "session.messages", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "minimum": 0, - "maximum": 9007199254740991 - }, - "required": false - }, - { - "name": "before", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "List of messages", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Message" - }, - "parts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Part" - } - } - }, - "required": ["info", "parts"], - "additionalProperties": false - }, - "description": "List of messages" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "description": "Retrieve all messages in a session, including user prompts and AI responses.", - "summary": "Get session messages", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.messages({\n ...\n})" - } - ] - }, - "post": { - "tags": ["session"], - "operationId": "session.prompt", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Created message", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["info", "parts"], - "properties": { - "info": { - "$ref": "#/components/schemas/AssistantMessage" - }, - "parts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Part" - } - } - } - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "description": "Create and send a new message to a session, streaming the AI response.", - "summary": "Send message", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "model": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - } - }, - "required": ["providerID", "modelID"], - "additionalProperties": false - }, - "agent": { - "type": "string" - }, - "noReply": { - "type": "boolean" - }, - "tools": { - "type": "object", - "additionalProperties": { - "type": "boolean" - } - }, - "format": { - "$ref": "#/components/schemas/OutputFormat" - }, - "system": { - "type": "string" - }, - "variant": { - "type": "string" - }, - "parts": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/TextPartInput" - }, - { - "$ref": "#/components/schemas/FilePartInput" - }, - { - "$ref": "#/components/schemas/AgentPartInput" - }, - { - "$ref": "#/components/schemas/SubtaskPartInput" - } - ] - } - } - }, - "required": ["parts"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.prompt({\n ...\n})" - } - ] - } - }, - "/session/{sessionID}/message/{messageID}": { - "get": { - "tags": ["session"], - "operationId": "session.message", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "messageID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^msg" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Message", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Message" - }, - "parts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Part" - } - } - }, - "required": ["info", "parts"], - "additionalProperties": false, - "description": "Message" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "description": "Retrieve a specific message from a session by its message ID.", - "summary": "Get message", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.message({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["session"], - "operationId": "session.deleteMessage", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "messageID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^msg" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Successfully deleted message", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Successfully deleted message" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - }, - "409": { - "description": "SessionBusyError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionBusyError" - } - } - } - } - }, - "description": "Permanently delete a specific message and all of its parts from a session without reverting file changes.", - "summary": "Delete message", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.deleteMessage({\n ...\n})" - } - ] - } - }, - "/session/{sessionID}/fork": { - "post": { - "tags": ["session"], - "operationId": "session.fork", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "200", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Session" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "description": "Create a new session by forking an existing session at a specific message point.", - "summary": "Fork session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg" - } - }, - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.fork({\n ...\n})" - } - ] - } - }, - "/session/{sessionID}/abort": { - "post": { - "tags": ["session"], - "operationId": "session.abort", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Aborted session", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Aborted session" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Abort an active session and stop any ongoing AI processing or command execution.", - "summary": "Abort session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.abort({\n ...\n})" - } - ] - } - }, - "/session/{sessionID}/init": { - "post": { - "tags": ["session"], - "operationId": "session.init", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "200", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "200" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "description": "Analyze the current application and create an AGENTS.md file with project-specific agent configurations.", - "summary": "Initialize session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "modelID": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - } - }, - "required": ["modelID", "providerID", "messageID"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.init({\n ...\n})" - } - ] - } - }, - "/session/{sessionID}/share": { - "post": { - "tags": ["session"], - "operationId": "session.share", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Successfully shared session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Session" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - }, - "500": { - "description": "InternalServerError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/effect_HttpApiError_InternalServerError" - } - } - } - } - }, - "description": "Create a shareable link for a session, allowing others to view the conversation.", - "summary": "Share session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.share({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["session"], - "operationId": "session.unshare", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Successfully unshared session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Session" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - }, - "500": { - "description": "InternalServerError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/effect_HttpApiError_InternalServerError" - } - } - } - } - }, - "description": "Remove the shareable link for a session, making it private again.", - "summary": "Unshare session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.unshare({\n ...\n})" - } - ] - } - }, - "/session/{sessionID}/summarize": { - "post": { - "tags": ["session"], - "operationId": "session.summarize", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Summarized session", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Summarized session" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "description": "Generate a concise summary of the session using AI compaction to preserve key information.", - "summary": "Summarize session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - }, - "auto": { - "type": "boolean" - } - }, - "required": ["providerID", "modelID"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.summarize({\n ...\n})" - } - ] - } - }, - "/session/{sessionID}/prompt_async": { - "post": { - "tags": ["session"], - "operationId": "session.prompt_async", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "204": { - "description": "Prompt accepted" - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "description": "Create and send a new message to a session asynchronously, starting the session if needed and returning immediately.", - "summary": "Send async message", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "model": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - } - }, - "required": ["providerID", "modelID"], - "additionalProperties": false - }, - "agent": { - "type": "string" - }, - "noReply": { - "type": "boolean" - }, - "tools": { - "type": "object", - "additionalProperties": { - "type": "boolean" - } - }, - "format": { - "$ref": "#/components/schemas/OutputFormat" - }, - "system": { - "type": "string" - }, - "variant": { - "type": "string" - }, - "parts": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/TextPartInput" - }, - { - "$ref": "#/components/schemas/FilePartInput" - }, - { - "$ref": "#/components/schemas/AgentPartInput" - }, - { - "$ref": "#/components/schemas/SubtaskPartInput" - } - ] - } - } - }, - "required": ["parts"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.prompt_async({\n ...\n})" - } - ] - } - }, - "/session/{sessionID}/command": { - "post": { - "tags": ["session"], - "operationId": "session.command", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Created message", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["info", "parts"], - "properties": { - "info": { - "$ref": "#/components/schemas/AssistantMessage" - }, - "parts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Part" - } - } - } - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "description": "Send a new command to a session for execution by the AI assistant.", - "summary": "Send command", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "string" - }, - "arguments": { - "type": "string" - }, - "command": { - "type": "string" - }, - "variant": { - "type": "string" - }, - "parts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "type": { - "type": "string", - "enum": ["file"] - }, - "mime": { - "type": "string" - }, - "filename": { - "type": "string" - }, - "url": { - "type": "string" - }, - "source": { - "$ref": "#/components/schemas/FilePartSource" - } - }, - "required": ["type", "mime", "url"], - "additionalProperties": false - } - } - }, - "required": ["arguments", "command"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.command({\n ...\n})" - } - ] - } - }, - "/session/{sessionID}/shell": { - "post": { - "tags": ["session"], - "operationId": "session.shell", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Created message", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Message" - }, - "parts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Part" - } - } - }, - "required": ["info", "parts"], - "additionalProperties": false, - "description": "Created message" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - }, - "409": { - "description": "SessionBusyError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionBusyError" - } - } - } - } - }, - "description": "Execute a shell command within the session context and return the AI's response.", - "summary": "Run shell command", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - } - }, - "required": ["providerID", "modelID"], - "additionalProperties": false - }, - "command": { - "type": "string" - } - }, - "required": ["agent", "command"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.shell({\n ...\n})" - } - ] - } - }, - "/session/{sessionID}/revert": { - "post": { - "tags": ["session"], - "operationId": "session.revert", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Updated session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Session" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - }, - "409": { - "description": "SessionBusyError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionBusyError" - } - } - } - } - }, - "description": "Revert a specific message in a session, undoing its effects and restoring the previous state.", - "summary": "Revert message", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "partID": { - "type": "string", - "pattern": "^prt" - } - }, - "required": ["messageID"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.revert({\n ...\n})" - } - ] - } - }, - "/session/{sessionID}/unrevert": { - "post": { - "tags": ["session"], - "operationId": "session.unrevert", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Updated session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Session" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - }, - "409": { - "description": "SessionBusyError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionBusyError" - } - } - } - } - }, - "description": "Restore all previously reverted messages in a session.", - "summary": "Restore reverted messages", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.unrevert({\n ...\n})" - } - ] - } - }, - "/session/{sessionID}/permissions/{permissionID}": { - "post": { - "tags": ["session"], - "operationId": "permission.respond", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "permissionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^per" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Permission processed successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Permission processed successfully" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError | PermissionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/NotFoundError" - }, - { - "$ref": "#/components/schemas/PermissionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Approve or deny a permission request from the AI assistant.", - "summary": "Respond to permission", - "deprecated": true, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "response": { - "type": "string", - "enum": ["once", "always", "reject"] - } - }, - "required": ["response"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.permission.respond({\n ...\n})" - } - ] - } - }, - "/session/{sessionID}/message/{messageID}/part/{partID}": { - "delete": { - "tags": ["session"], - "operationId": "part.delete", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "messageID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^msg" - }, - "required": true - }, - { - "name": "partID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^prt" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Successfully deleted part", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Successfully deleted part" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "description": "Delete a part from a message.", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.part.delete({\n ...\n})" - } - ] - }, - "patch": { - "tags": ["session"], - "operationId": "part.update", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "messageID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^msg" - }, - "required": true - }, - { - "name": "partID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^prt" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Successfully updated part", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Part" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "description": "Update a part in a message.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Part" - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.part.update({\n ...\n})" - } - ] - } - }, - "/sync/start": { - "post": { - "tags": ["sync"], - "operationId": "sync.start", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Workspace sync started", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Workspace sync started" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Start sync loops for workspaces in the current project that have active sessions.", - "summary": "Start workspace sync", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.sync.start({\n ...\n})" - } - ] - } - }, - "/sync/replay": { - "post": { - "tags": ["sync"], - "operationId": "sync.replay", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Replayed sync events", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionID": { - "type": "string" - } - }, - "required": ["sessionID"], - "additionalProperties": false, - "description": "Replayed sync events" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Validate and replay a complete sync event history.", - "summary": "Replay sync events", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "events": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "minimum": 0 - }, - "type": { - "type": "string" - }, - "data": { - "type": "object" - } - }, - "required": ["id", "aggregateID", "seq", "type", "data"], - "additionalProperties": false - } - } - }, - "required": ["directory", "events"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.sync.replay({\n ...\n})" - } - ] - } - }, - "/sync/steal": { - "post": { - "tags": ["sync"], - "operationId": "sync.steal", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Session stolen into workspace", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["sessionID"], - "additionalProperties": false, - "description": "Session stolen into workspace" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Update a session to belong to the current workspace through the sync event system.", - "summary": "Steal session into workspace", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.sync.steal({\n ...\n})" - } - ] - } - }, - "/sync/history": { - "post": { - "tags": ["sync"], - "operationId": "sync.history.list", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Sync events", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "aggregate_id": { - "type": "string" - }, - "seq": { - "type": "integer", - "minimum": 0 - }, - "type": { - "type": "string" - }, - "data": { - "type": "object" - } - }, - "required": ["id", "aggregate_id", "seq", "type", "data"], - "additionalProperties": false - }, - "description": "Sync events" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "List sync events for all aggregates. Keys are aggregate IDs the client already knows about, values are the last known sequence ID. Events with seq > value are returned for those aggregates. Aggregates not listed in the input get their full history.", - "summary": "List sync events", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "type": "integer", - "minimum": 0 - } - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.sync.history.list({\n ...\n})" - } - ] - } - }, - "/tui/append-prompt": { - "post": { - "tags": ["tui"], - "operationId": "tui.appendPrompt", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Prompt processed successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Prompt processed successfully" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Append prompt to the TUI.", - "summary": "Append TUI prompt", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.appendPrompt({\n ...\n})" - } - ] - } - }, - "/tui/open-help": { - "post": { - "tags": ["tui"], - "operationId": "tui.openHelp", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Help dialog opened successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Help dialog opened successfully" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Open the help dialog in the TUI to display user assistance information.", - "summary": "Open help dialog", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.openHelp({\n ...\n})" - } - ] - } - }, - "/tui/open-sessions": { - "post": { - "tags": ["tui"], - "operationId": "tui.openSessions", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Session dialog opened successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Session dialog opened successfully" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Open the session dialog.", - "summary": "Open sessions dialog", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.openSessions({\n ...\n})" - } - ] - } - }, - "/tui/open-themes": { - "post": { - "tags": ["tui"], - "operationId": "tui.openThemes", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Theme dialog opened successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Theme dialog opened successfully" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Open the theme dialog.", - "summary": "Open themes dialog", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.openThemes({\n ...\n})" - } - ] - } - }, - "/tui/open-models": { - "post": { - "tags": ["tui"], - "operationId": "tui.openModels", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Model dialog opened successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Model dialog opened successfully" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Open the model dialog.", - "summary": "Open models dialog", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.openModels({\n ...\n})" - } - ] - } - }, - "/tui/submit-prompt": { - "post": { - "tags": ["tui"], - "operationId": "tui.submitPrompt", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Prompt submitted successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Prompt submitted successfully" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Submit the prompt.", - "summary": "Submit TUI prompt", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.submitPrompt({\n ...\n})" - } - ] - } - }, - "/tui/clear-prompt": { - "post": { - "tags": ["tui"], - "operationId": "tui.clearPrompt", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Prompt cleared successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Prompt cleared successfully" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Clear the prompt.", - "summary": "Clear TUI prompt", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.clearPrompt({\n ...\n})" - } - ] - } - }, - "/tui/execute-command": { - "post": { - "tags": ["tui"], - "operationId": "tui.executeCommand", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Command executed successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Command executed successfully" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Execute a TUI command.", - "summary": "Execute TUI command", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "command": { - "type": "string" - } - }, - "required": ["command"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.executeCommand({\n ...\n})" - } - ] - } - }, - "/tui/show-toast": { - "post": { - "tags": ["tui"], - "operationId": "tui.showToast", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Toast notification shown successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Toast notification shown successfully" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Show a toast notification in the TUI.", - "summary": "Show TUI toast", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "message": { - "type": "string" - }, - "variant": { - "type": "string", - "enum": ["info", "success", "warning", "error"] - }, - "duration": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["message", "variant"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.showToast({\n ...\n})" - } - ] - } - }, - "/tui/publish": { - "post": { - "tags": ["tui"], - "operationId": "tui.publish", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Event published successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Event published successfully" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Publish a TUI event.", - "summary": "Publish TUI event", - "requestBody": { - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/EventTuiPromptAppend" - }, - { - "$ref": "#/components/schemas/EventTuiCommandExecute" - }, - { - "$ref": "#/components/schemas/EventTuiToastShow" - }, - { - "$ref": "#/components/schemas/EventTuiSessionSelect" - } - ] - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.publish({\n ...\n})" - } - ] - } - }, - "/tui/select-session": { - "post": { - "tags": ["tui"], - "operationId": "tui.selectSession", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Session selected successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Session selected successfully" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "description": "Navigate the TUI to display the specified session.", - "summary": "Select session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses", - "description": "Session ID to navigate to" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.selectSession({\n ...\n})" - } - ] - } - }, - "/tui/control/next": { - "get": { - "tags": ["tui"], - "operationId": "tui.control.next", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Next TUI request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "path": { - "type": "string" - }, - "body": {} - }, - "required": ["path", "body"], - "additionalProperties": false, - "description": "Next TUI request" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Retrieve the next TUI request from the queue for processing.", - "summary": "Get next TUI request", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.control.next({\n ...\n})" - } - ] - } - }, - "/tui/control/response": { - "post": { - "tags": ["tui"], - "operationId": "tui.control.response", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Response submitted successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Response submitted successfully" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Submit a response to the TUI request queue to complete a pending request.", - "summary": "Submit TUI response", - "requestBody": { - "content": { - "application/json": { - "schema": {} - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.control.response({\n ...\n})" - } - ] - } - }, - "/experimental/workspace/adapter": { - "get": { - "tags": ["workspace"], - "operationId": "experimental.workspace.adapter.list", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Workspace adapters", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "name": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": ["type", "name", "description"], - "additionalProperties": false - }, - "description": "Workspace adapters" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "List all available workspace adapters for the current project.", - "summary": "List workspace adapters", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.adapter.list({\n ...\n})" - } - ] - } - }, - "/experimental/workspace": { - "get": { - "tags": ["workspace"], - "operationId": "experimental.workspace.list", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Workspaces", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Workspace" - }, - "description": "Workspaces" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "List all workspaces.", - "summary": "List workspaces", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.list({\n ...\n})" - } - ] - }, - "post": { - "tags": ["workspace"], - "operationId": "experimental.workspace.create", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Workspace created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Workspace" - } - } - } - }, - "400": { - "description": "WorkspaceCreateError | BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/WorkspaceCreateError" - }, - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Create a workspace for the current project.", - "summary": "Create workspace", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^wrk" - }, - "type": { - "type": "string" - }, - "branch": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "extra": { - "anyOf": [ - {}, - { - "type": "null" - } - ] - } - }, - "required": ["type"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.create({\n ...\n})" - } - ] - } - }, - "/experimental/workspace/sync-list": { - "post": { - "tags": ["workspace"], - "operationId": "experimental.workspace.syncList", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "204": { - "description": "Workspace list synced" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Register missing workspaces returned by workspace adapters.", - "summary": "Sync workspace list", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.syncList({\n ...\n})" - } - ] - } - }, - "/experimental/workspace/status": { - "get": { - "tags": ["workspace"], - "operationId": "experimental.workspace.status", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Workspace status", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WorkspaceEventConnectionStatus" - }, - "description": "Workspace status" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get connection status for workspaces in the current project.", - "summary": "Workspace status", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.status({\n ...\n})" - } - ] - } - }, - "/experimental/workspace/{id}": { - "delete": { - "tags": ["workspace"], - "operationId": "experimental.workspace.remove", - "parameters": [ - { - "name": "id", - "in": "path", - "schema": { - "type": "string", - "pattern": "^wrk" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Workspace removed", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Workspace" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Remove an existing workspace.", - "summary": "Remove workspace", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.remove({\n ...\n})" - } - ] - } - }, - "/experimental/workspace/warp": { - "post": { - "tags": ["workspace"], - "operationId": "experimental.workspace.warp", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "204": { - "description": "Session warped" - }, - "400": { - "description": "WorkspaceWarpError | VcsApplyError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/WorkspaceWarpError" - }, - { - "$ref": "#/components/schemas/VcsApplyError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "description": "Move a session's sync history into the target workspace, or detach it to the local project.", - "summary": "Warp session into workspace", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { - "anyOf": [ - { - "type": "string", - "pattern": "^wrk" - }, - { - "type": "null" - } - ] - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "copyChanges": { - "type": "boolean" - } - }, - "required": ["id", "sessionID"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.warp({\n ...\n})" - } - ] - } - }, - "/api/health": { - "get": { - "tags": ["opencode HttpApi"], - "operationId": "v2.health.get", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "healthy": { - "type": "boolean", - "enum": [true] - } - }, - "required": ["healthy"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Check whether the API server is ready to accept requests.", - "summary": "Check server health", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.health.get({\n ...\n})" - } - ] - } - }, - "/api/location": { - "get": { - "tags": ["opencode HttpApi"], - "operationId": "v2.location.get", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Location.Info", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LocationInfo" - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Resolve the requested location or the server default location.", - "summary": "Get location", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.location.get({\n ...\n})" - } - ] - } - }, - "/api/agent": { - "get": { - "tags": ["opencode HttpApi"], - "operationId": "v2.agent.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AgentV2Info" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Retrieve currently registered agents.", - "summary": "List agents", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.agent.list({\n ...\n})" - } - ] - } - }, - "/api/session": { - "get": { - "tags": ["sessions"], - "operationId": "v2.session.list", - "parameters": [ - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string", - "pattern": "^wrk" - }, - "required": false - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "number" - }, - "required": false - }, - { - "name": "order", - "in": "query", - "schema": { - "type": "string", - "enum": ["asc", "desc"] - }, - "required": false - }, - { - "name": "search", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "project", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "subpath", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "cursor", - "in": "query", - "schema": { - "type": "string", - "description": "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response." - }, - "required": false - } - ], - "security": [], - "responses": { - "200": { - "description": "SessionsResponse", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionsResponse" - } - } - } - }, - "400": { - "description": "InvalidCursorError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidCursorError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list.", - "summary": "List sessions", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.list({\n ...\n})" - } - ] - }, - "post": { - "tags": ["sessions"], - "operationId": "v2.session.create", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/SessionV2Info" - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Create a session at the requested location.", - "summary": "Create session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^ses" - }, - "agent": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - } - }, - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.create({\n ...\n})" - } - ] - } - }, - "/api/session/active": { - "get": { - "tags": ["sessions"], - "operationId": "v2.session.active", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "patternProperties": { - "^ses": { - "$ref": "#/components/schemas/SessionActive" - } - } - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Retrieve foreground Session drains currently owned by this OpenCode process. Sessions absent from the result are inactive.", - "summary": "List active sessions", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.active({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}": { - "get": { - "tags": ["sessions"], - "operationId": "v2.session.get", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/SessionV2Info" - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Retrieve a session by ID.", - "summary": "Get session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.get({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/agent": { - "post": { - "tags": ["sessions"], - "operationId": "v2.session.switchAgent", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Switch the agent used by subsequent provider turns.", - "summary": "Switch session agent", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "agent": { - "type": "string" - } - }, - "required": ["agent"], - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.switchAgent({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/model": { - "post": { - "tags": ["sessions"], - "operationId": "v2.session.switchModel", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Switch the model used by subsequent provider turns.", - "summary": "Switch session model", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "model": { - "$ref": "#/components/schemas/ModelRef" - } - }, - "required": ["model"], - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.switchModel({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/prompt": { - "post": { - "tags": ["sessions"], - "operationId": "v2.session.prompt", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/SessionInputAdmitted" - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - }, - "409": { - "description": "ConflictError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConflictError" - } - } - } - } - }, - "description": "Durably admit one session input and schedule agent-loop execution unless resume is false.", - "summary": "Send message", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^msg_" - }, - "prompt": { - "$ref": "#/components/schemas/PromptInput" - }, - "delivery": { - "type": "string", - "enum": ["steer", "queue"] - }, - "resume": { - "type": "boolean" - } - }, - "required": ["prompt"], - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.prompt({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/compact": { - "post": { - "tags": ["sessions"], - "operationId": "v2.session.compact", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - }, - "503": { - "description": "ServiceUnavailableError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ServiceUnavailableError" - } - } - } - } - }, - "description": "Compact a session conversation.", - "summary": "Compact session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.compact({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/wait": { - "post": { - "tags": ["sessions"], - "operationId": "v2.session.wait", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - }, - "503": { - "description": "ServiceUnavailableError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ServiceUnavailableError" - } - } - } - } - }, - "description": "Wait for a session agent loop to become idle.", - "summary": "Wait for session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.wait({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/revert/stage": { - "post": { - "tags": ["sessions"], - "operationId": "v2.session.revert.stage", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/RevertState" - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "MessageNotFoundError | SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/MessageNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - }, - "500": { - "description": "UnknownError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnknownError1" - } - } - } - } - }, - "description": "Stage or move a reversible session boundary and optionally apply its file changes.", - "summary": "Stage session revert", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "files": { - "type": "boolean" - } - }, - "required": ["messageID"], - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.revert.stage({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/revert/clear": { - "post": { - "tags": ["sessions"], - "operationId": "v2.session.revert.clear", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - }, - "500": { - "description": "UnknownError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnknownError1" - } - } - } - } - }, - "summary": "Clear staged revert", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.revert.clear({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/revert/commit": { - "post": { - "tags": ["sessions"], - "operationId": "v2.session.revert.commit", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "summary": "Commit staged revert", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.revert.commit({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/context": { - "get": { - "tags": ["sessions"], - "operationId": "v2.session.context", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SessionMessage" - } - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - }, - "500": { - "description": "UnknownError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnknownError1" - } - } - } - } - }, - "description": "Retrieve the active context messages for a session (all messages after the last compaction).", - "summary": "Get session context", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.context({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/history": { - "get": { - "tags": ["sessions"], - "operationId": "v2.session.history", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "after", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "security": [], - "responses": { - "200": { - "description": "SessionHistory", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionHistory" - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Read one finite page of public durable Session events after an exclusive aggregate sequence. Newly committed events may appear on later pages.", - "summary": "Get session history", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.history({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/event": { - "get": { - "tags": ["sessions"], - "operationId": "v2.session.events", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "after", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "text/event-stream": { - "schema": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "event": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/SessionDurableEventStream" - } - }, - "required": ["id", "event", "data"], - "additionalProperties": false - }, - "x-effect-stream": { - "encoding": "sse", - "causeSchema": { - "type": "array", - "items": { - "anyOf": [ - { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["Fail"] - }, - "error": { - "not": {} - } - }, - "required": ["_tag", "error"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["Die"] - }, - "defect": {} - }, - "required": ["_tag", "defect"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["Interrupt"] - }, - "fiberId": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ] - } - }, - "required": ["_tag", "fiberId"], - "additionalProperties": false - } - ] - } - }, - "errorSchema": { - "not": {} - }, - "failureEvent": "effect/httpapi/stream/failure" - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Replay durable events after an aggregate sequence, then continue with new durable events.", - "summary": "Subscribe to session events", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.events({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/interrupt": { - "post": { - "tags": ["sessions"], - "operationId": "v2.session.interrupt", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Interrupt active execution owned by this OpenCode process. Idle interruption is a no-op.", - "summary": "Interrupt session execution", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.interrupt({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/message/{messageID}": { - "get": { - "tags": ["sessions"], - "operationId": "v2.session.message", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "messageID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^msg_" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/SessionMessage" - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | MessageNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/MessageNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Retrieve one projected message owned by the Session.", - "summary": "Get session message", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.message({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/message": { - "get": { - "tags": ["messages"], - "operationId": "v2.session.messages", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "number" - }, - "required": false - }, - { - "name": "order", - "in": "query", - "schema": { - "type": "string", - "enum": ["asc", "desc"] - }, - "required": false - }, - { - "name": "cursor", - "in": "query", - "schema": { - "type": "string", - "description": "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order." - }, - "required": false - } - ], - "security": [], - "responses": { - "200": { - "description": "SessionMessagesResponse", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionMessagesResponse" - } - } - } - }, - "400": { - "description": "InvalidCursorError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidCursorError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - }, - "500": { - "description": "UnknownError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnknownError1" - } - } - } - } - }, - "description": "Retrieve projected messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.", - "summary": "Get session messages", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.messages({\n ...\n})" - } - ] - } - }, - "/api/model": { - "get": { - "tags": ["models"], - "operationId": "v2.model.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ModelV2Info" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "503": { - "description": "ServiceUnavailableError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ServiceUnavailableError" - } - } - } - } - }, - "description": "Retrieve available models ordered by release date.", - "summary": "List models", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.model.list({\n ...\n})" - } - ] - } - }, - "/api/provider": { - "get": { - "tags": ["providers"], - "operationId": "v2.provider.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProviderV2Info" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "503": { - "description": "ServiceUnavailableError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ServiceUnavailableError" - } - } - } - } - }, - "description": "Retrieve active AI providers so clients can show provider availability and configuration.", - "summary": "List providers", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.provider.list({\n ...\n})" - } - ] - } - }, - "/api/provider/{providerID}": { - "get": { - "tags": ["providers"], - "operationId": "v2.provider.get", - "parameters": [ - { - "name": "providerID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "$ref": "#/components/schemas/ProviderV2Info" - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "ProviderNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProviderNotFoundError" - } - } - } - }, - "503": { - "description": "ServiceUnavailableError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ServiceUnavailableError" - } - } - } - } - }, - "description": "Retrieve a single AI provider so clients can inspect its availability and endpoint settings.", - "summary": "Get provider", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.provider.get({\n ...\n})" - } - ] - } - }, - "/api/integration": { - "get": { - "tags": ["integrations"], - "operationId": "v2.integration.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IntegrationInfo" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Retrieve available integrations and their authentication methods.", - "summary": "List integrations", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.integration.list({\n ...\n})" - } - ] - } - }, - "/api/integration/{integrationID}": { - "get": { - "tags": ["integrations"], - "operationId": "v2.integration.get", - "parameters": [ - { - "name": "integrationID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "$ref": "#/components/schemas/IntegrationInfo" - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Retrieve one integration and its authentication methods.", - "summary": "Get integration", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.integration.get({\n ...\n})" - } - ] - } - }, - "/api/integration/{integrationID}/connect/key": { - "post": { - "tags": ["integrations"], - "operationId": "v2.integration.connect.key", - "parameters": [ - { - "name": "integrationID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Run a key authentication method and store the resulting credential.", - "summary": "Connect with key", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "label": { - "type": "string" - } - }, - "required": ["key"], - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.integration.connect.key({\n ...\n})" - } - ] - } - }, - "/api/integration/{integrationID}/connect/oauth": { - "post": { - "tags": ["integrations"], - "operationId": "v2.integration.connect.oauth", - "parameters": [ - { - "name": "integrationID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "$ref": "#/components/schemas/IntegrationAttempt" - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Start an OAuth attempt and return the authorization details.", - "summary": "Begin OAuth connection", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "methodID": { - "type": "string" - }, - "inputs": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "label": { - "type": "string" - } - }, - "required": ["methodID", "inputs"], - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.integration.connect.oauth({\n ...\n})" - } - ] - } - }, - "/api/integration/attempt/{attemptID}": { - "get": { - "tags": ["integrations"], - "operationId": "v2.integration.attempt.status", - "parameters": [ - { - "name": "attemptID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "$ref": "#/components/schemas/IntegrationAttemptStatus" - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Poll the current status of an OAuth attempt.", - "summary": "Get OAuth attempt status", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.integration.attempt.status({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["integrations"], - "operationId": "v2.integration.attempt.cancel", - "parameters": [ - { - "name": "attemptID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Cancel an OAuth attempt and release its resources.", - "summary": "Cancel OAuth connection", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.integration.attempt.cancel({\n ...\n})" - } - ] - } - }, - "/api/integration/attempt/{attemptID}/complete": { - "post": { - "tags": ["integrations"], - "operationId": "v2.integration.attempt.complete", - "parameters": [ - { - "name": "attemptID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Complete a code-based OAuth attempt and store the resulting credential.", - "summary": "Complete OAuth connection", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": { - "type": "string" - } - }, - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.integration.attempt.complete({\n ...\n})" - } - ] - } - }, - "/api/credential/{credentialID}": { - "patch": { - "tags": ["opencode HttpApi"], - "operationId": "v2.credential.update", - "parameters": [ - { - "name": "credentialID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Update a stored credential label.", - "summary": "Update credential", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "label": { - "type": "string" - } - }, - "required": ["label"], - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.credential.update({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["opencode HttpApi"], - "operationId": "v2.credential.remove", - "parameters": [ - { - "name": "credentialID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Remove a stored integration credential.", - "summary": "Remove credential", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.credential.remove({\n ...\n})" - } - ] - } - }, - "/api/permission/request": { - "get": { - "tags": ["permissions"], - "operationId": "v2.permission.request.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionV2Request" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Retrieve pending permission requests for a location.", - "summary": "List pending permission requests", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.permission.request.list({\n ...\n})" - } - ] - } - }, - "/api/permission/saved": { - "get": { - "tags": ["permissions"], - "operationId": "v2.permission.saved.list", - "parameters": [ - { - "name": "projectID", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionSavedInfo" - } - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Retrieve saved permissions, optionally filtered by project.", - "summary": "List saved permissions", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.permission.saved.list({\n ...\n})" - } - ] - } - }, - "/api/permission/saved/{id}": { - "delete": { - "tags": ["permissions"], - "operationId": "v2.permission.saved.remove", - "parameters": [ - { - "name": "id", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Remove a saved permission by ID.", - "summary": "Remove saved permission", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.permission.saved.remove({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/permission": { - "post": { - "tags": ["permissions"], - "operationId": "v2.session.permission.create", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per" - }, - "effect": { - "$ref": "#/components/schemas/PermissionV2Effect" - } - }, - "required": ["id", "effect"], - "additionalProperties": false - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Evaluate and, when approval is required, create a permission request for a session.", - "summary": "Create permission request", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per" - }, - "action": { - "type": "string" - }, - "resources": { - "type": "array", - "items": { - "type": "string" - } - }, - "save": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "source": { - "$ref": "#/components/schemas/PermissionV2Source" - }, - "agent": { - "type": "string" - } - }, - "required": ["action", "resources"], - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.permission.create({\n ...\n})" - } - ] - }, - "get": { - "tags": ["permissions"], - "operationId": "v2.session.permission.list", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionV2Request" - } - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Retrieve pending permission requests owned by a session.", - "summary": "List session permission requests", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.permission.list({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/permission/{requestID}": { - "get": { - "tags": ["permissions"], - "operationId": "v2.session.permission.get", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "requestID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^per" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/PermissionV2Request" - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | PermissionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/PermissionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Retrieve a pending permission request owned by a session.", - "summary": "Get permission request", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.permission.get({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/permission/{requestID}/reply": { - "post": { - "tags": ["permissions"], - "operationId": "v2.session.permission.reply", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "requestID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^per" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | PermissionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/PermissionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Respond to a pending permission request owned by a session.", - "summary": "Reply to pending permission request", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "reply": { - "$ref": "#/components/schemas/PermissionV2Reply" - }, - "message": { - "type": "string" - } - }, - "required": ["reply"], - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.permission.reply({\n ...\n})" - } - ] - } - }, - "/api/fs/read/*": { - "get": { - "tags": ["filesystem"], - "operationId": "v2.fs.read", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/octet-stream": { - "schema": { - "type": "string", - "format": "binary" - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Serve one file relative to the requested location.", - "summary": "Read file", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.fs.read({\n ...\n})" - } - ] - } - }, - "/api/fs/list": { - "get": { - "tags": ["filesystem"], - "operationId": "v2.fs.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - }, - { - "name": "path", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileSystemEntry" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "List direct children of one directory relative to the requested location.", - "summary": "List directory", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.fs.list({\n ...\n})" - } - ] - } - }, - "/api/fs/find": { - "get": { - "tags": ["filesystem"], - "operationId": "v2.fs.find", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - }, - { - "name": "query", - "in": "query", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "type", - "in": "query", - "schema": { - "type": "string", - "enum": ["file", "directory"] - }, - "required": false - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileSystemEntry" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Find recursively ranked filesystem entries relative to the requested location.", - "summary": "Find files", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.fs.find({\n ...\n})" - } - ] - } - }, - "/api/command": { - "get": { - "tags": ["commands"], - "operationId": "v2.command.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CommandV2Info" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Retrieve currently registered commands.", - "summary": "List commands", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.command.list({\n ...\n})" - } - ] - } - }, - "/api/skill": { - "get": { - "tags": ["skills"], - "operationId": "v2.skill.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SkillV2Info" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Retrieve currently registered skills.", - "summary": "List skills", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.skill.list({\n ...\n})" - } - ] - } - }, - "/api/event": { - "get": { - "tags": ["events"], - "operationId": "v2.event.subscribe", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Event stream", - "content": { - "text/event-stream": { - "schema": { - "$ref": "#/components/schemas/V2Event" - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Subscribe to native event payloads for the server.", - "summary": "Subscribe to events", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.event.subscribe({\n ...\n})" - } - ] - } - }, - "/api/pty": { - "get": { - "tags": ["pty"], - "operationId": "v2.pty.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pty" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "List PTY sessions for a location, including exited sessions retained until removal.", - "summary": "List PTY sessions", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.pty.list({\n ...\n})" - } - ] - }, - "post": { - "tags": ["pty"], - "operationId": "v2.pty.create", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Create a pseudo-terminal session for a location.", - "summary": "Create PTY session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "command": { - "type": "string" - }, - "args": { - "type": "array", - "items": { - "type": "string" - } - }, - "cwd": { - "type": "string" - }, - "title": { - "type": "string" - }, - "env": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.pty.create({\n ...\n})" - } - ] - } - }, - "/api/pty/{ptyID}": { - "get": { - "tags": ["pty"], - "operationId": "v2.pty.get", - "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^pty" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "PtyNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } - } - }, - "description": "Get one PTY session, including its exit code once exited.", - "summary": "Get PTY session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.pty.get({\n ...\n})" - } - ] - }, - "put": { - "tags": ["pty"], - "operationId": "v2.pty.update", - "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^pty" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "PtyNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } - } - }, - "description": "Update the title or viewport size of one PTY session.", - "summary": "Update PTY session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "size": { - "type": "object", - "properties": { - "rows": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "cols": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["rows", "cols"], - "additionalProperties": false - } - }, - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.pty.update({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["pty"], - "operationId": "v2.pty.remove", - "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^pty" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "PtyNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } - } - }, - "description": "Terminate and remove one PTY session.", - "summary": "Remove PTY session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.pty.remove({\n ...\n})" - } - ] - } - }, - "/api/pty/{ptyID}/connect-token": { - "post": { - "tags": ["pty"], - "operationId": "v2.pty.connectToken", - "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^pty" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "$ref": "#/components/schemas/PtyTicketConnectToken" - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "403": { - "description": "ForbiddenError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ForbiddenError" - } - } - } - }, - "404": { - "description": "PtyNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } - } - }, - "description": "Create a short-lived single-use ticket for opening a PTY WebSocket connection.", - "summary": "Create PTY WebSocket token", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.pty.connectToken({\n ...\n})" - } - ] - } - }, - "/api/pty/{ptyID}/connect": { - "get": { - "tags": ["pty"], - "operationId": "v2.pty.connect", - "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^pty" - }, - "required": true - }, - { - "in": "query", - "name": "location[directory]", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "location[workspace]", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "cursor", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "ticket", - "schema": { - "type": "string" - } - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "403": { - "description": "ForbiddenError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ForbiddenError" - } - } - } - }, - "404": { - "description": "PtyNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } - } - }, - "description": "Establish a WebSocket connection streaming PTY output and accepting terminal input.", - "summary": "Connect to PTY session", - "x-websocket": true, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.pty.connect({\n ...\n})" - } - ] - } - }, - "/api/question/request": { - "get": { - "tags": ["session questions"], - "operationId": "v2.question.request.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Request" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Retrieve pending question requests for a location.", - "summary": "List pending question requests", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.question.request.list({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/question": { - "get": { - "tags": ["session questions"], - "operationId": "v2.session.question.list", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Request" - } - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Retrieve pending question requests owned by a session.", - "summary": "List session question requests", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.question.list({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/question/{requestID}/reply": { - "post": { - "tags": ["session questions"], - "operationId": "v2.session.question.reply", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "requestID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^que" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | QuestionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/QuestionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Answer a pending question request owned by a session.", - "summary": "Reply to pending question request", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/QuestionV2Reply" - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.question.reply({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/question/{requestID}/reject": { - "post": { - "tags": ["session questions"], - "operationId": "v2.session.question.reject", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "requestID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^que" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | QuestionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/QuestionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Reject a pending question request owned by a session.", - "summary": "Reject pending question request", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.question.reject({\n ...\n})" - } - ] - } - }, - "/api/reference": { - "get": { - "tags": ["reference"], - "operationId": "v2.reference.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ReferenceInfo" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "List references available in the requested location.", - "summary": "List references", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.reference.list({\n ...\n})" - } - ] - } - }, - "/experimental/project/{projectID}/copy": { - "post": { - "tags": ["projectCopy"], - "operationId": "v2.projectCopy.create", - "parameters": [ - { - "name": "projectID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "responses": { - "200": { - "description": "ProjectCopy.Copy", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProjectCopyCopy" - } - } - } - }, - "400": { - "description": "ProjectCopyError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProjectCopyError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "strategy": { - "type": "string" - }, - "directory": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": ["strategy", "directory"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.projectCopy.create({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["projectCopy"], - "operationId": "v2.projectCopy.remove", - "parameters": [ - { - "name": "projectID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "ProjectCopyError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProjectCopyError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "force": { - "type": "boolean" - } - }, - "required": ["directory", "force"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.projectCopy.remove({\n ...\n})" - } - ] - } - }, - "/experimental/project/{projectID}/copy/refresh": { - "post": { - "tags": ["projectCopy"], - "operationId": "v2.projectCopy.refresh", - "parameters": [ - { - "name": "projectID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "ProjectCopyError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProjectCopyError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.projectCopy.refresh({\n ...\n})" - } - ] - } - }, - "/pty/{ptyID}/connect": { - "get": { - "tags": ["pty"], - "operationId": "pty.connect", - "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^pty" - }, - "required": true - }, - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "workspace", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "cursor", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "ticket", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Connected session", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Connected session" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/effect_HttpApiError_Forbidden" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "description": "Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time.", - "summary": "Connect to PTY session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.connect({\n ...\n})" - } - ] - } - } - }, - "components": { - "schemas": { - "Event": { - "anyOf": [ - { - "$ref": "#/components/schemas/EventModels-devRefreshed" - }, - { - "$ref": "#/components/schemas/EventIntegrationUpdated" - }, - { - "$ref": "#/components/schemas/EventIntegrationConnectionUpdated" - }, - { - "$ref": "#/components/schemas/EventCatalogUpdated" - }, - { - "$ref": "#/components/schemas/EventSessionCreated" - }, - { - "$ref": "#/components/schemas/EventSessionUpdated" - }, - { - "$ref": "#/components/schemas/EventSessionDeleted" - }, - { - "$ref": "#/components/schemas/EventMessageUpdated" - }, - { - "$ref": "#/components/schemas/EventMessageRemoved" - }, - { - "$ref": "#/components/schemas/EventMessagePartUpdated" - }, - { - "$ref": "#/components/schemas/EventMessagePartRemoved" - }, - { - "$ref": "#/components/schemas/EventSessionNextAgentSwitched" - }, - { - "$ref": "#/components/schemas/EventSessionNextModelSwitched" - }, - { - "$ref": "#/components/schemas/EventSessionNextMoved" - }, - { - "$ref": "#/components/schemas/EventSessionNextPrompted" - }, - { - "$ref": "#/components/schemas/EventSessionNextPromptAdmitted" - }, - { - "$ref": "#/components/schemas/EventSessionNextContextUpdated" - }, - { - "$ref": "#/components/schemas/EventSessionNextSynthetic" - }, - { - "$ref": "#/components/schemas/EventSessionNextShellStarted" - }, - { - "$ref": "#/components/schemas/EventSessionNextShellEnded" - }, - { - "$ref": "#/components/schemas/EventSessionNextStepStarted" - }, - { - "$ref": "#/components/schemas/EventSessionNextStepEnded" - }, - { - "$ref": "#/components/schemas/EventSessionNextStepFailed" - }, - { - "$ref": "#/components/schemas/EventSessionNextTextStarted" - }, - { - "$ref": "#/components/schemas/EventSessionNextTextDelta" - }, - { - "$ref": "#/components/schemas/EventSessionNextTextEnded" - }, - { - "$ref": "#/components/schemas/EventSessionNextReasoningStarted" - }, - { - "$ref": "#/components/schemas/EventSessionNextReasoningDelta" - }, - { - "$ref": "#/components/schemas/EventSessionNextReasoningEnded" - }, - { - "$ref": "#/components/schemas/EventSessionNextToolInputStarted" - }, - { - "$ref": "#/components/schemas/EventSessionNextToolInputDelta" - }, - { - "$ref": "#/components/schemas/EventSessionNextToolInputEnded" - }, - { - "$ref": "#/components/schemas/EventSessionNextToolCalled" - }, - { - "$ref": "#/components/schemas/EventSessionNextToolProgress" - }, - { - "$ref": "#/components/schemas/EventSessionNextToolSuccess" - }, - { - "$ref": "#/components/schemas/EventSessionNextToolFailed" - }, - { - "$ref": "#/components/schemas/EventSessionNextRetried" - }, - { - "$ref": "#/components/schemas/EventSessionNextCompactionStarted" - }, - { - "$ref": "#/components/schemas/EventSessionNextCompactionDelta" - }, - { - "$ref": "#/components/schemas/EventSessionNextCompactionEnded" - }, - { - "$ref": "#/components/schemas/EventSessionNextRevertStaged" - }, - { - "$ref": "#/components/schemas/EventSessionNextRevertCleared" - }, - { - "$ref": "#/components/schemas/EventSessionNextRevertCommitted" - }, - { - "$ref": "#/components/schemas/EventMessagePartDelta" - }, - { - "$ref": "#/components/schemas/EventSessionDiff" - }, - { - "$ref": "#/components/schemas/EventSessionError" - }, - { - "$ref": "#/components/schemas/EventInstallationUpdated" - }, - { - "$ref": "#/components/schemas/EventInstallationUpdate-available" - }, - { - "$ref": "#/components/schemas/EventFileEdited" - }, - { - "$ref": "#/components/schemas/EventReferenceUpdated" - }, - { - "$ref": "#/components/schemas/EventPermissionV2Asked" - }, - { - "$ref": "#/components/schemas/EventPermissionV2Replied" - }, - { - "$ref": "#/components/schemas/EventPluginAdded" - }, - { - "$ref": "#/components/schemas/EventProjectDirectoriesUpdated" - }, - { - "$ref": "#/components/schemas/EventFileWatcherUpdated" - }, - { - "$ref": "#/components/schemas/EventPtyCreated" - }, - { - "$ref": "#/components/schemas/EventPtyUpdated" - }, - { - "$ref": "#/components/schemas/EventPtyExited" - }, - { - "$ref": "#/components/schemas/EventPtyDeleted" - }, - { - "$ref": "#/components/schemas/EventQuestionV2Asked" - }, - { - "$ref": "#/components/schemas/EventQuestionV2Replied" - }, - { - "$ref": "#/components/schemas/EventQuestionV2Rejected" - }, - { - "$ref": "#/components/schemas/EventLspUpdated" - }, - { - "$ref": "#/components/schemas/EventPermissionAsked" - }, - { - "$ref": "#/components/schemas/EventPermissionReplied" - }, - { - "$ref": "#/components/schemas/Event.tui.prompt.append" - }, - { - "$ref": "#/components/schemas/Event.tui.command.execute" - }, - { - "$ref": "#/components/schemas/Event.tui.toast.show" - }, - { - "$ref": "#/components/schemas/Event.tui.session.select" - }, - { - "$ref": "#/components/schemas/EventMcpToolsChanged" - }, - { - "$ref": "#/components/schemas/EventMcpBrowserOpenFailed" - }, - { - "$ref": "#/components/schemas/EventCommandExecuted" - }, - { - "$ref": "#/components/schemas/EventProjectUpdated" - }, - { - "$ref": "#/components/schemas/EventSessionStatus" - }, - { - "$ref": "#/components/schemas/EventSessionIdle" - }, - { - "$ref": "#/components/schemas/EventQuestionAsked" - }, - { - "$ref": "#/components/schemas/EventQuestionReplied" - }, - { - "$ref": "#/components/schemas/EventQuestionRejected" - }, - { - "$ref": "#/components/schemas/EventSessionCompacted" - }, - { - "$ref": "#/components/schemas/EventVcsBranchUpdated" - }, - { - "$ref": "#/components/schemas/EventWorkspaceReady" - }, - { - "$ref": "#/components/schemas/EventWorkspaceFailed" - }, - { - "$ref": "#/components/schemas/EventWorkspaceStatus" - }, - { - "$ref": "#/components/schemas/EventWorktreeReady" - }, - { - "$ref": "#/components/schemas/EventWorktreeFailed" - }, - { - "$ref": "#/components/schemas/EventServerConnected" - }, - { - "$ref": "#/components/schemas/EventGlobalDisposed" - }, - { - "$ref": "#/components/schemas/EventServerInstanceDisposed" - } - ] - }, - "QuestionReplied": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } - } - }, - "required": ["sessionID", "requestID", "answers"], - "additionalProperties": false - }, - "QuestionRejected": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - } - }, - "required": ["sessionID", "requestID"], - "additionalProperties": false - }, - "OAuth": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["oauth"] - }, - "refresh": { - "type": "string" - }, - "access": { - "type": "string" - }, - "expires": { - "type": "integer", - "minimum": 0 - }, - "accountId": { - "type": "string" - }, - "enterpriseUrl": { - "type": "string" - } - }, - "required": ["type", "refresh", "access", "expires"], - "additionalProperties": false - }, - "ApiAuth": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["api"] - }, - "key": { - "type": "string" - }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "required": ["type", "key"], - "additionalProperties": false - }, - "WellKnownAuth": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["wellknown"] - }, - "key": { - "type": "string" - }, - "token": { - "type": "string" - } - }, - "required": ["type", "key", "token"], - "additionalProperties": false - }, - "Auth": { - "anyOf": [ - { - "$ref": "#/components/schemas/OAuth" - }, - { - "$ref": "#/components/schemas/ApiAuth" - }, - { - "$ref": "#/components/schemas/WellKnownAuth" - } - ] - }, - "effect_HttpApiError_BadRequest": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["BadRequest"] - } - }, - "required": ["_tag"], - "additionalProperties": false - }, - "InvalidRequestError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["InvalidRequestError"] - }, - "message": { - "type": "string" - }, - "kind": { - "type": "string" - }, - "field": { - "type": "string" - } - }, - "required": ["_tag", "message"], - "additionalProperties": false - }, - "MoveSessionError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["MoveSessionError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "SnapshotFileDiff": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "patch": { - "type": "string" - }, - "additions": { - "type": "number" - }, - "deletions": { - "type": "number" - }, - "status": { - "type": "string", - "enum": ["added", "deleted", "modified"] - } - }, - "required": ["additions", "deletions"], - "additionalProperties": false - }, - "PermissionAction": { - "type": "string", - "enum": ["allow", "deny", "ask"] - }, - "PermissionRule": { - "type": "object", - "properties": { - "permission": { - "type": "string" - }, - "pattern": { - "type": "string" - }, - "action": { - "$ref": "#/components/schemas/PermissionAction" - } - }, - "required": ["permission", "pattern", "action"], - "additionalProperties": false - }, - "PermissionRuleset": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionRule" - } - }, - "Session": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^ses" - }, - "slug": { - "type": "string" - }, - "projectID": { - "type": "string" - }, - "workspaceID": { - "type": "string", - "pattern": "^wrk" - }, - "directory": { - "type": "string" - }, - "path": { - "type": "string" - }, - "parentID": { - "type": "string", - "pattern": "^ses" - }, - "summary": { - "type": "object", - "properties": { - "additions": { - "type": "number" - }, - "deletions": { - "type": "number" - }, - "files": { - "type": "number" - }, - "diffs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" - } - } - }, - "required": ["additions", "deletions", "files"], - "additionalProperties": false - }, - "cost": { - "type": "number" - }, - "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false - }, - "share": { - "type": "object", - "properties": { - "url": { - "type": "string" - } - }, - "required": ["url"], - "additionalProperties": false - }, - "title": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "variant": { - "type": "string" - } - }, - "required": ["id", "providerID"], - "additionalProperties": false - }, - "version": { - "type": "string" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "integer", - "minimum": 0 - }, - "updated": { - "type": "integer", - "minimum": 0 - }, - "compacting": { - "type": "integer", - "minimum": 0 - }, - "archived": { - "type": "number" - } - }, - "required": ["created", "updated"], - "additionalProperties": false - }, - "permission": { - "$ref": "#/components/schemas/PermissionRuleset" - }, - "revert": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "partID": { - "type": "string", - "pattern": "^prt" - }, - "snapshot": { - "type": "string" - }, - "diff": { - "type": "string" - } - }, - "required": ["messageID"], - "additionalProperties": false - } - }, - "required": ["id", "slug", "projectID", "directory", "title", "version", "time"], - "additionalProperties": false - }, - "OutputFormatText": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["text"] - } - }, - "required": ["type"], - "additionalProperties": false - }, - "JSONSchema": { - "type": "object" - }, - "OutputFormatJsonSchema": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["json_schema"] - }, - "schema": { - "$ref": "#/components/schemas/JSONSchema" - }, - "retryCount": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["type", "schema"], - "additionalProperties": false - }, - "OutputFormat": { - "anyOf": [ - { - "$ref": "#/components/schemas/OutputFormatText" - }, - { - "$ref": "#/components/schemas/OutputFormatJsonSchema" - } - ] - }, - "UserMessage": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^msg" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "role": { - "type": "string", - "enum": ["user"] - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number", - "minimum": 0 - } - }, - "required": ["created"], - "additionalProperties": false - }, - "format": { - "$ref": "#/components/schemas/OutputFormat" - }, - "summary": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "body": { - "type": "string" - }, - "diffs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" - } - } - }, - "required": ["diffs"], - "additionalProperties": false - }, - "agent": { - "type": "string" - }, - "model": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - }, - "variant": { - "type": "string" - } - }, - "required": ["providerID", "modelID"], - "additionalProperties": false - }, - "system": { - "type": "string" - }, - "tools": { - "type": "object", - "additionalProperties": { - "type": "boolean" - } - } - }, - "required": ["id", "sessionID", "role", "time", "agent", "model"], - "additionalProperties": false - }, - "ProviderAuthError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["ProviderAuthError"] - }, - "data": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["providerID", "message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "UnknownError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["UnknownError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "ref": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "MessageOutputLengthError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["MessageOutputLengthError"] - }, - "data": { - "type": "object", - "properties": {} - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "MessageAbortedError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["MessageAbortedError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "StructuredOutputError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["StructuredOutputError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "retries": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["message", "retries"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "ContextOverflowError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["ContextOverflowError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "responseBody": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "ContentFilterError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["ContentFilterError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "APIError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["APIError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "statusCode": { - "type": "integer", - "minimum": 0 - }, - "isRetryable": { - "type": "boolean" - }, - "responseHeaders": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "responseBody": { - "type": "string" - }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "required": ["message", "isRetryable"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "AssistantMessage": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^msg" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "role": { - "type": "string", - "enum": ["assistant"] - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "integer", - "minimum": 0 - }, - "completed": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["created"], - "additionalProperties": false - }, - "error": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProviderAuthError" - }, - { - "$ref": "#/components/schemas/UnknownError" - }, - { - "$ref": "#/components/schemas/MessageOutputLengthError" - }, - { - "$ref": "#/components/schemas/MessageAbortedError" - }, - { - "$ref": "#/components/schemas/StructuredOutputError" - }, - { - "$ref": "#/components/schemas/ContextOverflowError" - }, - { - "$ref": "#/components/schemas/ContentFilterError" - }, - { - "$ref": "#/components/schemas/APIError" - } - ] - }, - "parentID": { - "type": "string", - "pattern": "^msg" - }, - "modelID": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "mode": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "path": { - "type": "object", - "properties": { - "cwd": { - "type": "string" - }, - "root": { - "type": "string" - } - }, - "required": ["cwd", "root"], - "additionalProperties": false - }, - "summary": { - "type": "boolean" - }, - "cost": { - "type": "number" - }, - "tokens": { - "type": "object", - "properties": { - "total": { - "type": "number" - }, - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false - }, - "structured": {}, - "variant": { - "type": "string" - }, - "finish": { - "type": "string" - } - }, - "required": [ - "id", - "sessionID", - "role", - "time", - "parentID", - "modelID", - "providerID", - "mode", - "agent", - "path", - "cost", - "tokens" - ], - "additionalProperties": false - }, - "Message": { - "anyOf": [ - { - "$ref": "#/components/schemas/UserMessage" - }, - { - "$ref": "#/components/schemas/AssistantMessage" - } - ] - }, - "TextPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["text"] - }, - "text": { - "type": "string" - }, - "session.synthetic": { - "type": "boolean" - }, - "ignored": { - "type": "boolean" - }, - "time": { - "type": "object", - "properties": { - "start": { - "type": "integer", - "minimum": 0 - }, - "end": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["start"], - "additionalProperties": false - }, - "metadata": { - "type": "object" - } - }, - "required": ["id", "sessionID", "messageID", "type", "text"], - "additionalProperties": false - }, - "SubtaskPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["subtask"] - }, - "prompt": { - "type": "string" - }, - "description": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - } - }, - "required": ["providerID", "modelID"], - "additionalProperties": false - }, - "command": { - "type": "string" - } - }, - "required": ["id", "sessionID", "messageID", "type", "prompt", "description", "agent"], - "additionalProperties": false - }, - "ReasoningPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["reasoning"] - }, - "text": { - "type": "string" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "start": { - "type": "integer", - "minimum": 0 - }, - "end": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["start"], - "additionalProperties": false - } - }, - "required": ["id", "sessionID", "messageID", "type", "text", "time"], - "additionalProperties": false - }, - "FilePartSourceText": { - "type": "object", - "properties": { - "value": { - "type": "string" - }, - "start": { - "type": "number" - }, - "end": { - "type": "number" - } - }, - "required": ["value", "start", "end"], - "additionalProperties": false - }, - "FileSource": { - "type": "object", - "properties": { - "text": { - "$ref": "#/components/schemas/FilePartSourceText" - }, - "type": { - "type": "string", - "enum": ["file"] - }, - "path": { - "type": "string" - } - }, - "required": ["text", "type", "path"], - "additionalProperties": false - }, - "Range": { - "type": "object", - "properties": { - "start": { - "type": "object", - "properties": { - "line": { - "type": "integer", - "minimum": 0 - }, - "character": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["line", "character"], - "additionalProperties": false - }, - "end": { - "type": "object", - "properties": { - "line": { - "type": "integer", - "minimum": 0 - }, - "character": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["line", "character"], - "additionalProperties": false - } - }, - "required": ["start", "end"], - "additionalProperties": false - }, - "SymbolSource": { - "type": "object", - "properties": { - "text": { - "$ref": "#/components/schemas/FilePartSourceText" - }, - "type": { - "type": "string", - "enum": ["symbol"] - }, - "path": { - "type": "string" - }, - "range": { - "$ref": "#/components/schemas/Range" - }, - "name": { - "type": "string" - }, - "kind": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["text", "type", "path", "range", "name", "kind"], - "additionalProperties": false - }, - "ResourceSource": { - "type": "object", - "properties": { - "text": { - "$ref": "#/components/schemas/FilePartSourceText" - }, - "type": { - "type": "string", - "enum": ["resource"] - }, - "clientName": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": ["text", "type", "clientName", "uri"], - "additionalProperties": false - }, - "FilePartSource": { - "anyOf": [ - { - "$ref": "#/components/schemas/FileSource" - }, - { - "$ref": "#/components/schemas/SymbolSource" - }, - { - "$ref": "#/components/schemas/ResourceSource" - } - ] - }, - "FilePart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["file"] - }, - "mime": { - "type": "string" - }, - "filename": { - "type": "string" - }, - "url": { - "type": "string" - }, - "source": { - "$ref": "#/components/schemas/FilePartSource" - } - }, - "required": ["id", "sessionID", "messageID", "type", "mime", "url"], - "additionalProperties": false - }, - "ToolStatePending": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["pending"] - }, - "input": { - "type": "object" - }, - "raw": { - "type": "string" - } - }, - "required": ["status", "input", "raw"], - "additionalProperties": false - }, - "ToolStateRunning": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["running"] - }, - "input": { - "type": "object" - }, - "title": { - "type": "string" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "start": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["start"], - "additionalProperties": false - } - }, - "required": ["status", "input", "time"], - "additionalProperties": false - }, - "ToolStateCompleted": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["completed"] - }, - "input": { - "type": "object" - }, - "output": { - "type": "string" - }, - "title": { - "type": "string" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "start": { - "type": "integer", - "minimum": 0 - }, - "end": { - "type": "integer", - "minimum": 0 - }, - "compacted": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["start", "end"], - "additionalProperties": false - }, - "attachments": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FilePart" - } - } - }, - "required": ["status", "input", "output", "title", "metadata", "time"], - "additionalProperties": false - }, - "ToolStateError": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["error"] - }, - "input": { - "type": "object" - }, - "error": { - "type": "string" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "start": { - "type": "integer", - "minimum": 0 - }, - "end": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["start", "end"], - "additionalProperties": false - } - }, - "required": ["status", "input", "error", "time"], - "additionalProperties": false - }, - "ToolState": { - "anyOf": [ - { - "$ref": "#/components/schemas/ToolStatePending" - }, - { - "$ref": "#/components/schemas/ToolStateRunning" - }, - { - "$ref": "#/components/schemas/ToolStateCompleted" - }, - { - "$ref": "#/components/schemas/ToolStateError" - } - ] - }, - "ToolPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["tool"] - }, - "callID": { - "type": "string" - }, - "tool": { - "type": "string" - }, - "state": { - "$ref": "#/components/schemas/ToolState" - }, - "metadata": { - "type": "object" - } - }, - "required": ["id", "sessionID", "messageID", "type", "callID", "tool", "state"], - "additionalProperties": false - }, - "StepStartPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["step-start"] - }, - "snapshot": { - "type": "string" - } - }, - "required": ["id", "sessionID", "messageID", "type"], - "additionalProperties": false - }, - "StepFinishPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["step-finish"] - }, - "reason": { - "type": "string" - }, - "snapshot": { - "type": "string" - }, - "cost": { - "type": "number" - }, - "tokens": { - "type": "object", - "properties": { - "total": { - "type": "number" - }, - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false - } - }, - "required": ["id", "sessionID", "messageID", "type", "reason", "cost", "tokens"], - "additionalProperties": false - }, - "SnapshotPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["snapshot"] - }, - "snapshot": { - "type": "string" - } - }, - "required": ["id", "sessionID", "messageID", "type", "snapshot"], - "additionalProperties": false - }, - "PatchPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["patch"] - }, - "hash": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["id", "sessionID", "messageID", "type", "hash", "files"], - "additionalProperties": false - }, - "AgentPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["agent"] - }, - "name": { - "type": "string" - }, - "source": { - "type": "object", - "properties": { - "value": { - "type": "string" - }, - "start": { - "type": "integer", - "minimum": 0 - }, - "end": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["value", "start", "end"], - "additionalProperties": false - } - }, - "required": ["id", "sessionID", "messageID", "type", "name"], - "additionalProperties": false - }, - "RetryPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["retry"] - }, - "attempt": { - "type": "integer", - "minimum": 0 - }, - "error": { - "$ref": "#/components/schemas/APIError" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["created"], - "additionalProperties": false - } - }, - "required": ["id", "sessionID", "messageID", "type", "attempt", "error", "time"], - "additionalProperties": false - }, - "CompactionPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["compaction"] - }, - "auto": { - "type": "boolean" - }, - "overflow": { - "type": "boolean" - }, - "tail_start_id": { - "type": "string", - "pattern": "^msg" - } - }, - "required": ["id", "sessionID", "messageID", "type", "auto"], - "additionalProperties": false - }, - "Part": { - "anyOf": [ - { - "$ref": "#/components/schemas/TextPart" - }, - { - "$ref": "#/components/schemas/SubtaskPart" - }, - { - "$ref": "#/components/schemas/ReasoningPart" - }, - { - "$ref": "#/components/schemas/FilePart" - }, - { - "$ref": "#/components/schemas/ToolPart" - }, - { - "$ref": "#/components/schemas/StepStartPart" - }, - { - "$ref": "#/components/schemas/StepFinishPart" - }, - { - "$ref": "#/components/schemas/SnapshotPart" - }, - { - "$ref": "#/components/schemas/PatchPart" - }, - { - "$ref": "#/components/schemas/AgentPart" - }, - { - "$ref": "#/components/schemas/RetryPart" - }, - { - "$ref": "#/components/schemas/CompactionPart" - } - ] - }, - "Prompt": { - "type": "object", - "properties": { - "text": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PromptFileAttachment" - } - }, - "agents": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PromptAgentAttachment" - } - } - }, - "required": ["text"], - "additionalProperties": false - }, - "Pty": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^pty" - }, - "title": { - "type": "string" - }, - "command": { - "type": "string" - }, - "args": { - "type": "array", - "items": { - "type": "string" - } - }, - "cwd": { - "type": "string" - }, - "status": { - "type": "string", - "enum": ["running", "exited"] - }, - "pid": { - "type": "integer", - "minimum": 0 - }, - "exitCode": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["id", "title", "command", "args", "cwd", "status", "pid"], - "additionalProperties": false - }, - "SessionStatus": { - "anyOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["idle"] - } - }, - "required": ["type"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["retry"] - }, - "attempt": { - "type": "integer", - "minimum": 0 - }, - "message": { - "type": "string" - }, - "action": { - "type": "object", - "properties": { - "reason": { - "type": "string" - }, - "provider": { - "type": "string" - }, - "title": { - "type": "string" - }, - "message": { - "type": "string" - }, - "label": { - "type": "string" - }, - "link": { - "type": "string" - } - }, - "required": ["reason", "provider", "title", "message", "label"], - "additionalProperties": false - }, - "next": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["type", "attempt", "message", "next"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["busy"] - } - }, - "required": ["type"], - "additionalProperties": false - } - ] - }, - "QuestionOption": { - "type": "object", - "properties": { - "label": { - "type": "string", - "description": "Display text (1-5 words, concise)" - }, - "description": { - "type": "string", - "description": "Explanation of choice" - } - }, - "required": ["label", "description"], - "additionalProperties": false - }, - "QuestionInfo": { - "type": "object", - "properties": { - "question": { - "type": "string", - "description": "Complete question" - }, - "header": { - "type": "string", - "description": "Very short label (max 30 chars)" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionOption" - }, - "description": "Available choices" - }, - "multiple": { - "type": "boolean" - }, - "custom": { - "type": "boolean" - } - }, - "required": ["question", "header", "options"], - "additionalProperties": false - }, - "QuestionTool": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "callID": { - "type": "string" - } - }, - "required": ["messageID", "callID"], - "additionalProperties": false - }, - "QuestionAnswer": { - "type": "array", - "items": { - "type": "string" - } - }, - "GlobalEvent": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "project": { - "type": "string" - }, - "workspace": { - "type": "string" - }, - "payload": { - "anyOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["models-dev.refreshed"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["integration.updated"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["integration.connection.updated"] - }, - "properties": { - "type": "object", - "properties": { - "integrationID": { - "type": "string" - } - }, - "required": ["integrationID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["catalog.updated"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.created"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.updated"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.deleted"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["message.updated"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Message" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["message.removed"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - } - }, - "required": ["sessionID", "messageID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["message.part.updated"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "part": { - "$ref": "#/components/schemas/Part" - }, - "time": { - "type": "number" - } - }, - "required": ["sessionID", "part", "time"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["message.part.removed"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "partID": { - "type": "string", - "pattern": "^prt" - } - }, - "required": ["sessionID", "messageID", "partID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.agent.switched"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "agent": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "agent"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.model.switched"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - } - }, - "required": ["timestamp", "sessionID", "messageID", "model"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.moved"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "subdirectory": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "location"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.prompted"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "prompt": { - "$ref": "#/components/schemas/Prompt" - }, - "delivery": { - "type": "string", - "enum": ["steer", "queue"] - } - }, - "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.prompt.admitted"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "prompt": { - "$ref": "#/components/schemas/Prompt" - }, - "delivery": { - "type": "string", - "enum": ["steer", "queue"] - } - }, - "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.context.updated"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.synthetic"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.shell.started"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "command": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "callID", "command"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.shell.ended"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "callID": { - "type": "string" - }, - "output": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "callID", "output"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.step.started"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "agent": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - }, - "snapshot": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "agent", "model"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.step.ended"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "finish": { - "type": "string" - }, - "cost": { - "type": "number" - }, - "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false - }, - "snapshot": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "finish", "cost", "tokens"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.step.failed"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "error": { - "$ref": "#/components/schemas/SessionErrorUnknown" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "error"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.text.started"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.text.delta"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "delta"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.text.ended"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.reasoning.started"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "reasoningID": { - "type": "string" - }, - "providerMetadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.reasoning.delta"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "reasoningID": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "delta"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.reasoning.ended"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "reasoningID": { - "type": "string" - }, - "text": { - "type": "string" - }, - "providerMetadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.input.started"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "name"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.input.delta"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "delta"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.input.ended"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.called"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "tool": { - "type": "string" - }, - "input": { - "type": "object" - }, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "tool", "input", "provider"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.progress"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "structured": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.success"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "structured": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } - }, - "outputPaths": { - "type": "array", - "items": { - "type": "string" - } - }, - "result": {}, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false - } - }, - "required": [ - "timestamp", - "sessionID", - "assistantMessageID", - "callID", - "structured", - "content", - "provider" - ], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.failed"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "error": { - "$ref": "#/components/schemas/SessionErrorUnknown" - }, - "result": {}, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "error", "provider"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.retried"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "attempt": { - "type": "number" - }, - "error": { - "$ref": "#/components/schemas/SessionNextRetry_error" - } - }, - "required": ["timestamp", "sessionID", "attempt", "error"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.compaction.started"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "reason": { - "type": "string", - "enum": ["auto", "manual"] - } - }, - "required": ["timestamp", "sessionID", "messageID", "reason"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.compaction.delta"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.compaction.ended"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "reason": { - "type": "string", - "enum": ["auto", "manual"] - }, - "text": { - "type": "string" - }, - "recent": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "reason", "text", "recent"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.revert.staged"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "revert": { - "$ref": "#/components/schemas/RevertState" - } - }, - "required": ["timestamp", "sessionID", "revert"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.revert.cleared"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["timestamp", "sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.revert.committed"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - } - }, - "required": ["timestamp", "sessionID", "messageID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["message.part.delta"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "partID": { - "type": "string", - "pattern": "^prt" - }, - "field": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["sessionID", "messageID", "partID", "field", "delta"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.diff"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "diff": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" - } - } - }, - "required": ["sessionID", "diff"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.error"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "error": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProviderAuthError" - }, - { - "$ref": "#/components/schemas/UnknownError" - }, - { - "$ref": "#/components/schemas/MessageOutputLengthError" - }, - { - "$ref": "#/components/schemas/MessageAbortedError" - }, - { - "$ref": "#/components/schemas/StructuredOutputError" - }, - { - "$ref": "#/components/schemas/ContextOverflowError" - }, - { - "$ref": "#/components/schemas/ContentFilterError" - }, - { - "$ref": "#/components/schemas/APIError" - } - ] - } - }, - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["installation.updated"] - }, - "properties": { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "required": ["version"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["installation.update-available"] - }, - "properties": { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "required": ["version"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["file.edited"] - }, - "properties": { - "type": "object", - "properties": { - "file": { - "type": "string" - } - }, - "required": ["file"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["reference.updated"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["permission.v2.asked"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "action": { - "type": "string" - }, - "resources": { - "type": "array", - "items": { - "type": "string" - } - }, - "save": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "source": { - "$ref": "#/components/schemas/PermissionV2Source" - } - }, - "required": ["id", "sessionID", "action", "resources"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["permission.v2.replied"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^per" - }, - "reply": { - "$ref": "#/components/schemas/PermissionV2Reply" - } - }, - "required": ["sessionID", "requestID", "reply"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["plugin.added"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": ["id"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["project.directories.updated"] - }, - "properties": { - "type": "object", - "properties": { - "projectID": { - "type": "string" - } - }, - "required": ["projectID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["file.watcher.updated"] - }, - "properties": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "event": { - "type": "string", - "enum": ["add", "change", "unlink"] - } - }, - "required": ["file", "event"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["pty.created"] - }, - "properties": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": ["info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["pty.updated"] - }, - "properties": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": ["info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["pty.exited"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^pty" - }, - "exitCode": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["id", "exitCode"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["pty.deleted"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^pty" - } - }, - "required": ["id"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["question.v2.asked"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^que" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Info" - }, - "description": "Questions to ask" - }, - "tool": { - "$ref": "#/components/schemas/QuestionV2Tool" - } - }, - "required": ["id", "sessionID", "questions"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["question.v2.replied"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Answer" - } - } - }, - "required": ["sessionID", "requestID", "answers"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["question.v2.rejected"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - } - }, - "required": ["sessionID", "requestID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["lsp.updated"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["permission.asked"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "permission": { - "type": "string" - }, - "patterns": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "always": { - "type": "array", - "items": { - "type": "string" - } - }, - "tool": { - "type": "object", - "properties": { - "messageID": { - "type": "string" - }, - "callID": { - "type": "string" - } - }, - "required": ["messageID", "callID"], - "additionalProperties": false - } - }, - "required": ["id", "sessionID", "permission", "patterns", "metadata", "always"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["permission.replied"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^per" - }, - "reply": { - "type": "string", - "enum": ["once", "always", "reject"] - } - }, - "required": ["sessionID", "requestID", "reply"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["tui.prompt.append"] - }, - "properties": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["tui.command.execute"] - }, - "properties": { - "type": "object", - "properties": { - "command": { - "anyOf": [ - { - "type": "string", - "enum": [ - "session.list", - "session.new", - "session.share", - "session.interrupt", - "session.compact", - "session.page.up", - "session.page.down", - "session.line.up", - "session.line.down", - "session.half.page.up", - "session.half.page.down", - "session.first", - "session.last", - "prompt.clear", - "prompt.submit", - "agent.cycle" - ] - }, - { - "type": "string" - } - ] - } - }, - "required": ["command"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["tui.toast.show"] - }, - "properties": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "message": { - "type": "string" - }, - "variant": { - "type": "string", - "enum": ["info", "success", "warning", "error"] - }, - "duration": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["message", "variant"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["tui.session.select"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses", - "description": "Session ID to navigate to" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["mcp.tools.changed"] - }, - "properties": { - "type": "object", - "properties": { - "server": { - "type": "string" - } - }, - "required": ["server"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["mcp.browser.open.failed"] - }, - "properties": { - "type": "object", - "properties": { - "mcpName": { - "type": "string" - }, - "url": { - "type": "string" - } - }, - "required": ["mcpName", "url"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["command.executed"] - }, - "properties": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "arguments": { - "type": "string" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - } - }, - "required": ["name", "sessionID", "arguments", "messageID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["project.updated"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "worktree": { - "type": "string" - }, - "vcs": { - "$ref": "#/components/schemas/ProjectVcs" - }, - "name": { - "type": "string" - }, - "icon": { - "$ref": "#/components/schemas/ProjectIcon" - }, - "commands": { - "$ref": "#/components/schemas/ProjectCommands" - }, - "time": { - "$ref": "#/components/schemas/ProjectTime" - }, - "sandboxes": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["id", "worktree", "time", "sandboxes"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.status"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "status": { - "$ref": "#/components/schemas/SessionStatus" - } - }, - "required": ["sessionID", "status"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.idle"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["question.asked"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^que" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionInfo" - }, - "description": "Questions to ask" - }, - "tool": { - "$ref": "#/components/schemas/QuestionTool" - } - }, - "required": ["id", "sessionID", "questions"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["question.replied"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } - } - }, - "required": ["sessionID", "requestID", "answers"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["question.rejected"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - } - }, - "required": ["sessionID", "requestID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.compacted"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["vcs.branch.updated"] - }, - "properties": { - "type": "object", - "properties": { - "branch": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["workspace.ready"] - }, - "properties": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": ["name"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["workspace.failed"] - }, - "properties": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["workspace.status"] - }, - "properties": { - "type": "object", - "properties": { - "workspaceID": { - "type": "string", - "pattern": "^wrk" - }, - "status": { - "type": "string", - "enum": ["connected", "connecting", "disconnected", "error"] - } - }, - "required": ["workspaceID", "status"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["worktree.ready"] - }, - "properties": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "branch": { - "type": "string" - } - }, - "required": ["name"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["worktree.failed"] - }, - "properties": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["server.connected"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["global.disposed"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "$ref": "#/components/schemas/EventServerInstanceDisposed" - }, - { - "$ref": "#/components/schemas/SyncEventSessionCreated" - }, - { - "$ref": "#/components/schemas/SyncEventSessionUpdated" - }, - { - "$ref": "#/components/schemas/SyncEventSessionDeleted" - }, - { - "$ref": "#/components/schemas/SyncEventMessageUpdated" - }, - { - "$ref": "#/components/schemas/SyncEventMessageRemoved" - }, - { - "$ref": "#/components/schemas/SyncEventMessagePartUpdated" - }, - { - "$ref": "#/components/schemas/SyncEventMessagePartRemoved" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextAgentSwitched" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextModelSwitched" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextMoved" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextPrompted" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextPromptAdmitted" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextContextUpdated" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextSynthetic" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextShellStarted" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextShellEnded" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextStepStarted" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextStepEnded" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextStepFailed" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextTextStarted" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextTextEnded" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextReasoningStarted" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextReasoningEnded" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextToolInputStarted" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextToolInputEnded" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextToolCalled" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextToolProgress" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextToolSuccess" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextToolFailed" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextRetried" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextCompactionStarted" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextCompactionEnded" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextRevertStaged" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextRevertCleared" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextRevertCommitted" - } - ] - } - }, - "required": ["directory", "payload"], - "additionalProperties": false - }, - "LogLevel": { - "type": "string", - "enum": ["DEBUG", "INFO", "WARN", "ERROR"], - "description": "Log level" - }, - "ServerConfig": { - "type": "object", - "properties": { - "port": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "hostname": { - "type": "string" - }, - "mdns": { - "type": "boolean" - }, - "mdnsDomain": { - "type": "string" - }, - "cors": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false, - "description": "Server configuration for opencode serve and web commands" - }, - "PermissionActionConfig": { - "type": "string", - "enum": ["ask", "allow", "deny"] - }, - "PermissionObjectConfig": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/PermissionActionConfig" - } - }, - "PermissionRuleConfig": { - "anyOf": [ - { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - { - "$ref": "#/components/schemas/PermissionObjectConfig" - } - ] - }, - "PermissionConfig": { - "anyOf": [ - { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - { - "type": "object", - "properties": { - "read": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "edit": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "glob": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "grep": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "list": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "bash": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "task": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "external_directory": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "question": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "webfetch": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "websearch": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "lsp": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "doom_loop": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "skill": { - "$ref": "#/components/schemas/PermissionRuleConfig" - } - }, - "additionalProperties": { - "$ref": "#/components/schemas/PermissionRuleConfig" - } - } - ] - }, - "AgentConfig": { - "type": "object", - "properties": { - "model": { - "type": "string" - }, - "variant": { - "type": "string" - }, - "temperature": { - "type": "number" - }, - "top_p": { - "type": "number" - }, - "prompt": { - "type": "string" - }, - "tools": { - "type": "object", - "additionalProperties": { - "type": "boolean" - } - }, - "disable": { - "type": "boolean" - }, - "description": { - "type": "string" - }, - "mode": { - "type": "string", - "enum": ["subagent", "primary", "all"] - }, - "hidden": { - "type": "boolean" - }, - "options": { - "type": "object" - }, - "color": { - "anyOf": [ - { - "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$" - }, - { - "type": "string", - "enum": ["primary", "secondary", "accent", "success", "warning", "error", "info"] - } - ], - "description": "Hex color code (e.g., #FF5733) or theme color (e.g., primary)" - }, - "steps": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "maxSteps": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "permission": { - "$ref": "#/components/schemas/PermissionConfig" - } - }, - "additionalProperties": {} - }, - "ProviderConfig": { - "type": "object", - "properties": { - "api": { - "type": "string" - }, - "name": { - "type": "string" - }, - "env": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "string" - }, - "npm": { - "type": "string" - }, - "whitelist": { - "type": "array", - "items": { - "type": "string" - } - }, - "blacklist": { - "type": "array", - "items": { - "type": "string" - } - }, - "options": { - "type": "object", - "properties": { - "apiKey": { - "type": "string" - }, - "baseURL": { - "type": "string" - }, - "enterpriseUrl": { - "type": "string" - }, - "setCacheKey": { - "type": "boolean" - }, - "timeout": { - "anyOf": [ - { - "type": "integer", - "exclusiveMinimum": 0 - }, - { - "type": "boolean", - "enum": [false] - } - ], - "description": "Timeout in milliseconds for full requests to this provider. Set to false to disable timeout." - }, - "headerTimeout": { - "anyOf": [ - { - "type": "integer", - "exclusiveMinimum": 0 - }, - { - "type": "boolean", - "enum": [false] - } - ], - "description": "Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout." - }, - "chunkTimeout": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "additionalProperties": {} - }, - "models": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "family": { - "type": "string" - }, - "release_date": { - "type": "string" - }, - "attachment": { - "type": "boolean" - }, - "reasoning": { - "type": "boolean" - }, - "temperature": { - "type": "boolean" - }, - "tool_call": { - "type": "boolean" - }, - "interleaved": { - "anyOf": [ - { - "type": "boolean", - "enum": [true] - }, - { - "type": "object", - "properties": { - "field": { - "type": "string", - "enum": ["reasoning", "reasoning_content", "reasoning_details"] - } - }, - "required": ["field"], - "additionalProperties": false - } - ] - }, - "cost": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "cache_read": { - "type": "number" - }, - "cache_write": { - "type": "number" - }, - "context_over_200k": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "cache_read": { - "type": "number" - }, - "cache_write": { - "type": "number" - } - }, - "required": ["input", "output"], - "additionalProperties": false - } - }, - "required": ["input", "output"], - "additionalProperties": false - }, - "limit": { - "type": "object", - "properties": { - "context": { - "type": "number" - }, - "input": { - "type": "number" - }, - "output": { - "type": "number" - } - }, - "required": ["context", "output"], - "additionalProperties": false - }, - "modalities": { - "type": "object", - "properties": { - "input": { - "type": "array", - "items": { - "type": "string", - "enum": ["text", "audio", "image", "video", "pdf"] - } - }, - "output": { - "type": "array", - "items": { - "type": "string", - "enum": ["text", "audio", "image", "video", "pdf"] - } - } - }, - "additionalProperties": false - }, - "experimental": { - "type": "boolean" - }, - "status": { - "type": "string", - "enum": ["alpha", "beta", "deprecated", "active"] - }, - "provider": { - "type": "object", - "properties": { - "npm": { - "type": "string" - }, - "api": { - "type": "string" - } - }, - "additionalProperties": false - }, - "options": { - "type": "object" - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "variants": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "disabled": { - "type": "boolean" - } - }, - "additionalProperties": {} - }, - "description": "Variant-specific configuration" - } - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - }, - "McpLocalConfig": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["local"], - "description": "Type of MCP server connection" - }, - "command": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Command and arguments to run the MCP server" - }, - "cwd": { - "type": "string" - }, - "environment": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "enabled": { - "type": "boolean" - }, - "timeout": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["type", "command"], - "additionalProperties": false - }, - "McpOAuthConfig": { - "type": "object", - "properties": { - "clientId": { - "type": "string" - }, - "clientSecret": { - "type": "string" - }, - "scope": { - "type": "string" - }, - "callbackPort": { - "type": "integer", - "minimum": 1, - "maximum": 65535 - }, - "redirectUri": { - "type": "string" - } - }, - "additionalProperties": false - }, - "McpRemoteConfig": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["remote"], - "description": "Type of MCP server connection" - }, - "url": { - "type": "string", - "description": "URL of the remote MCP server" - }, - "enabled": { - "type": "boolean" - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "oauth": { - "anyOf": [ - { - "$ref": "#/components/schemas/McpOAuthConfig" - }, - { - "type": "boolean", - "enum": [false] - } - ], - "description": "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection." - }, - "timeout": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["type", "url"], - "additionalProperties": false - }, - "LayoutConfig": { - "type": "string", - "enum": ["auto", "stretch"], - "description": "@deprecated Always uses stretch layout." - }, - "ImageAttachmentConfig": { - "type": "object", - "properties": { - "auto_resize": { - "type": "boolean" - }, - "max_width": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "max_height": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "max_base64_bytes": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "additionalProperties": false - }, - "AttachmentConfig": { - "type": "object", - "properties": { - "image": { - "$ref": "#/components/schemas/ImageAttachmentConfig" - } - }, - "additionalProperties": false - }, - "Config": { - "type": "object", - "properties": { - "$schema": { - "type": "string" - }, - "shell": { - "type": "string" - }, - "logLevel": { - "$ref": "#/components/schemas/LogLevel" - }, - "server": { - "$ref": "#/components/schemas/ServerConfig" - }, - "command": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "template": { - "type": "string" - }, - "description": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "string" - }, - "variant": { - "type": "string" - }, - "subtask": { - "type": "boolean" - } - }, - "required": ["template"], - "additionalProperties": false - } - }, - "skills": { - "type": "object", - "properties": { - "paths": { - "type": "array", - "items": { - "type": "string" - } - }, - "urls": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - }, - "references": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "type": "string" - }, - { - "$ref": "#/components/schemas/ConfigV2ReferenceGit" - }, - { - "$ref": "#/components/schemas/ConfigV2ReferenceLocal" - } - ] - } - }, - "reference": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "type": "string" - }, - { - "$ref": "#/components/schemas/ConfigV2ReferenceGit" - }, - { - "$ref": "#/components/schemas/ConfigV2ReferenceLocal" - } - ] - } - }, - "watcher": { - "type": "object", - "properties": { - "ignore": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - }, - "snapshot": { - "type": "boolean" - }, - "plugin": { - "type": "array", - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "array", - "prefixItems": [ - { - "type": "string" - }, - { - "type": "object" - } - ], - "maxItems": 2, - "minItems": 2 - } - ] - } - }, - "share": { - "type": "string", - "enum": ["manual", "auto", "disabled"] - }, - "autoshare": { - "type": "boolean" - }, - "autoupdate": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "string", - "enum": ["notify"] - } - ], - "description": "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications" - }, - "disabled_providers": { - "type": "array", - "items": { - "type": "string" - } - }, - "enabled_providers": { - "type": "array", - "items": { - "type": "string" - } - }, - "model": { - "type": "string" - }, - "small_model": { - "type": "string" - }, - "default_agent": { - "type": "string" - }, - "subagent_depth": { - "type": "integer", - "minimum": 0 - }, - "username": { - "type": "string" - }, - "mode": { - "type": "object", - "properties": { - "build": { - "$ref": "#/components/schemas/AgentConfig" - }, - "plan": { - "$ref": "#/components/schemas/AgentConfig" - } - }, - "additionalProperties": { - "$ref": "#/components/schemas/AgentConfig" - } - }, - "agent": { - "type": "object", - "properties": { - "plan": { - "$ref": "#/components/schemas/AgentConfig" - }, - "build": { - "$ref": "#/components/schemas/AgentConfig" - }, - "general": { - "$ref": "#/components/schemas/AgentConfig" - }, - "explore": { - "$ref": "#/components/schemas/AgentConfig" - }, - "title": { - "$ref": "#/components/schemas/AgentConfig" - }, - "summary": { - "$ref": "#/components/schemas/AgentConfig" - }, - "compaction": { - "$ref": "#/components/schemas/AgentConfig" - } - }, - "additionalProperties": { - "$ref": "#/components/schemas/AgentConfig" - } - }, - "provider": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/ProviderConfig" - } - }, - "mcp": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "$ref": "#/components/schemas/McpLocalConfig" - }, - { - "$ref": "#/components/schemas/McpRemoteConfig" - }, - { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - } - }, - "required": ["enabled"], - "additionalProperties": false - } - ] - } - }, - "formatter": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "disabled": { - "type": "boolean" - }, - "command": { - "type": "array", - "items": { - "type": "string" - } - }, - "environment": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "extensions": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - } - } - ], - "description": "Enable or configure formatters. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides." - }, - "lsp": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "type": "object", - "properties": { - "disabled": { - "type": "boolean", - "enum": [true] - } - }, - "required": ["disabled"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "command": { - "type": "array", - "items": { - "type": "string" - } - }, - "extensions": { - "type": "array", - "items": { - "type": "string" - } - }, - "disabled": { - "type": "boolean" - }, - "env": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "initialization": { - "type": "object" - } - }, - "required": ["command"], - "additionalProperties": false - } - ] - } - } - ], - "description": "Enable or configure LSP servers. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides." - }, - "instructions": { - "type": "array", - "items": { - "type": "string" - } - }, - "layout": { - "$ref": "#/components/schemas/LayoutConfig" - }, - "permission": { - "$ref": "#/components/schemas/PermissionConfig" - }, - "tools": { - "type": "object", - "additionalProperties": { - "type": "boolean" - } - }, - "attachment": { - "$ref": "#/components/schemas/AttachmentConfig" - }, - "enterprise": { - "type": "object", - "properties": { - "url": { - "type": "string" - } - }, - "additionalProperties": false - }, - "tool_output": { - "type": "object", - "properties": { - "max_lines": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "max_bytes": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "additionalProperties": false - }, - "compaction": { - "type": "object", - "properties": { - "auto": { - "type": "boolean" - }, - "prune": { - "type": "boolean" - }, - "tail_turns": { - "type": "integer", - "minimum": 0 - }, - "preserve_recent_tokens": { - "type": "integer", - "minimum": 0 - }, - "reserved": { - "type": "integer", - "minimum": 0 - } - }, - "additionalProperties": false - }, - "experimental": { - "type": "object", - "properties": { - "disable_paste_summary": { - "type": "boolean" - }, - "batch_tool": { - "type": "boolean" - }, - "openTelemetry": { - "type": "boolean" - }, - "primary_tools": { - "type": "array", - "items": { - "type": "string" - } - }, - "continue_loop_on_deny": { - "type": "boolean" - }, - "mcp_timeout": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "policies": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ConfigV2ExperimentalPolicy" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "Model": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "api": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "url": { - "type": "string" - }, - "npm": { - "type": "string" - } - }, - "required": ["id", "url", "npm"], - "additionalProperties": false - }, - "name": { - "type": "string" - }, - "family": { - "type": "string" - }, - "capabilities": { - "type": "object", - "properties": { - "temperature": { - "type": "boolean" - }, - "reasoning": { - "type": "boolean" - }, - "attachment": { - "type": "boolean" - }, - "toolcall": { - "type": "boolean" - }, - "input": { - "type": "object", - "properties": { - "text": { - "type": "boolean" - }, - "audio": { - "type": "boolean" - }, - "image": { - "type": "boolean" - }, - "video": { - "type": "boolean" - }, - "pdf": { - "type": "boolean" - } - }, - "required": ["text", "audio", "image", "video", "pdf"], - "additionalProperties": false - }, - "output": { - "type": "object", - "properties": { - "text": { - "type": "boolean" - }, - "audio": { - "type": "boolean" - }, - "image": { - "type": "boolean" - }, - "video": { - "type": "boolean" - }, - "pdf": { - "type": "boolean" - } - }, - "required": ["text", "audio", "image", "video", "pdf"], - "additionalProperties": false - }, - "interleaved": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "object", - "properties": { - "field": { - "type": "string", - "enum": ["reasoning", "reasoning_content", "reasoning_details"] - } - }, - "required": ["field"], - "additionalProperties": false - } - ] - } - }, - "required": ["temperature", "reasoning", "attachment", "toolcall", "input", "output", "interleaved"], - "additionalProperties": false - }, - "cost": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - }, - "tiers": { - "type": "array", - "items": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - }, - "tier": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["context"] - }, - "size": { - "type": "number" - } - }, - "required": ["type", "size"], - "additionalProperties": false - } - }, - "required": ["input", "output", "cache", "tier"], - "additionalProperties": false - } - }, - "experimentalOver200K": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "cache"], - "additionalProperties": false - } - }, - "required": ["input", "output", "cache"], - "additionalProperties": false - }, - "limit": { - "type": "object", - "properties": { - "context": { - "type": "number" - }, - "input": { - "type": "number" - }, - "output": { - "type": "number" - } - }, - "required": ["context", "output"], - "additionalProperties": false - }, - "status": { - "type": "string", - "enum": ["alpha", "beta", "deprecated", "active"] - }, - "options": { - "type": "object" - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "release_date": { - "type": "string" - }, - "variants": { - "type": "object", - "additionalProperties": { - "type": "object" - } - } - }, - "required": [ - "id", - "providerID", - "api", - "name", - "capabilities", - "cost", - "limit", - "status", - "options", - "headers", - "release_date" - ], - "additionalProperties": false - }, - "Provider": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "source": { - "type": "string", - "enum": ["env", "config", "custom", "api"] - }, - "env": { - "type": "array", - "items": { - "type": "string" - } - }, - "key": { - "type": "string" - }, - "options": { - "type": "object" - }, - "models": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/Model" - } - } - }, - "required": ["id", "name", "source", "env", "options", "models"], - "additionalProperties": false - }, - "ExperimentalCapabilities": { - "type": "object", - "properties": { - "backgroundSubagents": { - "type": "boolean" - } - }, - "required": ["backgroundSubagents"], - "additionalProperties": false - }, - "ConsoleState": { - "type": "object", - "properties": { - "consoleManagedProviders": { - "type": "array", - "items": { - "type": "string" - } - }, - "activeOrgName": { - "type": "string" - }, - "switchableOrgCount": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["consoleManagedProviders", "switchableOrgCount"], - "additionalProperties": false - }, - "effect_HttpApiError_InternalServerError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["InternalServerError"] - } - }, - "required": ["_tag"], - "additionalProperties": false - }, - "ToolListItem": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "description": { - "type": "string" - }, - "parameters": {} - }, - "required": ["id", "description", "parameters"], - "additionalProperties": false - }, - "ToolList": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ToolListItem" - } - }, - "ToolIDs": { - "type": "array", - "items": { - "type": "string" - } - }, - "WorktreeError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": [ - "WorktreeNotGitError", - "WorktreeNameGenerationFailedError", - "WorktreeCreateFailedError", - "WorktreeStartCommandFailedError", - "WorktreeRemoveFailedError", - "WorktreeResetFailedError", - "WorktreeListFailedError" - ] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "WorktreeCreateInput": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "startCommand": { - "type": "string", - "description": "Additional startup script to run after the project's start command" - } - }, - "additionalProperties": false - }, - "Worktree": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "branch": { - "type": "string" - }, - "directory": { - "type": "string" - } - }, - "required": ["name", "directory"], - "additionalProperties": false - }, - "WorktreeRemoveInput": { - "type": "object", - "properties": { - "directory": { - "type": "string" - } - }, - "required": ["directory"], - "additionalProperties": false - }, - "WorktreeResetInput": { - "type": "object", - "properties": { - "directory": { - "type": "string" - } - }, - "required": ["directory"], - "additionalProperties": false - }, - "ProjectSummary": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "worktree": { - "type": "string" - } - }, - "required": ["id", "worktree"], - "additionalProperties": false - }, - "GlobalSession": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^ses" - }, - "slug": { - "type": "string" - }, - "projectID": { - "type": "string" - }, - "workspaceID": { - "type": "string", - "pattern": "^wrk" - }, - "directory": { - "type": "string" - }, - "path": { - "type": "string" - }, - "parentID": { - "type": "string", - "pattern": "^ses" - }, - "summary": { - "type": "object", - "properties": { - "additions": { - "type": "number" - }, - "deletions": { - "type": "number" - }, - "files": { - "type": "number" - }, - "diffs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" - } - } - }, - "required": ["additions", "deletions", "files"], - "additionalProperties": false - }, - "cost": { - "type": "number" - }, - "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false - }, - "share": { - "type": "object", - "properties": { - "url": { - "type": "string" - } - }, - "required": ["url"], - "additionalProperties": false - }, - "title": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "variant": { - "type": "string" - } - }, - "required": ["id", "providerID"], - "additionalProperties": false - }, - "version": { - "type": "string" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "integer", - "minimum": 0 - }, - "updated": { - "type": "integer", - "minimum": 0 - }, - "compacting": { - "type": "integer", - "minimum": 0 - }, - "archived": { - "type": "number" - } - }, - "required": ["created", "updated"], - "additionalProperties": false - }, - "permission": { - "$ref": "#/components/schemas/PermissionRuleset" - }, - "revert": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "partID": { - "type": "string", - "pattern": "^prt" - }, - "snapshot": { - "type": "string" - }, - "diff": { - "type": "string" - } - }, - "required": ["messageID"], - "additionalProperties": false - }, - "project": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProjectSummary" - }, - { - "type": "null" - } - ] - } - }, - "required": ["id", "slug", "projectID", "directory", "title", "version", "time", "project"], - "additionalProperties": false - }, - "McpResource": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "uri": { - "type": "string" - }, - "description": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "client": { - "type": "string" - } - }, - "required": ["name", "uri", "client"], - "additionalProperties": false - }, - "Symbol": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "kind": { - "type": "integer", - "minimum": 0 - }, - "location": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "range": { - "$ref": "#/components/schemas/Range" - } - }, - "required": ["uri", "range"], - "additionalProperties": false - } - }, - "required": ["name", "kind", "location"], - "additionalProperties": false - }, - "FileNode": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "absolute": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["file", "directory"] - }, - "ignored": { - "type": "boolean" - } - }, - "required": ["name", "path", "absolute", "type", "ignored"], - "additionalProperties": false - }, - "FileContent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["text", "binary"] - }, - "content": { - "type": "string" - }, - "diff": { - "type": "string" - }, - "patch": { - "type": "object", - "properties": { - "oldFileName": { - "type": "string" - }, - "newFileName": { - "type": "string" - }, - "oldHeader": { - "type": "string" - }, - "newHeader": { - "type": "string" - }, - "hunks": { - "type": "array", - "items": { - "type": "object", - "properties": { - "oldStart": { - "type": "integer", - "minimum": 0 - }, - "oldLines": { - "type": "integer", - "minimum": 0 - }, - "newStart": { - "type": "integer", - "minimum": 0 - }, - "newLines": { - "type": "integer", - "minimum": 0 - }, - "lines": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["oldStart", "oldLines", "newStart", "newLines", "lines"], - "additionalProperties": false - } - }, - "index": { - "type": "string" - } - }, - "required": ["oldFileName", "newFileName", "hunks"], - "additionalProperties": false - }, - "encoding": { - "type": "string", - "enum": ["base64"] - }, - "mimeType": { - "type": "string" - } - }, - "required": ["type", "content"], - "additionalProperties": false - }, - "File": { - "type": "object", - "properties": { - "path": { - "type": "string" - }, - "added": { - "type": "integer", - "minimum": 0 - }, - "removed": { - "type": "integer", - "minimum": 0 - }, - "status": { - "type": "string", - "enum": ["added", "deleted", "modified"] - } - }, - "required": ["path", "added", "removed", "status"], - "additionalProperties": false - }, - "Path": { - "type": "object", - "properties": { - "home": { - "type": "string" - }, - "state": { - "type": "string" - }, - "config": { - "type": "string" - }, - "worktree": { - "type": "string" - }, - "directory": { - "type": "string" - } - }, - "required": ["home", "state", "config", "worktree", "directory"], - "additionalProperties": false - }, - "VcsInfo": { - "type": "object", - "properties": { - "branch": { - "type": "string" - }, - "default_branch": { - "type": "string" - } - }, - "additionalProperties": false - }, - "VcsFileStatus": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "additions": { - "type": "number" - }, - "deletions": { - "type": "number" - }, - "status": { - "type": "string", - "enum": ["added", "deleted", "modified"] - } - }, - "required": ["file", "additions", "deletions", "status"], - "additionalProperties": false - }, - "VcsFileDiff": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "patch": { - "type": "string" - }, - "additions": { - "type": "number" - }, - "deletions": { - "type": "number" - }, - "status": { - "type": "string", - "enum": ["added", "deleted", "modified"] - } - }, - "required": ["file", "additions", "deletions"], - "additionalProperties": false - }, - "VcsApplyError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["VcsApplyError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "reason": { - "type": "string", - "enum": ["non-git", "not-clean"] - } - }, - "required": ["message", "reason"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "Command": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "string" - }, - "source": { - "type": "string", - "enum": ["command", "mcp", "skill"] - }, - "template": { - "type": "string" - }, - "subtask": { - "type": "boolean" - }, - "hints": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["name", "template", "hints"], - "additionalProperties": false - }, - "Agent": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "mode": { - "type": "string", - "enum": ["subagent", "primary", "all"] - }, - "native": { - "type": "boolean" - }, - "hidden": { - "type": "boolean" - }, - "topP": { - "type": "number" - }, - "temperature": { - "type": "number" - }, - "color": { - "type": "string" - }, - "permission": { - "$ref": "#/components/schemas/PermissionRuleset" - }, - "model": { - "type": "object", - "properties": { - "modelID": { - "type": "string" - }, - "providerID": { - "type": "string" - } - }, - "required": ["modelID", "providerID"], - "additionalProperties": false - }, - "variant": { - "type": "string" - }, - "prompt": { - "type": "string" - }, - "options": { - "type": "object" - }, - "steps": { - "type": "number" - } - }, - "required": ["name", "mode", "permission", "options"], - "additionalProperties": false - }, - "LSPStatus": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "root": { - "type": "string" - }, - "status": { - "type": "string", - "enum": ["connected", "error"] - } - }, - "required": ["id", "name", "root", "status"], - "additionalProperties": false - }, - "FormatterStatus": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "extensions": { - "type": "array", - "items": { - "type": "string" - } - }, - "enabled": { - "type": "boolean" - } - }, - "required": ["name", "extensions", "enabled"], - "additionalProperties": false - }, - "MCPStatusConnected": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["connected"] - } - }, - "required": ["status"], - "additionalProperties": false - }, - "MCPStatusDisabled": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["disabled"] - } - }, - "required": ["status"], - "additionalProperties": false - }, - "MCPStatusFailed": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["failed"] - }, - "error": { - "type": "string" - } - }, - "required": ["status", "error"], - "additionalProperties": false - }, - "MCPStatusNeedsAuth": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["needs_auth"] - } - }, - "required": ["status"], - "additionalProperties": false - }, - "MCPStatusNeedsClientRegistration": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["needs_client_registration"] - }, - "error": { - "type": "string" - } - }, - "required": ["status", "error"], - "additionalProperties": false - }, - "MCPStatus": { - "anyOf": [ - { - "$ref": "#/components/schemas/MCPStatusConnected" - }, - { - "$ref": "#/components/schemas/MCPStatusDisabled" - }, - { - "$ref": "#/components/schemas/MCPStatusFailed" - }, - { - "$ref": "#/components/schemas/MCPStatusNeedsAuth" - }, - { - "$ref": "#/components/schemas/MCPStatusNeedsClientRegistration" - } - ] - }, - "McpUnsupportedOAuthError": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - }, - "required": ["error"], - "additionalProperties": false - }, - "McpServerNotFoundError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["McpServerNotFoundError"] - }, - "name": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "name", "message"], - "additionalProperties": false - }, - "Project": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "worktree": { - "type": "string" - }, - "vcs": { - "$ref": "#/components/schemas/ProjectVcs" - }, - "name": { - "type": "string" - }, - "icon": { - "$ref": "#/components/schemas/ProjectIcon" - }, - "commands": { - "$ref": "#/components/schemas/ProjectCommands" - }, - "time": { - "$ref": "#/components/schemas/ProjectTime" - }, - "sandboxes": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["id", "worktree", "time", "sandboxes"], - "additionalProperties": false - }, - "ProjectNotFoundError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["ProjectNotFoundError"] - }, - "projectID": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "projectID", "message"], - "additionalProperties": false - }, - "PtyNotFoundError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["PtyNotFoundError"] - }, - "ptyID": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "ptyID", "message"], - "additionalProperties": false - }, - "PtyForbiddenError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["PtyForbiddenError"] - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "message"], - "additionalProperties": false - }, - "QuestionRequest": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^que" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionInfo" - }, - "description": "Questions to ask" - }, - "tool": { - "$ref": "#/components/schemas/QuestionTool" - } - }, - "required": ["id", "sessionID", "questions"], - "additionalProperties": false - }, - "QuestionNotFoundError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["QuestionNotFoundError"] - }, - "requestID": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "requestID", "message"], - "additionalProperties": false - }, - "PermissionRequest": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "permission": { - "type": "string" - }, - "patterns": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "always": { - "type": "array", - "items": { - "type": "string" - } - }, - "tool": { - "type": "object", - "properties": { - "messageID": { - "type": "string" - }, - "callID": { - "type": "string" - } - }, - "required": ["messageID", "callID"], - "additionalProperties": false - } - }, - "required": ["id", "sessionID", "permission", "patterns", "metadata", "always"], - "additionalProperties": false - }, - "PermissionNotFoundError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["PermissionNotFoundError"] - }, - "requestID": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "requestID", "message"], - "additionalProperties": false - }, - "ProviderAuthMethod": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["oauth", "api"] - }, - "label": { - "type": "string" - }, - "prompts": { - "type": "array", - "items": { - "anyOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["text"] - }, - "key": { - "type": "string" - }, - "message": { - "type": "string" - }, - "placeholder": { - "type": "string" - }, - "when": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "op": { - "type": "string", - "enum": ["eq", "neq"] - }, - "value": { - "type": "string" - } - }, - "required": ["key", "op", "value"], - "additionalProperties": false - } - }, - "required": ["type", "key", "message"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["select"] - }, - "key": { - "type": "string" - }, - "message": { - "type": "string" - }, - "options": { - "type": "array", - "items": { - "type": "object", - "properties": { - "label": { - "type": "string" - }, - "value": { - "type": "string" - }, - "hint": { - "type": "string" - } - }, - "required": ["label", "value"], - "additionalProperties": false - } - }, - "when": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "op": { - "type": "string", - "enum": ["eq", "neq"] - }, - "value": { - "type": "string" - } - }, - "required": ["key", "op", "value"], - "additionalProperties": false - } - }, - "required": ["type", "key", "message", "options"], - "additionalProperties": false - } - ] - } - } - }, - "required": ["type", "label"], - "additionalProperties": false - }, - "ProviderAuthAuthorization": { - "type": "object", - "properties": { - "url": { - "type": "string" - }, - "method": { - "type": "string", - "enum": ["auto", "code"] - }, - "instructions": { - "type": "string" - } - }, - "required": ["url", "method", "instructions"], - "additionalProperties": false - }, - "ProviderAuthError1": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": [ - "BadRequest", - "ProviderAuthOauthMissing", - "ProviderAuthOauthCodeMissing", - "ProviderAuthOauthCallbackFailed", - "ProviderAuthValidationFailed" - ] - }, - "data": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "field": { - "type": "string" - }, - "message": { - "type": "string" - }, - "kind": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "NotFoundError": { - "type": "object", - "required": ["name", "data"], - "properties": { - "name": { - "type": "string", - "enum": ["NotFoundError"] - }, - "data": { - "type": "object", - "required": ["message"], - "properties": { - "message": { - "type": "string" - } - } - } - } - }, - "TextPartInput": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "type": { - "type": "string", - "enum": ["text"] - }, - "text": { - "type": "string" - }, - "session.synthetic": { - "type": "boolean" - }, - "ignored": { - "type": "boolean" - }, - "time": { - "type": "object", - "properties": { - "start": { - "type": "integer", - "minimum": 0 - }, - "end": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["start"], - "additionalProperties": false - }, - "metadata": { - "type": "object" - } - }, - "required": ["type", "text"], - "additionalProperties": false - }, - "FilePartInput": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "type": { - "type": "string", - "enum": ["file"] - }, - "mime": { - "type": "string" - }, - "filename": { - "type": "string" - }, - "url": { - "type": "string" - }, - "source": { - "$ref": "#/components/schemas/FilePartSource" - } - }, - "required": ["type", "mime", "url"], - "additionalProperties": false - }, - "AgentPartInput": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "type": { - "type": "string", - "enum": ["agent"] - }, - "name": { - "type": "string" - }, - "source": { - "type": "object", - "properties": { - "value": { - "type": "string" - }, - "start": { - "type": "integer", - "minimum": 0 - }, - "end": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["value", "start", "end"], - "additionalProperties": false - } - }, - "required": ["type", "name"], - "additionalProperties": false - }, - "SubtaskPartInput": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "type": { - "type": "string", - "enum": ["subtask"] - }, - "prompt": { - "type": "string" - }, - "description": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - } - }, - "required": ["providerID", "modelID"], - "additionalProperties": false - }, - "command": { - "type": "string" - } - }, - "required": ["type", "prompt", "description", "agent"], - "additionalProperties": false - }, - "SessionBusyError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["SessionBusyError"] - }, - "sessionID": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "sessionID", "message"], - "additionalProperties": false - }, - "EventTuiPromptAppend": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["tui.prompt.append"] - }, - "properties": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"], - "additionalProperties": false - } - }, - "required": ["type", "properties"], - "additionalProperties": false - }, - "EventTuiCommandExecute": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["tui.command.execute"] - }, - "properties": { - "type": "object", - "properties": { - "command": { - "anyOf": [ - { - "type": "string", - "enum": [ - "session.list", - "session.new", - "session.share", - "session.interrupt", - "session.compact", - "session.page.up", - "session.page.down", - "session.line.up", - "session.line.down", - "session.half.page.up", - "session.half.page.down", - "session.first", - "session.last", - "prompt.clear", - "prompt.submit", - "agent.cycle" - ] - }, - { - "type": "string" - } - ] - } - }, - "required": ["command"], - "additionalProperties": false - } - }, - "required": ["type", "properties"], - "additionalProperties": false - }, - "EventTuiToastShow": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["tui.toast.show"] - }, - "properties": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "message": { - "type": "string" - }, - "variant": { - "type": "string", - "enum": ["info", "success", "warning", "error"] - }, - "duration": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["message", "variant"], - "additionalProperties": false - } - }, - "required": ["type", "properties"], - "additionalProperties": false - }, - "EventTuiSessionSelect": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["tui.session.select"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses", - "description": "Session ID to navigate to" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["type", "properties"], - "additionalProperties": false - }, - "Workspace": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^wrk" - }, - "type": { - "type": "string" - }, - "name": { - "type": "string" - }, - "branch": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "extra": { - "anyOf": [ - {}, - { - "type": "null" - } - ] - }, - "projectID": { - "type": "string" - }, - "timeUsed": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - } - }, - "required": ["id", "type", "name", "projectID", "timeUsed"], - "additionalProperties": false - }, - "WorkspaceCreateError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["WorkspaceCreateError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "WorkspaceWarpError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["WorkspaceWarpError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "UnauthorizedError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["UnauthorizedError"] - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "message"], - "additionalProperties": false - }, - "SessionsResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SessionV2Info" - } - }, - "cursor": { - "type": "object", - "properties": { - "previous": { - "type": "string" - }, - "next": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "required": ["data", "cursor"], - "additionalProperties": false - }, - "InvalidCursorError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["InvalidCursorError"] - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "message"], - "additionalProperties": false - }, - "SessionActive": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["running"] - } - }, - "required": ["type"], - "additionalProperties": false - }, - "SessionNotFoundError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["SessionNotFoundError"] - }, - "sessionID": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "sessionID", "message"], - "additionalProperties": false - }, - "PromptInput": { - "type": "object", - "properties": { - "text": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PromptInputFileAttachment" - } - }, - "agents": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PromptAgentAttachment" - } - } - }, - "required": ["text"], - "additionalProperties": false - }, - "ConflictError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["ConflictError"] - }, - "message": { - "type": "string" - }, - "resource": { - "type": "string" - } - }, - "required": ["_tag", "message"], - "additionalProperties": false - }, - "ServiceUnavailableError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["ServiceUnavailableError"] - }, - "message": { - "type": "string" - }, - "service": { - "type": "string" - } - }, - "required": ["_tag", "message"], - "additionalProperties": false - }, - "MessageNotFoundError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["MessageNotFoundError"] - }, - "sessionID": { - "type": "string" - }, - "messageID": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "sessionID", "messageID", "message"], - "additionalProperties": false - }, - "UnknownError1": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["UnknownError"] - }, - "message": { - "type": "string" - }, - "ref": { - "type": "string" - } - }, - "required": ["_tag", "message"], - "additionalProperties": false - }, - "SessionDurableEvent": { - "oneOf": [ - { - "$ref": "#/components/schemas/SessionNextAgentSwitched" - }, - { - "$ref": "#/components/schemas/SessionNextModelSwitched" - }, - { - "$ref": "#/components/schemas/SessionNextMoved" - }, - { - "$ref": "#/components/schemas/SessionNextPrompted" - }, - { - "$ref": "#/components/schemas/SessionNextPromptAdmitted" - }, - { - "$ref": "#/components/schemas/SessionNextContextUpdated" - }, - { - "$ref": "#/components/schemas/SessionNextSynthetic" - }, - { - "$ref": "#/components/schemas/SessionNextShellStarted" - }, - { - "$ref": "#/components/schemas/SessionNextShellEnded" - }, - { - "$ref": "#/components/schemas/SessionNextStepStarted" - }, - { - "$ref": "#/components/schemas/SessionNextStepEnded" - }, - { - "$ref": "#/components/schemas/SessionNextStepFailed" - }, - { - "$ref": "#/components/schemas/SessionNextTextStarted" - }, - { - "$ref": "#/components/schemas/SessionNextTextEnded" - }, - { - "$ref": "#/components/schemas/SessionNextToolInputStarted" - }, - { - "$ref": "#/components/schemas/SessionNextToolInputEnded" - }, - { - "$ref": "#/components/schemas/SessionNextToolCalled" - }, - { - "$ref": "#/components/schemas/SessionNextToolProgress" - }, - { - "$ref": "#/components/schemas/SessionNextToolSuccess" - }, - { - "$ref": "#/components/schemas/SessionNextToolFailed" - }, - { - "$ref": "#/components/schemas/SessionNextReasoningStarted" - }, - { - "$ref": "#/components/schemas/SessionNextReasoningEnded" - }, - { - "$ref": "#/components/schemas/SessionNextRetried" - }, - { - "$ref": "#/components/schemas/SessionNextCompactionStarted" - }, - { - "$ref": "#/components/schemas/SessionNextCompactionEnded" - }, - { - "$ref": "#/components/schemas/SessionNextRevertStaged" - }, - { - "$ref": "#/components/schemas/SessionNextRevertCleared" - }, - { - "$ref": "#/components/schemas/SessionNextRevertCommitted" - } - ] - }, - "SessionHistory": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SessionDurableEvent" - } - }, - "hasMore": { - "type": "boolean" - } - }, - "required": ["data", "hasMore"], - "additionalProperties": false - }, - "SessionDurableEventStream": { - "type": "string", - "contentSchema": { - "$ref": "#/components/schemas/SessionDurableEvent" - }, - "contentMediaType": "application/json" - }, - "SessionMessagesResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SessionMessage" - } - }, - "cursor": { - "type": "object", - "properties": { - "previous": { - "type": "string" - }, - "next": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "required": ["data", "cursor"], - "additionalProperties": false - }, - "ProviderNotFoundError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["ProviderNotFoundError"] - }, - "providerID": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "providerID", "message"], - "additionalProperties": false - }, - "OutputFormat1": { - "anyOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["text"] - } - }, - "required": ["type"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["json_schema"] - }, - "schema": { - "$ref": "#/components/schemas/JSONSchema" - }, - "retryCount": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["type", "schema"], - "additionalProperties": false - } - ] - }, - "session.status": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.status"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "status": { - "$ref": "#/components/schemas/SessionStatus" - } - }, - "required": ["sessionID", "status"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "question.replied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["question.replied"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } - } - }, - "required": ["sessionID", "requestID", "answers"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "question.rejected": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["question.rejected"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - } - }, - "required": ["sessionID", "requestID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "V2Event": { - "anyOf": [ - { - "$ref": "#/components/schemas/Models-devRefreshed" - }, - { - "$ref": "#/components/schemas/IntegrationUpdated" - }, - { - "$ref": "#/components/schemas/IntegrationConnectionUpdated" - }, - { - "$ref": "#/components/schemas/CatalogUpdated" - }, - { - "$ref": "#/components/schemas/SessionCreated" - }, - { - "$ref": "#/components/schemas/SessionUpdated" - }, - { - "$ref": "#/components/schemas/SessionDeleted" - }, - { - "$ref": "#/components/schemas/MessageUpdated" - }, - { - "$ref": "#/components/schemas/MessageRemoved" - }, - { - "$ref": "#/components/schemas/MessagePartUpdated" - }, - { - "$ref": "#/components/schemas/MessagePartRemoved" - }, - { - "$ref": "#/components/schemas/SessionNextAgentSwitched" - }, - { - "$ref": "#/components/schemas/SessionNextModelSwitched" - }, - { - "$ref": "#/components/schemas/SessionNextMoved" - }, - { - "$ref": "#/components/schemas/SessionNextPrompted" - }, - { - "$ref": "#/components/schemas/SessionNextPromptAdmitted" - }, - { - "$ref": "#/components/schemas/SessionNextContextUpdated" - }, - { - "$ref": "#/components/schemas/SessionNextSynthetic" - }, - { - "$ref": "#/components/schemas/SessionNextShellStarted" - }, - { - "$ref": "#/components/schemas/SessionNextShellEnded" - }, - { - "$ref": "#/components/schemas/SessionNextStepStarted" - }, - { - "$ref": "#/components/schemas/SessionNextStepEnded" - }, - { - "$ref": "#/components/schemas/SessionNextStepFailed" - }, - { - "$ref": "#/components/schemas/SessionNextTextStarted" - }, - { - "$ref": "#/components/schemas/SessionNextTextDelta" - }, - { - "$ref": "#/components/schemas/SessionNextTextEnded" - }, - { - "$ref": "#/components/schemas/SessionNextReasoningStarted" - }, - { - "$ref": "#/components/schemas/SessionNextReasoningDelta" - }, - { - "$ref": "#/components/schemas/SessionNextReasoningEnded" - }, - { - "$ref": "#/components/schemas/SessionNextToolInputStarted" - }, - { - "$ref": "#/components/schemas/SessionNextToolInputDelta" - }, - { - "$ref": "#/components/schemas/SessionNextToolInputEnded" - }, - { - "$ref": "#/components/schemas/SessionNextToolCalled" - }, - { - "$ref": "#/components/schemas/SessionNextToolProgress" - }, - { - "$ref": "#/components/schemas/SessionNextToolSuccess" - }, - { - "$ref": "#/components/schemas/SessionNextToolFailed" - }, - { - "$ref": "#/components/schemas/SessionNextRetried" - }, - { - "$ref": "#/components/schemas/SessionNextCompactionStarted" - }, - { - "$ref": "#/components/schemas/SessionNextCompactionDelta" - }, - { - "$ref": "#/components/schemas/SessionNextCompactionEnded" - }, - { - "$ref": "#/components/schemas/SessionNextRevertStaged" - }, - { - "$ref": "#/components/schemas/SessionNextRevertCleared" - }, - { - "$ref": "#/components/schemas/SessionNextRevertCommitted" - }, - { - "$ref": "#/components/schemas/MessagePartDelta" - }, - { - "$ref": "#/components/schemas/SessionDiff" - }, - { - "$ref": "#/components/schemas/SessionError" - }, - { - "$ref": "#/components/schemas/InstallationUpdated" - }, - { - "$ref": "#/components/schemas/InstallationUpdate-available" - }, - { - "$ref": "#/components/schemas/FileEdited" - }, - { - "$ref": "#/components/schemas/ReferenceUpdated" - }, - { - "$ref": "#/components/schemas/PermissionV2Asked" - }, - { - "$ref": "#/components/schemas/PermissionV2Replied" - }, - { - "$ref": "#/components/schemas/PluginAdded" - }, - { - "$ref": "#/components/schemas/ProjectDirectoriesUpdated" - }, - { - "$ref": "#/components/schemas/FileWatcherUpdated" - }, - { - "$ref": "#/components/schemas/PtyCreated" - }, - { - "$ref": "#/components/schemas/PtyUpdated" - }, - { - "$ref": "#/components/schemas/PtyExited" - }, - { - "$ref": "#/components/schemas/PtyDeleted" - }, - { - "$ref": "#/components/schemas/QuestionV2Asked" - }, - { - "$ref": "#/components/schemas/QuestionV2Replied" - }, - { - "$ref": "#/components/schemas/QuestionV2Rejected" - }, - { - "$ref": "#/components/schemas/LspUpdated" - }, - { - "$ref": "#/components/schemas/PermissionAsked" - }, - { - "$ref": "#/components/schemas/PermissionReplied" - }, - { - "$ref": "#/components/schemas/TuiPromptAppend" - }, - { - "$ref": "#/components/schemas/TuiCommandExecute" - }, - { - "$ref": "#/components/schemas/TuiToastShow" - }, - { - "$ref": "#/components/schemas/TuiSessionSelect" - }, - { - "$ref": "#/components/schemas/McpToolsChanged" - }, - { - "$ref": "#/components/schemas/McpBrowserOpenFailed" - }, - { - "$ref": "#/components/schemas/CommandExecuted" - }, - { - "$ref": "#/components/schemas/ProjectUpdated" - }, - { - "$ref": "#/components/schemas/session.status" - }, - { - "$ref": "#/components/schemas/SessionIdle" - }, - { - "$ref": "#/components/schemas/QuestionAsked" - }, - { - "$ref": "#/components/schemas/question.replied" - }, - { - "$ref": "#/components/schemas/question.rejected" - }, - { - "$ref": "#/components/schemas/SessionCompacted" - }, - { - "$ref": "#/components/schemas/VcsBranchUpdated" - }, - { - "$ref": "#/components/schemas/WorkspaceReady" - }, - { - "$ref": "#/components/schemas/WorkspaceFailed" - }, - { - "$ref": "#/components/schemas/WorkspaceStatus" - }, - { - "$ref": "#/components/schemas/WorktreeReady" - }, - { - "$ref": "#/components/schemas/WorktreeFailed" - }, - { - "$ref": "#/components/schemas/ServerConnected" - }, - { - "$ref": "#/components/schemas/GlobalDisposed" - } - ] - }, - "V2EventStream": { - "type": "string", - "contentSchema": { - "$ref": "#/components/schemas/V2Event" - }, - "contentMediaType": "application/json" - }, - "ForbiddenError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["ForbiddenError"] - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "message"], - "additionalProperties": false - }, - "ProjectCopyError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["ProjectCopyError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "forceRequired": { - "type": "boolean" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "effect_HttpApiError_Forbidden": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["Forbidden"] - } - }, - "required": ["_tag"], - "additionalProperties": false - }, - "Event.tui.prompt.append": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["tui.prompt.append"] - }, - "properties": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "Event.tui.command.execute": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["tui.command.execute"] - }, - "properties": { - "type": "object", - "properties": { - "command": { - "anyOf": [ - { - "type": "string", - "enum": [ - "session.list", - "session.new", - "session.share", - "session.interrupt", - "session.compact", - "session.page.up", - "session.page.down", - "session.line.up", - "session.line.down", - "session.half.page.up", - "session.half.page.down", - "session.first", - "session.last", - "prompt.clear", - "prompt.submit", - "agent.cycle" - ] - }, - { - "type": "string" - } - ] - } - }, - "required": ["command"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "Event.tui.toast.show": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["tui.toast.show"] - }, - "properties": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "message": { - "type": "string" - }, - "variant": { - "type": "string", - "enum": ["info", "success", "warning", "error"] - }, - "duration": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["message", "variant"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "Event.tui.session.select": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["tui.session.select"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses", - "description": "Session ID to navigate to" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "CredentialValue": { - "anyOf": [ - { - "$ref": "#/components/schemas/CredentialOAuth" - }, - { - "$ref": "#/components/schemas/CredentialKey" - } - ] - }, - "IntegrationInputs": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "IntegrationMethod": { - "anyOf": [ - { - "$ref": "#/components/schemas/IntegrationOAuthMethod" - }, - { - "$ref": "#/components/schemas/IntegrationKeyMethod" - }, - { - "$ref": "#/components/schemas/IntegrationEnvMethod" - } - ] - }, - "IntegrationRef": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": ["id", "name"], - "additionalProperties": false - }, - "SkillV2Source": { - "anyOf": [ - { - "$ref": "#/components/schemas/SkillV2DirectorySource" - }, - { - "$ref": "#/components/schemas/SkillV2UrlSource" - }, - { - "$ref": "#/components/schemas/SkillV2EmbeddedSource" - } - ] - }, - "MoveSessionDestination": { - "type": "object", - "properties": { - "directory": { - "type": "string" - } - }, - "required": ["directory"], - "additionalProperties": false - }, - "ModelRef": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "variant": { - "type": "string" - } - }, - "required": ["id", "providerID"], - "additionalProperties": false - }, - "LocationRef": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspaceID": { - "type": "string", - "pattern": "^wrk" - } - }, - "required": ["directory"], - "additionalProperties": false - }, - "PromptSource": { - "type": "object", - "properties": { - "start": { - "type": "number" - }, - "end": { - "type": "number" - }, - "text": { - "type": "string" - } - }, - "required": ["start", "end", "text"], - "additionalProperties": false - }, - "PromptFileAttachment": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "mime": { - "type": "string" - }, - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "source": { - "$ref": "#/components/schemas/PromptSource" - } - }, - "required": ["uri", "mime"], - "additionalProperties": false - }, - "PromptAgentAttachment": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "source": { - "$ref": "#/components/schemas/PromptSource" - } - }, - "required": ["name"], - "additionalProperties": false - }, - "SessionErrorUnknown": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["unknown"] - }, - "message": { - "type": "string" - } - }, - "required": ["type", "message"], - "additionalProperties": false - }, - "LLMProviderMetadata": { - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "ToolTextContent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["text"] - }, - "text": { - "type": "string" - } - }, - "required": ["type", "text"], - "additionalProperties": false - }, - "ToolFileContent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["file"] - }, - "uri": { - "type": "string" - }, - "mime": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": ["type", "uri", "mime"], - "additionalProperties": false - }, - "LLMToolContent": { - "anyOf": [ - { - "$ref": "#/components/schemas/ToolTextContent" - }, - { - "$ref": "#/components/schemas/ToolFileContent" - } - ] - }, - "SessionNextRetry_error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "statusCode": { - "type": "number" - }, - "isRetryable": { - "type": "boolean" - }, - "responseHeaders": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "responseBody": { - "type": "string" - }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "required": ["message", "isRetryable"], - "additionalProperties": false - }, - "FileDiff": { - "type": "object", - "properties": { - "path": { - "type": "string" - }, - "status": { - "type": "string", - "enum": ["added", "modified", "deleted"] - }, - "additions": { - "type": "integer", - "minimum": 0 - }, - "deletions": { - "type": "integer", - "minimum": 0 - }, - "patch": { - "type": "string" - } - }, - "required": ["path", "status", "additions", "deletions", "patch"], - "additionalProperties": false - }, - "RevertState": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "partID": { - "type": "string" - }, - "snapshot": { - "type": "string" - }, - "diff": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileDiff" - } - } - }, - "required": ["messageID"], - "additionalProperties": false - }, - "PermissionV2Source": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["tool"] - }, - "messageID": { - "type": "string" - }, - "callID": { - "type": "string" - } - }, - "required": ["type", "messageID", "callID"], - "additionalProperties": false - }, - "PermissionV2Reply": { - "type": "string", - "enum": ["once", "always", "reject"] - }, - "QuestionV2Option": { - "type": "object", - "properties": { - "label": { - "type": "string", - "description": "Display text (1-5 words, concise)" - }, - "description": { - "type": "string", - "description": "Explanation of choice" - } - }, - "required": ["label", "description"], - "additionalProperties": false - }, - "QuestionV2Info": { - "type": "object", - "properties": { - "question": { - "type": "string", - "description": "Complete question" - }, - "header": { - "type": "string", - "description": "Very short label (max 30 chars)" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Option" - }, - "description": "Available choices" - }, - "multiple": { - "type": "boolean" - }, - "custom": { - "type": "boolean" - } - }, - "required": ["question", "header", "options"], - "additionalProperties": false - }, - "QuestionV2Tool": { - "type": "object", - "properties": { - "messageID": { - "type": "string" - }, - "callID": { - "type": "string" - } - }, - "required": ["messageID", "callID"], - "additionalProperties": false - }, - "QuestionV2Answer": { - "type": "array", - "items": { - "type": "string" - } - }, - "ProjectVcs": { - "type": "string", - "enum": ["git"] - }, - "ProjectIcon": { - "type": "object", - "properties": { - "url": { - "type": "string" - }, - "override": { - "type": "string" - }, - "color": { - "type": "string" - } - }, - "additionalProperties": false - }, - "ProjectCommands": { - "type": "object", - "properties": { - "start": { - "type": "string", - "description": "Startup script to run when creating a new workspace (worktree)" - } - }, - "additionalProperties": false - }, - "ProjectTime": { - "type": "object", - "properties": { - "created": { - "type": "integer", - "minimum": 0 - }, - "updated": { - "type": "integer", - "minimum": 0 - }, - "initialized": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["created", "updated"], - "additionalProperties": false - }, - "EventServerInstanceDisposed": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["server.instance.disposed"] - }, - "properties": { - "type": "object", - "properties": { - "directory": { - "type": "string" - } - }, - "required": ["directory"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "SyncEventSessionCreated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.created.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionUpdated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.updated.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionDeleted": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.deleted.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventMessageUpdated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["message.updated.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Message" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventMessageRemoved": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["message.removed.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - } - }, - "required": ["sessionID", "messageID"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventMessagePartUpdated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["message.part.updated.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "part": { - "$ref": "#/components/schemas/Part" - }, - "time": { - "type": "number" - } - }, - "required": ["sessionID", "part", "time"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventMessagePartRemoved": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["message.part.removed.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "partID": { - "type": "string", - "pattern": "^prt" - } - }, - "required": ["sessionID", "messageID", "partID"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextAgentSwitched": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.agent.switched.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "agent": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "agent"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextModelSwitched": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.model.switched.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - } - }, - "required": ["timestamp", "sessionID", "messageID", "model"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextMoved": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.moved.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "subdirectory": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "location"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextPrompted": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.prompted.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "prompt": { - "$ref": "#/components/schemas/Prompt" - }, - "delivery": { - "type": "string", - "enum": ["steer", "queue"] - } - }, - "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextPromptAdmitted": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.prompt.admitted.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "prompt": { - "$ref": "#/components/schemas/Prompt" - }, - "delivery": { - "type": "string", - "enum": ["steer", "queue"] - } - }, - "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextContextUpdated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.context.updated.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "text"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextSynthetic": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.synthetic.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "text"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextShellStarted": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.shell.started.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "command": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "callID", "command"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextShellEnded": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.shell.ended.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "callID": { - "type": "string" - }, - "output": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "callID", "output"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextStepStarted": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.step.started.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "agent": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - }, - "snapshot": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "agent", "model"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextStepEnded": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.step.ended.2"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "finish": { - "type": "string" - }, - "cost": { - "type": "number" - }, - "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false - }, - "snapshot": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "finish", "cost", "tokens"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextStepFailed": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.step.failed.2"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "error": { - "$ref": "#/components/schemas/SessionErrorUnknown" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "error"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextTextStarted": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.text.started.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextTextEnded": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.text.ended.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "text"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextReasoningStarted": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.reasoning.started.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "reasoningID": { - "type": "string" - }, - "providerMetadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextReasoningEnded": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.reasoning.ended.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "reasoningID": { - "type": "string" - }, - "text": { - "type": "string" - }, - "providerMetadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "text"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextToolInputStarted": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.tool.input.started.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "name"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextToolInputEnded": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.tool.input.ended.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "text"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextToolCalled": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.tool.called.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "tool": { - "type": "string" - }, - "input": { - "type": "object" - }, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "tool", "input", "provider"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextToolProgress": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.tool.progress.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "structured": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextToolSuccess": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.tool.success.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "structured": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } - }, - "outputPaths": { - "type": "array", - "items": { - "type": "string" - } - }, - "result": {}, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false - } - }, - "required": [ - "timestamp", - "sessionID", - "assistantMessageID", - "callID", - "structured", - "content", - "provider" - ], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextToolFailed": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.tool.failed.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "error": { - "$ref": "#/components/schemas/SessionErrorUnknown" - }, - "result": {}, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "error", "provider"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextRetried": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.retried.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "attempt": { - "type": "number" - }, - "error": { - "$ref": "#/components/schemas/SessionNextRetry_error" - } - }, - "required": ["timestamp", "sessionID", "attempt", "error"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextCompactionStarted": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.compaction.started.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "reason": { - "type": "string", - "enum": ["auto", "manual"] - } - }, - "required": ["timestamp", "sessionID", "messageID", "reason"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextCompactionEnded": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.compaction.ended.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "reason": { - "type": "string", - "enum": ["auto", "manual"] - }, - "text": { - "type": "string" - }, - "recent": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "reason", "text", "recent"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextRevertStaged": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.revert.staged.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "revert": { - "$ref": "#/components/schemas/RevertState" - } - }, - "required": ["timestamp", "sessionID", "revert"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextRevertCleared": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.revert.cleared.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["timestamp", "sessionID"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextRevertCommitted": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.revert.committed.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - } - }, - "required": ["timestamp", "sessionID", "messageID"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "ConfigV2ReferenceGit": { - "type": "object", - "properties": { - "repository": { - "type": "string" - }, - "branch": { - "type": "string" - }, - "description": { - "type": "string" - }, - "hidden": { - "type": "boolean" - } - }, - "required": ["repository"], - "additionalProperties": false - }, - "ConfigV2ReferenceLocal": { - "type": "object", - "properties": { - "path": { - "type": "string" - }, - "description": { - "type": "string" - }, - "hidden": { - "type": "boolean" - } - }, - "required": ["path"], - "additionalProperties": false - }, - "PolicyEffect": { - "type": "string", - "enum": ["allow", "deny"] - }, - "ConfigV2ExperimentalPolicy": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": ["provider.use"] - }, - "effect": { - "$ref": "#/components/schemas/PolicyEffect" - }, - "resource": { - "type": "string" - } - }, - "required": ["action", "effect", "resource"], - "additionalProperties": false - }, - "ProjectDirectories": { - "type": "array", - "items": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "strategy": { - "type": "string" - } - }, - "required": ["directory"], - "additionalProperties": false - } - }, - "PtyTicketConnectToken": { - "type": "object", - "properties": { - "ticket": { - "type": "string" - }, - "expires_in": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["ticket", "expires_in"], - "additionalProperties": false - }, - "WorkspaceEventConnectionStatus": { - "type": "object", - "properties": { - "workspaceID": { - "type": "string", - "pattern": "^wrk" - }, - "status": { - "type": "string", - "enum": ["connected", "connecting", "disconnected", "error"] - } - }, - "required": ["workspaceID", "status"], - "additionalProperties": false - }, - "LocationInfo": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspaceID": { - "type": "string", - "pattern": "^wrk" - }, - "project": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "directory": { - "type": "string" - } - }, - "required": ["id", "directory"], - "additionalProperties": false - } - }, - "required": ["directory", "project"], - "additionalProperties": false - }, - "ProviderRequest": { - "type": "object", - "properties": { - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "body": { - "type": "object" - } - }, - "required": ["headers", "body"], - "additionalProperties": false - }, - "AgentColor": { - "anyOf": [ - { - "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$" - }, - { - "type": "string", - "enum": ["primary", "secondary", "accent", "success", "warning", "error", "info"] - } - ] - }, - "PermissionV2Effect": { - "type": "string", - "enum": ["allow", "deny", "ask"] - }, - "PermissionV2Rule": { - "type": "object", - "properties": { - "action": { - "type": "string" - }, - "resource": { - "type": "string" - }, - "effect": { - "$ref": "#/components/schemas/PermissionV2Effect" - } - }, - "required": ["action", "resource", "effect"], - "additionalProperties": false - }, - "PermissionV2Ruleset": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionV2Rule" - } - }, - "AgentV2Info": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - }, - "request": { - "$ref": "#/components/schemas/ProviderRequest" - }, - "system": { - "type": "string" - }, - "description": { - "type": "string" - }, - "mode": { - "type": "string", - "enum": ["subagent", "primary", "all"] - }, - "hidden": { - "type": "boolean" - }, - "color": { - "$ref": "#/components/schemas/AgentColor" - }, - "steps": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "permissions": { - "$ref": "#/components/schemas/PermissionV2Ruleset" - } - }, - "required": ["id", "request", "mode", "hidden", "permissions"], - "additionalProperties": false - }, - "SessionV2Info": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^ses" - }, - "parentID": { - "type": "string", - "pattern": "^ses" - }, - "projectID": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - }, - "cost": { - "type": "number" - }, - "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - }, - "updated": { - "type": "number" - }, - "archived": { - "type": "number" - } - }, - "required": ["created", "updated"], - "additionalProperties": false - }, - "title": { - "type": "string" - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "subpath": { - "type": "string" - }, - "revert": { - "$ref": "#/components/schemas/RevertState" - } - }, - "required": ["id", "projectID", "cost", "tokens", "time", "title", "location"], - "additionalProperties": false - }, - "PromptInputFileAttachment": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "source": { - "$ref": "#/components/schemas/PromptSource" - } - }, - "required": ["uri"], - "additionalProperties": false - }, - "SessionInputAdmitted": { - "type": "object", - "properties": { - "admittedSeq": { - "type": "integer", - "minimum": 0 - }, - "id": { - "type": "string", - "pattern": "^msg_" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "prompt": { - "$ref": "#/components/schemas/Prompt" - }, - "delivery": { - "type": "string", - "enum": ["steer", "queue"] - }, - "timeCreated": { - "type": "number" - }, - "promotedSeq": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["admittedSeq", "id", "sessionID", "prompt", "delivery", "timeCreated"], - "additionalProperties": false - }, - "SessionMessageAgentSwitched": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^msg_" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - } - }, - "required": ["created"], - "additionalProperties": false - }, - "type": { - "type": "string", - "enum": ["agent-switched"] - }, - "agent": { - "type": "string" - } - }, - "required": ["id", "time", "type", "agent"], - "additionalProperties": false - }, - "SessionMessageModelSwitched": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^msg_" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - } - }, - "required": ["created"], - "additionalProperties": false - }, - "type": { - "type": "string", - "enum": ["model-switched"] - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - } - }, - "required": ["id", "time", "type", "model"], - "additionalProperties": false - }, - "SessionMessageUser": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^msg_" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - } - }, - "required": ["created"], - "additionalProperties": false - }, - "text": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PromptFileAttachment" - } - }, - "agents": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PromptAgentAttachment" - } - }, - "type": { - "type": "string", - "enum": ["user"] - } - }, - "required": ["id", "time", "text", "type"], - "additionalProperties": false - }, - "SessionMessageSynthetic": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^msg_" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - } - }, - "required": ["created"], - "additionalProperties": false - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "text": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["session.synthetic"] - } - }, - "required": ["id", "time", "sessionID", "text", "type"], - "additionalProperties": false - }, - "SessionMessageSystem": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^msg_" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - } - }, - "required": ["created"], - "additionalProperties": false - }, - "type": { - "type": "string", - "enum": ["system"] - }, - "text": { - "type": "string" - } - }, - "required": ["id", "time", "type", "text"], - "additionalProperties": false - }, - "SessionMessageShell": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^msg_" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - }, - "completed": { - "type": "number" - } - }, - "required": ["created"], - "additionalProperties": false - }, - "type": { - "type": "string", - "enum": ["shell"] - }, - "callID": { - "type": "string" - }, - "command": { - "type": "string" - }, - "output": { - "type": "string" - } - }, - "required": ["id", "time", "type", "callID", "command", "output"], - "additionalProperties": false - }, - "SessionMessageAssistantText": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["text"] - }, - "id": { - "type": "string" - }, - "text": { - "type": "string" - } - }, - "required": ["type", "id", "text"], - "additionalProperties": false - }, - "SessionMessageAssistantReasoning": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["reasoning"] - }, - "id": { - "type": "string" - }, - "text": { - "type": "string" - }, - "providerMetadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - }, - "completed": { - "type": "number" - } - }, - "required": ["created"], - "additionalProperties": false - } - }, - "required": ["type", "id", "text"], - "additionalProperties": false - }, - "SessionMessageToolStatePending": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["pending"] - }, - "input": { - "type": "string" - } - }, - "required": ["status", "input"], - "additionalProperties": false - }, - "SessionMessageToolStateRunning": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["running"] - }, - "input": { - "type": "object" - }, - "structured": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } - } - }, - "required": ["status", "input", "structured", "content"], - "additionalProperties": false - }, - "SessionMessageToolStateCompleted": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["completed"] - }, - "input": { - "type": "object" - }, - "attachments": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PromptFileAttachment" - } - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } - }, - "outputPaths": { - "type": "array", - "items": { - "type": "string" - } - }, - "structured": { - "type": "object" - }, - "result": {} - }, - "required": ["status", "input", "content", "structured"], - "additionalProperties": false - }, - "SessionMessageToolStateError": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["error"] - }, - "input": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } - }, - "structured": { - "type": "object" - }, - "error": { - "$ref": "#/components/schemas/SessionErrorUnknown" - }, - "result": {} - }, - "required": ["status", "input", "content", "structured", "error"], - "additionalProperties": false - }, - "SessionMessageAssistantTool": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["tool"] - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - }, - "resultMetadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false - }, - "state": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionMessageToolStatePending" - }, - { - "$ref": "#/components/schemas/SessionMessageToolStateRunning" - }, - { - "$ref": "#/components/schemas/SessionMessageToolStateCompleted" - }, - { - "$ref": "#/components/schemas/SessionMessageToolStateError" - } - ] - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - }, - "ran": { - "type": "number" - }, - "completed": { - "type": "number" - }, - "pruned": { - "type": "number" - } - }, - "required": ["created"], - "additionalProperties": false - } - }, - "required": ["type", "id", "name", "state", "time"], - "additionalProperties": false - }, - "SessionMessageAssistant": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^msg_" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - }, - "completed": { - "type": "number" - } - }, - "required": ["created"], - "additionalProperties": false - }, - "type": { - "type": "string", - "enum": ["assistant"] - }, - "agent": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - }, - "content": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionMessageAssistantText" - }, - { - "$ref": "#/components/schemas/SessionMessageAssistantReasoning" - }, - { - "$ref": "#/components/schemas/SessionMessageAssistantTool" - } - ] - } - }, - "snapshot": { - "type": "object", - "properties": { - "start": { - "type": "string" - }, - "end": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - }, - "finish": { - "type": "string" - }, - "cost": { - "type": "number" - }, - "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false - }, - "error": { - "$ref": "#/components/schemas/SessionErrorUnknown" - } - }, - "required": ["id", "time", "type", "agent", "model", "content"], - "additionalProperties": false - }, - "SessionMessageCompaction": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["compaction"] - }, - "reason": { - "type": "string", - "enum": ["auto", "manual"] - }, - "summary": { - "type": "string" - }, - "recent": { - "type": "string" - }, - "id": { - "type": "string", - "pattern": "^msg_" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - } - }, - "required": ["created"], - "additionalProperties": false - } - }, - "required": ["type", "reason", "summary", "recent", "id", "time"], - "additionalProperties": false - }, - "SessionMessage": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionMessageAgentSwitched" - }, - { - "$ref": "#/components/schemas/SessionMessageModelSwitched" - }, - { - "$ref": "#/components/schemas/SessionMessageUser" - }, - { - "$ref": "#/components/schemas/SessionMessageSynthetic" - }, - { - "$ref": "#/components/schemas/SessionMessageSystem" - }, - { - "$ref": "#/components/schemas/SessionMessageShell" - }, - { - "$ref": "#/components/schemas/SessionMessageAssistant" - }, - { - "$ref": "#/components/schemas/SessionMessageCompaction" - } - ] - }, - "SessionNextAgentSwitched": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.agent.switched"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "agent": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "agent"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextModelSwitched": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.model.switched"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - } - }, - "required": ["timestamp", "sessionID", "messageID", "model"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextMoved": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.moved"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "subdirectory": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "location"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextPrompted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.prompted"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "prompt": { - "$ref": "#/components/schemas/Prompt" - }, - "delivery": { - "type": "string", - "enum": ["steer", "queue"] - } - }, - "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextPromptAdmitted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.prompt.admitted"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "prompt": { - "$ref": "#/components/schemas/Prompt" - }, - "delivery": { - "type": "string", - "enum": ["steer", "queue"] - } - }, - "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextContextUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.context.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextSynthetic": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.synthetic"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextShellStarted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.shell.started"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "command": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "callID", "command"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextShellEnded": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.shell.ended"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "callID": { - "type": "string" - }, - "output": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "callID", "output"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextStepStarted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.step.started"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "agent": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - }, - "snapshot": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "agent", "model"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextStepEnded": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.step.ended"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "finish": { - "type": "string" - }, - "cost": { - "type": "number" - }, - "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false - }, - "snapshot": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "finish", "cost", "tokens"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextStepFailed": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.step.failed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "error": { - "$ref": "#/components/schemas/SessionErrorUnknown" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "error"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextTextStarted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.text.started"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextTextEnded": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.text.ended"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextToolInputStarted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.input.started"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "name"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextToolInputEnded": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.input.ended"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextToolCalled": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.called"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "tool": { - "type": "string" - }, - "input": { - "type": "object" - }, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "tool", "input", "provider"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextToolProgress": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.progress"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "structured": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextToolSuccess": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.success"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "structured": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } - }, - "outputPaths": { - "type": "array", - "items": { - "type": "string" - } - }, - "result": {}, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content", "provider"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextToolFailed": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.failed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "error": { - "$ref": "#/components/schemas/SessionErrorUnknown" - }, - "result": {}, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "error", "provider"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextReasoningStarted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.reasoning.started"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "reasoningID": { - "type": "string" - }, - "providerMetadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextReasoningEnded": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.reasoning.ended"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "reasoningID": { - "type": "string" - }, - "text": { - "type": "string" - }, - "providerMetadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextRetried": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.retried"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "attempt": { - "type": "number" - }, - "error": { - "$ref": "#/components/schemas/SessionNextRetry_error" - } - }, - "required": ["timestamp", "sessionID", "attempt", "error"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextCompactionStarted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.compaction.started"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "reason": { - "type": "string", - "enum": ["auto", "manual"] - } - }, - "required": ["timestamp", "sessionID", "messageID", "reason"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextCompactionEnded": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.compaction.ended"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "reason": { - "type": "string", - "enum": ["auto", "manual"] - }, - "text": { - "type": "string" - }, - "recent": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "reason", "text", "recent"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextRevertStaged": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.revert.staged"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "revert": { - "$ref": "#/components/schemas/RevertState" - } - }, - "required": ["timestamp", "sessionID", "revert"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextRevertCleared": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.revert.cleared"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["timestamp", "sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextRevertCommitted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.revert.committed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - } - }, - "required": ["timestamp", "sessionID", "messageID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "ModelApi": { - "anyOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["aisdk"] - }, - "package": { - "type": "string" - }, - "url": { - "type": "string" - }, - "settings": { - "type": "object" - } - }, - "required": ["id", "type", "package"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["native"] - }, - "url": { - "type": "string" - }, - "settings": { - "type": "object" - } - }, - "required": ["id", "type", "settings"], - "additionalProperties": false - } - ] - }, - "ModelCapabilities": { - "type": "object", - "properties": { - "tools": { - "type": "boolean" - }, - "input": { - "type": "array", - "items": { - "type": "string" - } - }, - "output": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["tools", "input", "output"], - "additionalProperties": false - }, - "ModelCost": { - "type": "object", - "properties": { - "tier": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["context"] - }, - "size": { - "type": "integer" - } - }, - "required": ["type", "size"], - "additionalProperties": false - }, - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "cache"], - "additionalProperties": false - }, - "ModelV2Info": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "family": { - "type": "string" - }, - "name": { - "type": "string" - }, - "api": { - "$ref": "#/components/schemas/ModelApi" - }, - "capabilities": { - "$ref": "#/components/schemas/ModelCapabilities" - }, - "request": { - "type": "object", - "properties": { - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "body": { - "type": "object" - }, - "variant": { - "type": "string" - } - }, - "required": ["headers", "body"], - "additionalProperties": false - }, - "variants": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "body": { - "type": "object" - } - }, - "required": ["id", "headers", "body"], - "additionalProperties": false - } - }, - "time": { - "type": "object", - "properties": { - "released": { - "type": "number" - } - }, - "required": ["released"], - "additionalProperties": false - }, - "cost": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ModelCost" - } - }, - "status": { - "type": "string", - "enum": ["alpha", "beta", "deprecated", "active"] - }, - "enabled": { - "type": "boolean" - }, - "limit": { - "type": "object", - "properties": { - "context": { - "type": "integer" - }, - "input": { - "type": "integer" - }, - "output": { - "type": "integer" - } - }, - "required": ["context", "output"], - "additionalProperties": false - } - }, - "required": [ - "id", - "providerID", - "name", - "api", - "capabilities", - "request", - "variants", - "time", - "cost", - "status", - "enabled", - "limit" - ], - "additionalProperties": false - }, - "ProviderAISDK": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["aisdk"] - }, - "package": { - "type": "string" - }, - "url": { - "type": "string" - }, - "settings": { - "type": "object" - } - }, - "required": ["type", "package"], - "additionalProperties": false - }, - "ProviderNative": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["native"] - }, - "url": { - "type": "string" - }, - "settings": { - "type": "object" - } - }, - "required": ["type", "settings"], - "additionalProperties": false - }, - "ProviderApi": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProviderAISDK" - }, - { - "$ref": "#/components/schemas/ProviderNative" - } - ] - }, - "ProviderV2Info": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "integrationID": { - "type": "string" - }, - "name": { - "type": "string" - }, - "disabled": { - "type": "boolean" - }, - "api": { - "$ref": "#/components/schemas/ProviderApi" - }, - "request": { - "$ref": "#/components/schemas/ProviderRequest" - } - }, - "required": ["id", "name", "api", "request"], - "additionalProperties": false - }, - "IntegrationWhen": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "op": { - "type": "string", - "enum": ["eq", "neq"] - }, - "value": { - "type": "string" - } - }, - "required": ["key", "op", "value"], - "additionalProperties": false - }, - "IntegrationTextPrompt": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["text"] - }, - "key": { - "type": "string" - }, - "message": { - "type": "string" - }, - "placeholder": { - "type": "string" - }, - "when": { - "$ref": "#/components/schemas/IntegrationWhen" - } - }, - "required": ["type", "key", "message"], - "additionalProperties": false - }, - "IntegrationSelectPrompt": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["select"] - }, - "key": { - "type": "string" - }, - "message": { - "type": "string" - }, - "options": { - "type": "array", - "items": { - "type": "object", - "properties": { - "label": { - "type": "string" - }, - "value": { - "type": "string" - }, - "hint": { - "type": "string" - } - }, - "required": ["label", "value"], - "additionalProperties": false - } - }, - "when": { - "$ref": "#/components/schemas/IntegrationWhen" - } - }, - "required": ["type", "key", "message", "options"], - "additionalProperties": false - }, - "IntegrationOAuthMethod": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["oauth"] - }, - "label": { - "type": "string" - }, - "prompts": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/IntegrationTextPrompt" - }, - { - "$ref": "#/components/schemas/IntegrationSelectPrompt" - } - ] - } - } - }, - "required": ["id", "type", "label"], - "additionalProperties": false - }, - "IntegrationKeyMethod": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["key"] - }, - "label": { - "type": "string" - } - }, - "required": ["type"], - "additionalProperties": false - }, - "IntegrationEnvMethod": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["env"] - }, - "names": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["type", "names"], - "additionalProperties": false - }, - "ConnectionCredentialInfo": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["credential"] - }, - "id": { - "type": "string" - }, - "label": { - "type": "string" - } - }, - "required": ["type", "id", "label"], - "additionalProperties": false - }, - "ConnectionEnvInfo": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["env"] - }, - "name": { - "type": "string" - } - }, - "required": ["type", "name"], - "additionalProperties": false - }, - "ConnectionInfo": { - "anyOf": [ - { - "$ref": "#/components/schemas/ConnectionCredentialInfo" - }, - { - "$ref": "#/components/schemas/ConnectionEnvInfo" - } - ] - }, - "IntegrationInfo": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "methods": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IntegrationMethod" - } - }, - "connections": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ConnectionInfo" - } - } - }, - "required": ["id", "name", "methods", "connections"], - "additionalProperties": false - }, - "IntegrationAttempt": { - "type": "object", - "properties": { - "attemptID": { - "type": "string" - }, - "url": { - "type": "string" - }, - "instructions": { - "type": "string" - }, - "mode": { - "type": "string", - "enum": ["auto", "code"] - }, - "time": { - "type": "object", - "properties": { - "created": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "expires": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - } - }, - "required": ["created", "expires"], - "additionalProperties": false - } - }, - "required": ["attemptID", "url", "instructions", "mode", "time"], - "additionalProperties": false - }, - "IntegrationAttemptStatus": { - "anyOf": [ - { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["pending"] - }, - "time": { - "type": "object", - "properties": { - "created": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "expires": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - } - }, - "required": ["created", "expires"], - "additionalProperties": false - } - }, - "required": ["status", "time"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["complete"] - }, - "time": { - "type": "object", - "properties": { - "created": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "expires": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - } - }, - "required": ["created", "expires"], - "additionalProperties": false - } - }, - "required": ["status", "time"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["failed"] - }, - "message": { - "type": "string" - }, - "time": { - "type": "object", - "properties": { - "created": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "expires": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - } - }, - "required": ["created", "expires"], - "additionalProperties": false - } - }, - "required": ["status", "message", "time"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["expired"] - }, - "time": { - "type": "object", - "properties": { - "created": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "expires": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - } - }, - "required": ["created", "expires"], - "additionalProperties": false - } - }, - "required": ["status", "time"], - "additionalProperties": false - } - ] - }, - "PermissionV2Request": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "action": { - "type": "string" - }, - "resources": { - "type": "array", - "items": { - "type": "string" - } - }, - "save": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "source": { - "$ref": "#/components/schemas/PermissionV2Source" - } - }, - "required": ["id", "sessionID", "action", "resources"], - "additionalProperties": false - }, - "PermissionSavedInfo": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "projectID": { - "type": "string" - }, - "action": { - "type": "string" - }, - "resource": { - "type": "string" - } - }, - "required": ["id", "projectID", "action", "resource"], - "additionalProperties": false - }, - "FileSystemEntry": { - "type": "object", - "properties": { - "path": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["file", "directory"] - } - }, - "required": ["path", "type"], - "additionalProperties": false - }, - "CommandV2Info": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "template": { - "type": "string" - }, - "description": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - }, - "subtask": { - "type": "boolean" - } - }, - "required": ["name", "template"], - "additionalProperties": false - }, - "SkillV2Info": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "slash": { - "type": "boolean" - }, - "location": { - "type": "string" - }, - "content": { - "type": "string" - } - }, - "required": ["name", "location", "content"], - "additionalProperties": false - }, - "Models-devRefreshed": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["models-dev.refreshed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "IntegrationUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["integration.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "IntegrationConnectionUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["integration.connection.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "integrationID": { - "type": "string" - } - }, - "required": ["integrationID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "CatalogUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["catalog.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionCreated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.created"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionDeleted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.deleted"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "MessageUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["message.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Message" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "MessageRemoved": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["message.removed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - } - }, - "required": ["sessionID", "messageID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "MessagePartUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["message.part.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "part": { - "$ref": "#/components/schemas/Part" - }, - "time": { - "type": "number" - } - }, - "required": ["sessionID", "part", "time"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "MessagePartRemoved": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["message.part.removed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "partID": { - "type": "string", - "pattern": "^prt" - } - }, - "required": ["sessionID", "messageID", "partID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextTextDelta": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.text.delta"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "delta"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextReasoningDelta": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.reasoning.delta"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "reasoningID": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "delta"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextToolInputDelta": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.input.delta"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "delta"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextCompactionDelta": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.compaction.delta"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "MessagePartDelta": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["message.part.delta"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "partID": { - "type": "string", - "pattern": "^prt" - }, - "field": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["sessionID", "messageID", "partID", "field", "delta"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionDiff": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.diff"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "diff": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" - } - } - }, - "required": ["sessionID", "diff"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionError": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.error"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "error": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProviderAuthError" - }, - { - "$ref": "#/components/schemas/UnknownError" - }, - { - "$ref": "#/components/schemas/MessageOutputLengthError" - }, - { - "$ref": "#/components/schemas/MessageAbortedError" - }, - { - "$ref": "#/components/schemas/StructuredOutputError" - }, - { - "$ref": "#/components/schemas/ContextOverflowError" - }, - { - "$ref": "#/components/schemas/ContentFilterError" - }, - { - "$ref": "#/components/schemas/APIError" - } - ] - } - }, - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "InstallationUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["installation.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "required": ["version"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "InstallationUpdate-available": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["installation.update-available"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "required": ["version"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "FileEdited": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["file.edited"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "file": { - "type": "string" - } - }, - "required": ["file"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "ReferenceUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["reference.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "PermissionV2Asked": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["permission.v2.asked"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "action": { - "type": "string" - }, - "resources": { - "type": "array", - "items": { - "type": "string" - } - }, - "save": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "source": { - "$ref": "#/components/schemas/PermissionV2Source" - } - }, - "required": ["id", "sessionID", "action", "resources"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "PermissionV2Replied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["permission.v2.replied"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^per" - }, - "reply": { - "$ref": "#/components/schemas/PermissionV2Reply" - } - }, - "required": ["sessionID", "requestID", "reply"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "PluginAdded": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["plugin.added"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": ["id"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "ProjectDirectoriesUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["project.directories.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "projectID": { - "type": "string" - } - }, - "required": ["projectID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "FileWatcherUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["file.watcher.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "event": { - "type": "string", - "enum": ["add", "change", "unlink"] - } - }, - "required": ["file", "event"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "PtyCreated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["pty.created"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": ["info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "PtyUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["pty.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": ["info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "PtyExited": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["pty.exited"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^pty" - }, - "exitCode": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["id", "exitCode"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "PtyDeleted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["pty.deleted"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^pty" - } - }, - "required": ["id"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "QuestionV2Asked": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["question.v2.asked"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^que" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Info" - }, - "description": "Questions to ask" - }, - "tool": { - "$ref": "#/components/schemas/QuestionV2Tool" - } - }, - "required": ["id", "sessionID", "questions"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "QuestionV2Replied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["question.v2.replied"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Answer" - } - } - }, - "required": ["sessionID", "requestID", "answers"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "QuestionV2Rejected": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["question.v2.rejected"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - } - }, - "required": ["sessionID", "requestID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "LspUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["lsp.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "PermissionAsked": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["permission.asked"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "permission": { - "type": "string" - }, - "patterns": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "always": { - "type": "array", - "items": { - "type": "string" - } - }, - "tool": { - "type": "object", - "properties": { - "messageID": { - "type": "string" - }, - "callID": { - "type": "string" - } - }, - "required": ["messageID", "callID"], - "additionalProperties": false - } - }, - "required": ["id", "sessionID", "permission", "patterns", "metadata", "always"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "PermissionReplied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["permission.replied"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^per" - }, - "reply": { - "type": "string", - "enum": ["once", "always", "reject"] - } - }, - "required": ["sessionID", "requestID", "reply"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "TuiPromptAppend": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["tui.prompt.append"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "TuiCommandExecute": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["tui.command.execute"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "command": { - "anyOf": [ - { - "type": "string", - "enum": [ - "session.list", - "session.new", - "session.share", - "session.interrupt", - "session.compact", - "session.page.up", - "session.page.down", - "session.line.up", - "session.line.down", - "session.half.page.up", - "session.half.page.down", - "session.first", - "session.last", - "prompt.clear", - "prompt.submit", - "agent.cycle" - ] - }, - { - "type": "string" - } - ] - } - }, - "required": ["command"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "TuiToastShow": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["tui.toast.show"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "message": { - "type": "string" - }, - "variant": { - "type": "string", - "enum": ["info", "success", "warning", "error"] - }, - "duration": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["message", "variant"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "TuiSessionSelect": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["tui.session.select"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses", - "description": "Session ID to navigate to" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "McpToolsChanged": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["mcp.tools.changed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "server": { - "type": "string" - } - }, - "required": ["server"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "McpBrowserOpenFailed": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["mcp.browser.open.failed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "mcpName": { - "type": "string" - }, - "url": { - "type": "string" - } - }, - "required": ["mcpName", "url"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "CommandExecuted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["command.executed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "arguments": { - "type": "string" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - } - }, - "required": ["name", "sessionID", "arguments", "messageID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "ProjectUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["project.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "worktree": { - "type": "string" - }, - "vcs": { - "$ref": "#/components/schemas/ProjectVcs" - }, - "name": { - "type": "string" - }, - "icon": { - "$ref": "#/components/schemas/ProjectIcon" - }, - "commands": { - "$ref": "#/components/schemas/ProjectCommands" - }, - "time": { - "$ref": "#/components/schemas/ProjectTime" - }, - "sandboxes": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["id", "worktree", "time", "sandboxes"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionIdle": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.idle"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "QuestionAsked": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["question.asked"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^que" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionInfo" - }, - "description": "Questions to ask" - }, - "tool": { - "$ref": "#/components/schemas/QuestionTool" - } - }, - "required": ["id", "sessionID", "questions"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionCompacted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.compacted"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "VcsBranchUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["vcs.branch.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "branch": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "WorkspaceReady": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["workspace.ready"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": ["name"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "WorkspaceFailed": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["workspace.failed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "WorkspaceStatus": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["workspace.status"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "workspaceID": { - "type": "string", - "pattern": "^wrk" - }, - "status": { - "type": "string", - "enum": ["connected", "connecting", "disconnected", "error"] - } - }, - "required": ["workspaceID", "status"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "WorktreeReady": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["worktree.ready"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "branch": { - "type": "string" - } - }, - "required": ["name"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "WorktreeFailed": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["worktree.failed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "ServerConnected": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["server.connected"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "GlobalDisposed": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["global.disposed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "QuestionV2Request": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^que" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Info" - }, - "description": "Questions to ask" - }, - "tool": { - "$ref": "#/components/schemas/QuestionV2Tool" - } - }, - "required": ["id", "sessionID", "questions"], - "additionalProperties": false - }, - "QuestionV2Reply": { - "type": "object", - "properties": { - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Answer" - }, - "description": "User answers in order of questions (each answer is an array of selected labels)" - } - }, - "required": ["answers"], - "additionalProperties": false - }, - "ReferenceLocalSource": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["local"] - }, - "path": { - "type": "string" - }, - "description": { - "type": "string" - }, - "hidden": { - "type": "boolean" - } - }, - "required": ["type", "path"], - "additionalProperties": false - }, - "ReferenceGitSource": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["git"] - }, - "repository": { - "type": "string" - }, - "branch": { - "type": "string" - }, - "description": { - "type": "string" - }, - "hidden": { - "type": "boolean" - } - }, - "required": ["type", "repository"], - "additionalProperties": false - }, - "ReferenceSource": { - "anyOf": [ - { - "$ref": "#/components/schemas/ReferenceLocalSource" - }, - { - "$ref": "#/components/schemas/ReferenceGitSource" - } - ] - }, - "ReferenceInfo": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "description": { - "type": "string" - }, - "hidden": { - "type": "boolean" - }, - "source": { - "$ref": "#/components/schemas/ReferenceSource" - } - }, - "required": ["name", "path", "source"], - "additionalProperties": false - }, - "ProjectCopyCopy": { - "type": "object", - "properties": { - "directory": { - "type": "string" - } - }, - "required": ["directory"], - "additionalProperties": false - }, - "EventModels-devRefreshed": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["models-dev.refreshed"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventIntegrationUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["integration.updated"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventIntegrationConnectionUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["integration.connection.updated"] - }, - "properties": { - "type": "object", - "properties": { - "integrationID": { - "type": "string" - } - }, - "required": ["integrationID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventCatalogUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["catalog.updated"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionCreated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.created"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.updated"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionDeleted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.deleted"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventMessageUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["message.updated"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Message" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventMessageRemoved": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["message.removed"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - } - }, - "required": ["sessionID", "messageID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventMessagePartUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["message.part.updated"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "part": { - "$ref": "#/components/schemas/Part" - }, - "time": { - "type": "number" - } - }, - "required": ["sessionID", "part", "time"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventMessagePartRemoved": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["message.part.removed"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "partID": { - "type": "string", - "pattern": "^prt" - } - }, - "required": ["sessionID", "messageID", "partID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextAgentSwitched": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.agent.switched"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "agent": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "agent"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextModelSwitched": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.model.switched"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - } - }, - "required": ["timestamp", "sessionID", "messageID", "model"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextMoved": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.moved"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "subdirectory": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "location"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextPrompted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.prompted"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "prompt": { - "$ref": "#/components/schemas/Prompt" - }, - "delivery": { - "type": "string", - "enum": ["steer", "queue"] - } - }, - "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextPromptAdmitted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.prompt.admitted"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "prompt": { - "$ref": "#/components/schemas/Prompt" - }, - "delivery": { - "type": "string", - "enum": ["steer", "queue"] - } - }, - "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextContextUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.context.updated"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextSynthetic": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.synthetic"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextShellStarted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.shell.started"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "command": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "callID", "command"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextShellEnded": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.shell.ended"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "callID": { - "type": "string" - }, - "output": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "callID", "output"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextStepStarted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.step.started"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "agent": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - }, - "snapshot": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "agent", "model"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextStepEnded": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.step.ended"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "finish": { - "type": "string" - }, - "cost": { - "type": "number" - }, - "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false - }, - "snapshot": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "finish", "cost", "tokens"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextStepFailed": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.step.failed"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "error": { - "$ref": "#/components/schemas/SessionErrorUnknown" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "error"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextTextStarted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.text.started"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextTextDelta": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.text.delta"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "delta"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextTextEnded": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.text.ended"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextReasoningStarted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.reasoning.started"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "reasoningID": { - "type": "string" - }, - "providerMetadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextReasoningDelta": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.reasoning.delta"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "reasoningID": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "delta"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextReasoningEnded": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.reasoning.ended"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "reasoningID": { - "type": "string" - }, - "text": { - "type": "string" - }, - "providerMetadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextToolInputStarted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.input.started"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "name"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextToolInputDelta": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.input.delta"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "delta"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextToolInputEnded": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.input.ended"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextToolCalled": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.called"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "tool": { - "type": "string" - }, - "input": { - "type": "object" - }, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "tool", "input", "provider"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextToolProgress": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.progress"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "structured": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextToolSuccess": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.success"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "structured": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } - }, - "outputPaths": { - "type": "array", - "items": { - "type": "string" - } - }, - "result": {}, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content", "provider"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextToolFailed": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.failed"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "error": { - "$ref": "#/components/schemas/SessionErrorUnknown" - }, - "result": {}, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "error", "provider"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextRetried": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.retried"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "attempt": { - "type": "number" - }, - "error": { - "$ref": "#/components/schemas/SessionNextRetry_error" - } - }, - "required": ["timestamp", "sessionID", "attempt", "error"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextCompactionStarted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.compaction.started"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "reason": { - "type": "string", - "enum": ["auto", "manual"] - } - }, - "required": ["timestamp", "sessionID", "messageID", "reason"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextCompactionDelta": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.compaction.delta"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextCompactionEnded": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.compaction.ended"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "reason": { - "type": "string", - "enum": ["auto", "manual"] - }, - "text": { - "type": "string" - }, - "recent": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "reason", "text", "recent"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextRevertStaged": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.revert.staged"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "revert": { - "$ref": "#/components/schemas/RevertState" - } - }, - "required": ["timestamp", "sessionID", "revert"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextRevertCleared": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.revert.cleared"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["timestamp", "sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextRevertCommitted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.revert.committed"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - } - }, - "required": ["timestamp", "sessionID", "messageID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventMessagePartDelta": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["message.part.delta"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "partID": { - "type": "string", - "pattern": "^prt" - }, - "field": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["sessionID", "messageID", "partID", "field", "delta"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionDiff": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.diff"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "diff": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" - } - } - }, - "required": ["sessionID", "diff"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionError": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.error"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "error": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProviderAuthError" - }, - { - "$ref": "#/components/schemas/UnknownError" - }, - { - "$ref": "#/components/schemas/MessageOutputLengthError" - }, - { - "$ref": "#/components/schemas/MessageAbortedError" - }, - { - "$ref": "#/components/schemas/StructuredOutputError" - }, - { - "$ref": "#/components/schemas/ContextOverflowError" - }, - { - "$ref": "#/components/schemas/ContentFilterError" - }, - { - "$ref": "#/components/schemas/APIError" - } - ] - } - }, - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventInstallationUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["installation.updated"] - }, - "properties": { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "required": ["version"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventInstallationUpdate-available": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["installation.update-available"] - }, - "properties": { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "required": ["version"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventFileEdited": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["file.edited"] - }, - "properties": { - "type": "object", - "properties": { - "file": { - "type": "string" - } - }, - "required": ["file"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventReferenceUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["reference.updated"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventPermissionV2Asked": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["permission.v2.asked"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "action": { - "type": "string" - }, - "resources": { - "type": "array", - "items": { - "type": "string" - } - }, - "save": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "source": { - "$ref": "#/components/schemas/PermissionV2Source" - } - }, - "required": ["id", "sessionID", "action", "resources"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventPermissionV2Replied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["permission.v2.replied"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^per" - }, - "reply": { - "$ref": "#/components/schemas/PermissionV2Reply" - } - }, - "required": ["sessionID", "requestID", "reply"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventPluginAdded": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["plugin.added"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": ["id"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventProjectDirectoriesUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["project.directories.updated"] - }, - "properties": { - "type": "object", - "properties": { - "projectID": { - "type": "string" - } - }, - "required": ["projectID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventFileWatcherUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["file.watcher.updated"] - }, - "properties": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "event": { - "type": "string", - "enum": ["add", "change", "unlink"] - } - }, - "required": ["file", "event"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventPtyCreated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["pty.created"] - }, - "properties": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": ["info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventPtyUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["pty.updated"] - }, - "properties": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": ["info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventPtyExited": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["pty.exited"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^pty" - }, - "exitCode": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["id", "exitCode"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventPtyDeleted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["pty.deleted"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^pty" - } - }, - "required": ["id"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventQuestionV2Asked": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["question.v2.asked"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^que" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Info" - }, - "description": "Questions to ask" - }, - "tool": { - "$ref": "#/components/schemas/QuestionV2Tool" - } - }, - "required": ["id", "sessionID", "questions"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventQuestionV2Replied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["question.v2.replied"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Answer" - } - } - }, - "required": ["sessionID", "requestID", "answers"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventQuestionV2Rejected": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["question.v2.rejected"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - } - }, - "required": ["sessionID", "requestID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventLspUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["lsp.updated"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventPermissionAsked": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["permission.asked"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "permission": { - "type": "string" - }, - "patterns": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "always": { - "type": "array", - "items": { - "type": "string" - } - }, - "tool": { - "type": "object", - "properties": { - "messageID": { - "type": "string" - }, - "callID": { - "type": "string" - } - }, - "required": ["messageID", "callID"], - "additionalProperties": false - } - }, - "required": ["id", "sessionID", "permission", "patterns", "metadata", "always"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventPermissionReplied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["permission.replied"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^per" - }, - "reply": { - "type": "string", - "enum": ["once", "always", "reject"] - } - }, - "required": ["sessionID", "requestID", "reply"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventMcpToolsChanged": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["mcp.tools.changed"] - }, - "properties": { - "type": "object", - "properties": { - "server": { - "type": "string" - } - }, - "required": ["server"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventMcpBrowserOpenFailed": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["mcp.browser.open.failed"] - }, - "properties": { - "type": "object", - "properties": { - "mcpName": { - "type": "string" - }, - "url": { - "type": "string" - } - }, - "required": ["mcpName", "url"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventCommandExecuted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["command.executed"] - }, - "properties": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "arguments": { - "type": "string" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - } - }, - "required": ["name", "sessionID", "arguments", "messageID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventProjectUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["project.updated"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "worktree": { - "type": "string" - }, - "vcs": { - "$ref": "#/components/schemas/ProjectVcs" - }, - "name": { - "type": "string" - }, - "icon": { - "$ref": "#/components/schemas/ProjectIcon" - }, - "commands": { - "$ref": "#/components/schemas/ProjectCommands" - }, - "time": { - "$ref": "#/components/schemas/ProjectTime" - }, - "sandboxes": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["id", "worktree", "time", "sandboxes"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionStatus": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.status"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "status": { - "$ref": "#/components/schemas/SessionStatus" - } - }, - "required": ["sessionID", "status"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionIdle": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.idle"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventQuestionAsked": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["question.asked"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^que" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionInfo" - }, - "description": "Questions to ask" - }, - "tool": { - "$ref": "#/components/schemas/QuestionTool" - } - }, - "required": ["id", "sessionID", "questions"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventQuestionReplied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["question.replied"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } - } - }, - "required": ["sessionID", "requestID", "answers"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventQuestionRejected": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["question.rejected"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - } - }, - "required": ["sessionID", "requestID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionCompacted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.compacted"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventVcsBranchUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["vcs.branch.updated"] - }, - "properties": { - "type": "object", - "properties": { - "branch": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventWorkspaceReady": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["workspace.ready"] - }, - "properties": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": ["name"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventWorkspaceFailed": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["workspace.failed"] - }, - "properties": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventWorkspaceStatus": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["workspace.status"] - }, - "properties": { - "type": "object", - "properties": { - "workspaceID": { - "type": "string", - "pattern": "^wrk" - }, - "status": { - "type": "string", - "enum": ["connected", "connecting", "disconnected", "error"] - } - }, - "required": ["workspaceID", "status"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventWorktreeReady": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["worktree.ready"] - }, - "properties": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "branch": { - "type": "string" - } - }, - "required": ["name"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventWorktreeFailed": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["worktree.failed"] - }, - "properties": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventServerConnected": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["server.connected"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventGlobalDisposed": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["global.disposed"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "CredentialOAuth": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["oauth"] - }, - "methodID": { - "type": "string" - }, - "refresh": { - "type": "string" - }, - "access": { - "type": "string" - }, - "expires": { - "type": "integer", - "minimum": 0 - }, - "metadata": { - "type": "object" - } - }, - "required": ["type", "methodID", "refresh", "access", "expires"], - "additionalProperties": false - }, - "CredentialKey": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["key"] - }, - "key": { - "type": "string" - }, - "metadata": { - "type": "object" - } - }, - "required": ["type", "key"], - "additionalProperties": false - }, - "SkillV2DirectorySource": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["directory"] - }, - "path": { - "type": "string" - } - }, - "required": ["type", "path"], - "additionalProperties": false - }, - "SkillV2UrlSource": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["url"] - }, - "url": { - "type": "string" - } - }, - "required": ["type", "url"], - "additionalProperties": false - }, - "SkillV2EmbeddedSource": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["embedded"] - }, - "skill": { - "$ref": "#/components/schemas/SkillV2Info" - } - }, - "required": ["type", "skill"], - "additionalProperties": false - }, - "BadRequestError": { - "type": "object", - "required": ["name", "data"], - "properties": { - "name": { - "type": "string", - "enum": ["BadRequest"] - }, - "data": { - "type": "object", - "required": ["message"], - "properties": { - "message": { - "type": "string" - }, - "kind": { - "type": "string", - "enum": ["Params", "Headers", "Query", "Body", "Payload"] - } - } - } - } - } - } - }, - "security": [], - "tags": [ - { - "name": "control", - "description": "Control plane routes." - }, - { - "name": "controlPlane", - "description": "Control-plane orchestration routes." - }, - { - "name": "global", - "description": "Global server routes." - }, - { - "name": "event", - "description": "Instance event stream route." - }, - { - "name": "config", - "description": "Experimental HttpApi config routes." - }, - { - "name": "experimental", - "description": "Experimental HttpApi read-only routes." - }, - { - "name": "file", - "description": "Experimental HttpApi file routes." - }, - { - "name": "instance", - "description": "Experimental HttpApi instance read routes." - }, - { - "name": "mcp", - "description": "Experimental HttpApi MCP routes." - }, - { - "name": "project", - "description": "Experimental HttpApi project routes." - }, - { - "name": "projectCopy", - "description": "Project copy naming routes." - }, - { - "name": "pty", - "description": "Experimental HttpApi PTY routes." - }, - { - "name": "question", - "description": "Question routes." - }, - { - "name": "permission", - "description": "Experimental HttpApi permission routes." - }, - { - "name": "provider", - "description": "Experimental HttpApi provider routes." - }, - { - "name": "session", - "description": "Experimental HttpApi session routes." - }, - { - "name": "sync", - "description": "Experimental HttpApi sync routes." - }, - { - "name": "tui", - "description": "Experimental HttpApi TUI routes." - }, - { - "name": "workspace", - "description": "Experimental HttpApi workspace routes." - }, - { - "name": "opencode HttpApi", - "description": "Experimental HttpApi surface for selected instance routes." - }, - { - "name": "opencode HttpApi", - "description": "Experimental HttpApi surface for selected instance routes." - }, - { - "name": "opencode HttpApi", - "description": "Experimental HttpApi surface for selected instance routes." - }, - { - "name": "sessions", - "description": "Experimental session routes." - }, - { - "name": "messages", - "description": "Experimental message routes." - }, - { - "name": "models", - "description": "Experimental model routes." - }, - { - "name": "providers", - "description": "Experimental provider routes." - }, - { - "name": "integrations", - "description": "Integration discovery and authentication routes." - }, - { - "name": "opencode HttpApi", - "description": "Experimental HttpApi surface for selected instance routes." - }, - { - "name": "permissions", - "description": "Experimental permission routes." - }, - { - "name": "filesystem", - "description": "Experimental location-scoped filesystem routes." - }, - { - "name": "commands", - "description": "Experimental command routes." - }, - { - "name": "skills", - "description": "Experimental skill routes." - }, - { - "name": "events", - "description": "Experimental event stream route." - }, - { - "name": "pty", - "description": "Experimental location-scoped PTY routes." - }, - { - "name": "session questions", - "description": "Experimental session question routes." - }, - { - "name": "reference", - "description": "Location-scoped project references." - }, - { - "name": "projectCopy", - "description": "Project copy management routes." - }, - { - "name": "pty", - "description": "PTY websocket route." - } - ] -} diff --git a/packages/session-ui/package.json b/packages/session-ui/package.json index 3cf74b043b67..d1dc8ef35b89 100644 --- a/packages/session-ui/package.json +++ b/packages/session-ui/package.json @@ -41,7 +41,7 @@ "@kobalte/core": "catalog:", "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13.tgz", "@opencode-ai/core": "workspace:*", - "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/sdk": "1.18.5", "@opencode-ai/ui": "workspace:*", "@pierre/diffs": "catalog:", "@shikijs/stream": "catalog:", diff --git a/packages/slack/package.json b/packages/slack/package.json index 3dd5746db5f2..525401a53d9f 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -8,7 +8,7 @@ "typecheck": "tsgo --noEmit" }, "dependencies": { - "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/sdk": "1.18.5", "@slack/bolt": "^3.17.1" }, "devDependencies": { diff --git a/packages/www/openapi.json b/packages/www/openapi.json index d430f5d21dd8..1c8b3dc9412d 100644 --- a/packages/www/openapi.json +++ b/packages/www/openapi.json @@ -16,36 +16,11 @@ "security": [], "responses": { "200": { - "description": "Success", + "description": "ServiceHealth", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "healthy": { - "type": "boolean", - "enum": [ - true - ] - }, - "version": { - "type": "string" - }, - "pid": { - "type": "integer", - "allOf": [ - { - "exclusiveMinimum": 0 - } - ] - } - }, - "required": [ - "healthy", - "version", - "pid" - ], - "additionalProperties": false + "$ref": "#/components/schemas/ServiceHealth" } } } @@ -71,10 +46,64 @@ } } }, - "description": "Check whether the API server is ready to accept requests.", + "description": "Report the owning server process and its application status.", "summary": "Check server health" } }, + "/api/service/stop": { + "post": { + "tags": [ + "health" + ], + "operationId": "v2.health.stop", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "ServiceStopResponse", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceStopResponse" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Request graceful shutdown of one exact managed server instance.", + "summary": "Stop the managed server", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceStopRequest" + } + } + }, + "required": true + } + } + }, "/api/server": { "get": { "tags": [ @@ -1406,35 +1435,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "destination": { - "type": "object", - "properties": { - "directory": { - "type": "string" - } - }, - "required": [ - "directory" - ], - "additionalProperties": false - }, - "moveChanges": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "destination" - ], - "additionalProperties": false + "$ref": "#/components/schemas/Location.Ref" } } }, @@ -1473,7 +1474,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/SessionInput.Admitted" + "$ref": "#/components/schemas/SessionPending.User" } }, "required": [ @@ -1562,8 +1563,23 @@ } ] }, - "prompt": { - "$ref": "#/components/schemas/PromptInput" + "text": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PromptInput.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + }, + "metadata": { + "type": "object" }, "delivery": { "anyOf": [ @@ -1591,7 +1607,7 @@ } }, "required": [ - "prompt" + "text" ], "additionalProperties": false } @@ -1632,7 +1648,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/SessionInput.Admitted" + "$ref": "#/components/schemas/SessionPending.User" } }, "required": [ @@ -1953,8 +1969,24 @@ ], "security": [], "responses": { - "204": { - "description": "" + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SessionPending.Synthetic" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } }, "400": { "description": "InvalidRequestError", @@ -1992,9 +2024,19 @@ } } } + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictError" + } + } + } } }, - "description": "Append a synthetic message to a session and resume execution.", + "description": "Durably admit synthetic session input and schedule execution unless resume is false.", "summary": "Add synthetic message", "requestBody": { "content": { @@ -2002,6 +2044,21 @@ "schema": { "type": "object", "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] + }, "text": { "type": "string" }, @@ -2018,6 +2075,20 @@ "metadata": { "type": "object" }, + "delivery": { + "anyOf": [ + { + "type": "string", + "enum": [ + "steer", + "queue" + ] + }, + { + "type": "null" + } + ] + }, "resume": { "anyOf": [ { @@ -2173,7 +2244,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/SessionInput.Compaction" + "$ref": "#/components/schemas/SessionPending.Compaction" } }, "required": [ @@ -2744,6 +2815,93 @@ "summary": "Get session context" } }, + "/api/session/{sessionID}/pending": { + "get": { + "tags": [ + "session" + ], + "operationId": "v2.session.pending.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionPending.Info" + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "List durable admitted session work not yet visible in projected history, ordered by admission. Includes unpromoted user and synthetic inputs and unhandled compaction barriers. The runner owns consumption; items disappear once promoted or handled.", + "summary": "List pending session work" + } + }, "/api/session/{sessionID}/instructions/entries": { "get": { "tags": [ @@ -2901,6 +3059,16 @@ } } } + }, + "413": { + "description": "InstructionEntryValueTooLargeError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstructionEntryValueTooLargeError" + } + } + } } }, "description": "Attach or replace one durable instruction entry. Changes announce as updates at the next step boundary.", @@ -2998,6 +3166,110 @@ "summary": "Remove instruction entry" } }, + "/api/session/{sessionID}/generate": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.generate", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "SessionGenerateResponse", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionGenerateResponse" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Generate transient text from the current session context without mutating session history.", + "summary": "Generate text from session context", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "prompt": { + "type": "string" + } + }, + "required": [ + "prompt" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, "/api/experimental/session/{sessionID}/log": { "get": { "tags": [ @@ -3076,7 +3348,7 @@ "type": "string" }, "data": { - "$ref": "#/components/schemas/SessionLogItemStream" + "$ref": "#/components/schemas/SessionLogItemJsonString" } }, "required": [ @@ -3694,7 +3966,7 @@ } } }, - "description": "Retrieve available models ordered by release date.", + "description": "Retrieve the current snapshot of available models ordered by release date. The snapshot may precede initial plugin settlement.", "summary": "List models" } }, @@ -4395,6 +4667,110 @@ "summary": "Get integration" } }, + "/api/experimental/integration/wellknown": { + "post": { + "tags": [ + "integration" + ], + "operationId": "v2.experimental.integration.wellknown.add", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Discover and persist an experimental wellknown integration source.", + "summary": "Add wellknown integration", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, "/api/integration/{integrationID}/connect/key": { "post": { "tags": [ @@ -4522,7 +4898,7 @@ "tags": [ "integration" ], - "operationId": "v2.integration.connect.oauth", + "operationId": "v2.integration.oauth.connect", "parameters": [ { "name": "integrationID", @@ -4666,13 +5042,21 @@ } } }, - "/api/integration/attempt/{attemptID}": { + "/api/integration/{integrationID}/connect/oauth/{attemptID}": { "get": { "tags": [ "integration" ], - "operationId": "v2.integration.attempt.status", + "operationId": "v2.integration.oauth.status", "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, { "name": "attemptID", "in": "path", @@ -4775,8 +5159,16 @@ "tags": [ "integration" ], - "operationId": "v2.integration.attempt.cancel", + "operationId": "v2.integration.oauth.cancel", "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, { "name": "attemptID", "in": "path", @@ -4856,13 +5248,21 @@ "summary": "Cancel OAuth connection" } }, - "/api/integration/attempt/{attemptID}/complete": { + "/api/integration/{integrationID}/connect/oauth/{attemptID}/complete": { "post": { "tags": [ "integration" ], - "operationId": "v2.integration.attempt.complete", + "operationId": "v2.integration.oauth.complete", "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, { "name": "attemptID", "in": "path", @@ -4972,13 +5372,21 @@ } } }, - "/api/mcp": { - "get": { + "/api/integration/{integrationID}/connect/command": { + "post": { "tags": [ - "mcp" + "integration" ], - "operationId": "v2.mcp.list", + "operationId": "v2.integration.command.connect", "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, { "name": "location", "in": "query", @@ -5033,10 +5441,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Mcp.Server" - } + "$ref": "#/components/schemas/Integration.CommandAttempt" } }, "required": [ @@ -5053,7 +5458,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] } } } @@ -5069,17 +5481,62 @@ } } }, - "description": "Retrieve configured MCP servers and their connection status.", - "summary": "List MCP servers" - } - }, - "/api/mcp/resource": { - "get": { - "tags": [ - "mcp" - ], - "operationId": "v2.mcp.resource.catalog", + "description": "Start a command authentication attempt.", + "summary": "Begin command connection", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "methodID": { + "type": "string" + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "methodID" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/integration/{integrationID}/connect/command/{attemptID}": { + "get": { + "tags": [ + "integration" + ], + "operationId": "v2.integration.command.status", "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "attemptID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, { "name": "location", "in": "query", @@ -5134,7 +5591,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/Mcp.ResourceCatalog" + "$ref": "#/components/schemas/Integration.CommandAttemptStatus" } }, "required": [ @@ -5167,19 +5624,25 @@ } } }, - "description": "Retrieve resources and resource templates from connected MCP servers.", - "summary": "List MCP resources" - } - }, - "/api/credential/{credentialID}": { - "patch": { + "description": "Poll the current status and output of a command authentication attempt.", + "summary": "Get command attempt status" + }, + "delete": { "tags": [ - "credential" + "integration" ], - "operationId": "v2.credential.update", + "operationId": "v2.integration.command.cancel", "parameters": [ { - "name": "credentialID", + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "attemptID", "in": "path", "schema": { "type": "string" @@ -5253,42 +5716,17 @@ } } }, - "description": "Update a stored credential label.", - "summary": "Update credential", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "label": { - "type": "string" - } - }, - "required": [ - "label" - ], - "additionalProperties": false - } - } - }, - "required": true - } - }, - "delete": { + "description": "Cancel a command authentication attempt and terminate its process.", + "summary": "Cancel command connection" + } + }, + "/api/mcp": { + "get": { "tags": [ - "credential" + "mcp" ], - "operationId": "v2.credential.remove", + "operationId": "v2.mcp.list", "parameters": [ - { - "name": "credentialID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, { "name": "location", "in": "query", @@ -5331,53 +5769,29 @@ } ], "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Remove a stored integration credential.", - "summary": "Remove credential" - } - }, - "/api/project": { - "get": { - "tags": [ - "project" - ], - "operationId": "v2.project.list", - "parameters": [], - "security": [], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Project" - } + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Mcp.Server" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false } } } @@ -5403,17 +5817,25 @@ } } }, - "description": "List known projects.", - "summary": "List projects" + "description": "Retrieve configured MCP servers and their connection status.", + "summary": "List MCP servers" } }, - "/api/project/current": { - "get": { + "/api/mcp/{server}": { + "put": { "tags": [ - "project" + "mcp" ], - "operationId": "v2.project.current", + "operationId": "v2.mcp.add", "parameters": [ + { + "name": "server", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, { "name": "location", "in": "query", @@ -5457,15 +5879,8 @@ ], "security": [], "responses": { - "200": { - "description": "Project.Current", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Project.Current" - } - } - } + "204": { + "description": "" }, "400": { "description": "InvalidRequestError", @@ -5488,19 +5903,43 @@ } } }, - "description": "Resolve the project for the requested location.", - "summary": "Get current project" - } - }, - "/api/project/{projectID}/directories": { - "get": { + "description": "Add an MCP server at runtime or replace an existing one, connecting it immediately.", + "summary": "Add MCP server", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "config": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.LocalConfig" + }, + { + "$ref": "#/components/schemas/Mcp.RemoteConfig" + } + ] + } + }, + "required": [ + "config" + ], + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { "tags": [ - "project" + "mcp" ], - "operationId": "v2.project.directories", + "operationId": "v2.mcp.remove", "parameters": [ { - "name": "projectID", + "name": "server", "in": "path", "schema": { "type": "string" @@ -5550,15 +5989,8 @@ ], "security": [], "responses": { - "200": { - "description": "Project.Directories", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Project.Directories" - } - } - } + "204": { + "description": "" }, "400": { "description": "InvalidRequestError", @@ -5579,19 +6011,37 @@ } } } + }, + "404": { + "description": "McpServerNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/McpServerNotFoundError" + } + } + } } }, - "description": "List known local absolute directories for a project.", - "summary": "List project directories" + "description": "Stop an MCP server and remove it from the runtime set until restart.", + "summary": "Remove MCP server" } }, - "/api/form/request": { - "get": { + "/api/mcp/{server}/connect": { + "post": { "tags": [ - "form" + "mcp" ], - "operationId": "v2.form.request.list", + "operationId": "v2.mcp.connect", "parameters": [ + { + "name": "server", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, { "name": "location", "in": "query", @@ -5635,38 +6085,8 @@ ], "security": [], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/Location.Info" - }, - "data": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/Form.FormInfo" - }, - { - "$ref": "#/components/schemas/Form.UrlInfo" - } - ] - } - } - }, - "required": [ - "location", - "data" - ], - "additionalProperties": false - } - } - } + "204": { + "description": "" }, "400": { "description": "InvalidRequestError", @@ -5687,58 +6107,82 @@ } } } + }, + "404": { + "description": "McpServerNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/McpServerNotFoundError" + } + } + } } }, - "description": "Retrieve pending forms for a location.", - "summary": "List pending form requests" + "description": "Connect an MCP server at runtime, overriding a disabled configuration until restart.", + "summary": "Connect MCP server" } }, - "/api/session/{sessionID}/form": { - "get": { + "/api/mcp/{server}/disconnect": { + "post": { "tags": [ - "form" + "mcp" ], - "operationId": "v2.session.form.list", + "operationId": "v2.mcp.disconnect", "parameters": [ { - "name": "sessionID", + "name": "server", "in": "path", "schema": { "type": "string" }, "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { "type": "object", "properties": { - "data": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/Form.FormInfo" - }, - { - "$ref": "#/components/schemas/Form.UrlInfo" - } - ] - } + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] } }, - "required": [ - "data" - ], "additionalProperties": false + }, + { + "type": "null" } - } - } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" }, "400": { "description": "InvalidRequestError", @@ -5761,39 +6205,66 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "McpServerNotFoundError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/McpServerNotFoundError" } } } } }, - "description": "Retrieve pending forms for a session.", - "summary": "List session forms" - }, - "post": { + "description": "Disconnect an MCP server at runtime, removing its tools until reconnected.", + "summary": "Disconnect MCP server" + } + }, + "/api/mcp/resource": { + "get": { "tags": [ - "form" + "mcp" ], - "operationId": "v2.session.form.create", + "operationId": "v2.mcp.resource.catalog", "parameters": [ { - "name": "sessionID", - "in": "path", + "name": "location", + "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": true + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], @@ -5805,18 +6276,15 @@ "schema": { "type": "object", "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, "data": { - "anyOf": [ - { - "$ref": "#/components/schemas/Form.FormInfo" - }, - { - "$ref": "#/components/schemas/Form.UrlInfo" - } - ] + "$ref": "#/components/schemas/Mcp.ResourceCatalog" } }, "required": [ + "location", "data" ], "additionalProperties": false @@ -5829,14 +6297,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestError1" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } @@ -5850,58 +6311,21 @@ } } } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - }, - "409": { - "description": "ConflictError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConflictError" - } - } - } } }, - "description": "Create a form for a session.", - "summary": "Create session form", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Form.CreatePayload" - } - } - }, - "required": true - } + "description": "Retrieve resources and resource templates from connected MCP servers.", + "summary": "List MCP resources" } }, - "/api/session/{sessionID}/form/{formID}": { - "get": { + "/api/credential/{credentialID}": { + "patch": { "tags": [ - "form" + "credential" ], - "operationId": "v2.session.form.get", + "operationId": "v2.credential.update", "parameters": [ { - "name": "sessionID", + "name": "credentialID", "in": "path", "schema": { "type": "string" @@ -5909,46 +6333,50 @@ "required": true }, { - "name": "formID", - "in": "path", + "name": "location", + "in": "query", "schema": { - "type": "string", - "allOf": [ + "anyOf": [ { - "pattern": "^frm_" - } - ] - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { "type": "object", "properties": { - "data": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { "anyOf": [ { - "$ref": "#/components/schemas/Form.FormInfo" + "type": "string" }, { - "$ref": "#/components/schemas/Form.UrlInfo" + "type": "null" } ] } }, - "required": [ - "data" - ], "additionalProperties": false + }, + { + "type": "null" } - } - } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" }, "400": { "description": "InvalidRequestError", @@ -5969,41 +6397,38 @@ } } } - }, - "404": { - "description": "SessionNotFoundError | FormNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } } }, - "description": "Retrieve a form for a session.", - "summary": "Get session form" - } - }, - "/api/session/{sessionID}/form/{formID}/state": { - "get": { + "description": "Update a stored credential label.", + "summary": "Update credential", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "label": { + "type": "string" + } + }, + "required": [ + "label" + ], + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { "tags": [ - "form" + "credential" ], - "operationId": "v2.session.form.state", + "operationId": "v2.credential.remove", "parameters": [ { - "name": "sessionID", + "name": "credentialID", "in": "path", "schema": { "type": "string" @@ -6011,39 +6436,50 @@ "required": true }, { - "name": "formID", - "in": "path", + "name": "location", + "in": "query", "schema": { - "type": "string", - "allOf": [ + "anyOf": [ { - "pattern": "^frm_" + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" } ] }, - "required": true + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/Form.State" - } - }, - "required": [ - "data" - ], - "additionalProperties": false - } - } - } + "204": { + "description": "" }, "400": { "description": "InvalidRequestError", @@ -6064,79 +6500,40 @@ } } } - }, - "404": { - "description": "SessionNotFoundError | FormNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } } }, - "description": "Retrieve the current state for a form.", - "summary": "Get form state" + "description": "Remove a stored integration credential.", + "summary": "Remove credential" } }, - "/api/session/{sessionID}/form/{formID}/reply": { - "post": { + "/api/project": { + "get": { "tags": [ - "form" - ], - "operationId": "v2.session.form.reply", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "formID", - "in": "path", - "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^frm_" - } - ] - }, - "required": true - } + "project" ], + "operationId": "v2.project.list", + "parameters": [], "security": [], "responses": { - "204": { - "description": "" + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Project" + } + } + } + } }, "400": { - "description": "FormInvalidAnswerError | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormInvalidAnswerError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } @@ -6150,148 +6547,112 @@ } } } - }, - "404": { - "description": "SessionNotFoundError | FormNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - }, - "409": { - "description": "FormAlreadySettledError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FormAlreadySettledError" - } - } - } } }, - "description": "Submit an answer to a pending form.", - "summary": "Reply to form", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Form.Reply" - } - } - }, - "required": true - } + "description": "List known projects.", + "summary": "List projects" } }, - "/api/session/{sessionID}/form/{formID}/cancel": { - "post": { + "/api/project/current": { + "get": { "tags": [ - "form" + "project" ], - "operationId": "v2.session.form.cancel", + "operationId": "v2.project.current", "parameters": [ { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "formID", - "in": "path", + "name": "location", + "in": "query", "schema": { - "type": "string", - "allOf": [ + "anyOf": [ { - "pattern": "^frm_" + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" } ] }, - "required": true + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", + "200": { + "description": "Project.Current", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnauthorizedError" + "$ref": "#/components/schemas/Project.Current" } } } }, - "404": { - "description": "SessionNotFoundError | FormNotFoundError", + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "409": { - "description": "FormAlreadySettledError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FormAlreadySettledError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Cancel a pending form.", - "summary": "Cancel form" + "description": "Resolve the project for the requested location.", + "summary": "Get current project" } }, - "/api/permission/request": { + "/api/project/{projectID}/directories": { "get": { "tags": [ - "permission" + "project" ], - "operationId": "v2.permission.request.list", + "operationId": "v2.project.directories", "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, { "name": "location", "in": "query", @@ -6336,27 +6697,11 @@ "security": [], "responses": { "200": { - "description": "Success", + "description": "Project.Directories", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/Location.Info" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionV2.Request" - } - } - }, - "required": [ - "location", - "data" - ], - "additionalProperties": false + "$ref": "#/components/schemas/Project.Directories" } } } @@ -6382,31 +6727,56 @@ } } }, - "description": "Retrieve pending permission requests for a location.", - "summary": "List pending permission requests" + "description": "List known local absolute directories for a project.", + "summary": "List project directories" } }, - "/api/permission/saved": { + "/api/form/request": { "get": { "tags": [ - "permission" + "form" ], - "operationId": "v2.permission.saved.list", + "operationId": "v2.form.request.list", "parameters": [ { - "name": "projectID", + "name": "location", "in": "query", "schema": { "anyOf": [ { - "type": "string" + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false }, { "type": "null" } ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], @@ -6418,14 +6788,18 @@ "schema": { "type": "object", "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, "data": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionSaved.Info" + "$ref": "#/components/schemas/Form.Info" } } }, "required": [ + "location", "data" ], "additionalProperties": false @@ -6454,19 +6828,19 @@ } } }, - "description": "Retrieve saved permissions, optionally filtered by project.", - "summary": "List saved permissions" + "description": "Retrieve pending forms for a location.", + "summary": "List pending form requests" } }, - "/api/permission/saved/{id}": { - "delete": { + "/api/session/{sessionID}/form": { + "get": { "tags": [ - "permission" + "form" ], - "operationId": "v2.permission.saved.remove", + "operationId": "v2.session.form.list", "parameters": [ { - "name": "id", + "name": "sessionID", "in": "path", "schema": { "type": "string" @@ -6476,8 +6850,27 @@ ], "security": [], "responses": { - "204": { - "description": "" + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Info" + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } }, "400": { "description": "InvalidRequestError", @@ -6498,29 +6891,39 @@ } } } - } - }, - "description": "Remove a saved permission by ID.", - "summary": "Remove saved permission" - } - }, - "/api/session/{sessionID}/permission": { + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve pending forms for a session.", + "summary": "List session forms" + }, "post": { "tags": [ - "permission" + "form" ], - "operationId": "v2.session.permission.create", + "operationId": "v2.session.form.create", "parameters": [ { "name": "sessionID", "in": "path", "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] + "type": "string" }, "required": true } @@ -6535,25 +6938,7 @@ "type": "object", "properties": { "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^per" - } - ] - }, - "effect": { - "$ref": "#/components/schemas/PermissionV2.Effect" - } - }, - "required": [ - "id", - "effect" - ], - "additionalProperties": false + "$ref": "#/components/schemas/Form.Info" } }, "required": [ @@ -6569,7 +6954,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] } } } @@ -6600,88 +6992,55 @@ } } } + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictError" + } + } + } } }, - "description": "Evaluate and, when approval is required, create a permission request for a session.", - "summary": "Create permission request", + "description": "Create a form for a session.", + "summary": "Create session form", "requestBody": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "id": { - "anyOf": [ - { - "type": "string", - "allOf": [ - { - "pattern": "^per" - } - ] - }, - { - "type": "null" - } - ] - }, - "action": { - "type": "string" - }, - "resources": { - "type": "array", - "items": { - "type": "string" - } - }, - "save": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "source": { - "$ref": "#/components/schemas/PermissionV2.Source" - }, - "agent": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "action", - "resources" - ], - "additionalProperties": false + "$ref": "#/components/schemas/Form.CreatePayload" } } }, "required": true } - }, + } + }, + "/api/session/{sessionID}/form/{formID}": { "get": { "tags": [ - "permission" + "form" ], - "operationId": "v2.session.permission.list", + "operationId": "v2.session.form.get", "parameters": [ { "name": "sessionID", "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", "schema": { "type": "string", "allOf": [ { - "pattern": "^ses" + "pattern": "^frm_" } ] }, @@ -6698,10 +7057,7 @@ "type": "object", "properties": { "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionV2.Request" - } + "$ref": "#/components/schemas/Form.Info" } }, "required": [ @@ -6733,11 +7089,14 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | FormNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundError" + }, { "$ref": "#/components/schemas/SessionNotFoundError" }, @@ -6750,38 +7109,33 @@ } } }, - "description": "Retrieve pending permission requests owned by a session.", - "summary": "List session permission requests" + "description": "Retrieve a form for a session.", + "summary": "Get session form" } }, - "/api/session/{sessionID}/permission/{requestID}": { + "/api/session/{sessionID}/form/{formID}/state": { "get": { "tags": [ - "permission" + "form" ], - "operationId": "v2.session.permission.get", + "operationId": "v2.session.form.state", "parameters": [ { "name": "sessionID", "in": "path", "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] + "type": "string" }, "required": true }, { - "name": "requestID", + "name": "formID", "in": "path", "schema": { "type": "string", "allOf": [ { - "pattern": "^per" + "pattern": "^frm_" } ] }, @@ -6798,7 +7152,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/PermissionV2.Request" + "$ref": "#/components/schemas/Form.State" } }, "required": [ @@ -6830,13 +7184,13 @@ } }, "404": { - "description": "SessionNotFoundError | PermissionNotFoundError", + "description": "SessionNotFoundError | FormNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/PermissionNotFoundError" + "$ref": "#/components/schemas/FormNotFoundError" }, { "$ref": "#/components/schemas/SessionNotFoundError" @@ -6850,38 +7204,33 @@ } } }, - "description": "Retrieve a pending permission request owned by a session.", - "summary": "Get permission request" + "description": "Retrieve the current state for a form.", + "summary": "Get form state" } }, - "/api/session/{sessionID}/permission/{requestID}/reply": { + "/api/session/{sessionID}/form/{formID}/reply": { "post": { "tags": [ - "permission" + "form" ], - "operationId": "v2.session.permission.reply", + "operationId": "v2.session.form.reply", "parameters": [ { "name": "sessionID", "in": "path", "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] + "type": "string" }, "required": true }, { - "name": "requestID", + "name": "formID", "in": "path", "schema": { "type": "string", "allOf": [ { - "pattern": "^per" + "pattern": "^frm_" } ] }, @@ -6894,11 +7243,18 @@ "description": "" }, "400": { - "description": "InvalidRequestError", + "description": "FormInvalidAnswerError | InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/FormInvalidAnswerError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] } } } @@ -6914,13 +7270,13 @@ } }, "404": { - "description": "SessionNotFoundError | PermissionNotFoundError", + "description": "SessionNotFoundError | FormNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/PermissionNotFoundError" + "$ref": "#/components/schemas/FormNotFoundError" }, { "$ref": "#/components/schemas/SessionNotFoundError" @@ -6932,34 +7288,25 @@ } } } + }, + "409": { + "description": "FormAlreadySettledError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormAlreadySettledError" + } + } + } } }, - "description": "Respond to a pending permission request owned by a session.", - "summary": "Reply to pending permission request", + "description": "Submit an answer to a pending form.", + "summary": "Reply to form", "requestBody": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "reply": { - "$ref": "#/components/schemas/PermissionV2.Reply" - }, - "message": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "reply" - ], - "additionalProperties": false + "$ref": "#/components/schemas/Form.Reply" } } }, @@ -6967,66 +7314,39 @@ } } }, - "/api/fs/read/*": { - "get": { + "/api/session/{sessionID}/form/{formID}/cancel": { + "post": { "tags": [ - "filesystem" + "form" ], - "operationId": "v2.fs.read", + "operationId": "v2.session.form.cancel", "parameters": [ { - "name": "location", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "workspace": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ { - "type": "null" + "pattern": "^frm_" } ] }, - "required": false, - "style": "deepObject", - "explode": true + "required": true } ], "security": [], "responses": { - "200": { - "description": "Success", - "content": { - "application/octet-stream": { - "schema": { - "type": "string", - "format": "binary" - } - } - } + "204": { + "description": "" }, "400": { "description": "InvalidRequestError", @@ -7047,18 +7367,48 @@ } } } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "FormAlreadySettledError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormAlreadySettledError" + } + } + } } }, - "description": "Serve one file relative to the requested location.", - "summary": "Read file" + "description": "Cancel a pending form.", + "summary": "Cancel form" } }, - "/api/fs/list": { + "/api/permission/request": { "get": { "tags": [ - "filesystem" + "permission" ], - "operationId": "v2.fs.list", + "operationId": "v2.permission.request.list", "parameters": [ { "name": "location", @@ -7099,21 +7449,6 @@ "required": false, "style": "deepObject", "explode": true - }, - { - "name": "path", - "in": "query", - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "required": false } ], "security": [], @@ -7131,7 +7466,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/FileSystem.Entry" + "$ref": "#/components/schemas/PermissionV2.Request" } } }, @@ -7165,79 +7500,19 @@ } } }, - "description": "List direct children of one directory relative to the requested location.", - "summary": "List directory" + "description": "Retrieve pending permission requests for a location.", + "summary": "List pending permission requests" } }, - "/api/fs/find": { + "/api/permission/saved": { "get": { "tags": [ - "filesystem" + "permission" ], - "operationId": "v2.fs.find", + "operationId": "v2.permission.saved.list", "parameters": [ { - "name": "location", - "in": "query", - "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "workspace": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - { - "type": "null" - } - ] - }, - "required": false, - "style": "deepObject", - "explode": true - }, - { - "name": "query", - "in": "query", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "type", - "in": "query", - "schema": { - "type": "string", - "enum": [ - "file", - "directory" - ] - }, - "required": false - }, - { - "name": "limit", + "name": "projectID", "in": "query", "schema": { "anyOf": [ @@ -7261,18 +7536,14 @@ "schema": { "type": "object", "properties": { - "location": { - "$ref": "#/components/schemas/Location.Info" - }, "data": { "type": "array", "items": { - "$ref": "#/components/schemas/FileSystem.Entry" + "$ref": "#/components/schemas/PermissionSaved.Info" } } }, "required": [ - "location", "data" ], "additionalProperties": false @@ -7301,56 +7572,75 @@ } } }, - "description": "Find recursively ranked filesystem entries relative to the requested location.", - "summary": "Find files" + "description": "Retrieve saved permissions, optionally filtered by project.", + "summary": "List saved permissions" } }, - "/api/command": { - "get": { + "/api/permission/saved/{id}": { + "delete": { "tags": [ - "command" + "permission" ], - "operationId": "v2.command.list", + "operationId": "v2.permission.saved.remove", "parameters": [ { - "name": "location", - "in": "query", + "name": "id", + "in": "path", "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "workspace": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - { - "type": "null" - } - ] + "type": "string" }, - "required": false, - "style": "deepObject", - "explode": true + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Remove a saved permission by ID.", + "summary": "Remove saved permission" + } + }, + "/api/session/{sessionID}/permission": { + "post": { + "tags": [ + "permission" + ], + "operationId": "v2.session.permission.create", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true } ], "security": [], @@ -7362,18 +7652,29 @@ "schema": { "type": "object", "properties": { - "location": { - "$ref": "#/components/schemas/Location.Info" - }, "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Command.Info" - } + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "effect": { + "$ref": "#/components/schemas/PermissionV2.Effect" + } + }, + "required": [ + "id", + "effect" + ], + "additionalProperties": false } }, "required": [ - "location", "data" ], "additionalProperties": false @@ -7400,58 +7701,109 @@ } } } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } } }, - "description": "Retrieve currently registered commands.", - "summary": "List commands" - } - }, - "/api/skill": { + "description": "Evaluate and, when approval is required, create a permission request for a session.", + "summary": "Create permission request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + { + "type": "null" + } + ] + }, + "action": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "save": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": { + "type": "object" + }, + "source": { + "$ref": "#/components/schemas/PermissionV2.Source" + }, + "agent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "action", + "resources" + ], + "additionalProperties": false + } + } + }, + "required": true + } + }, "get": { "tags": [ - "skill" + "permission" ], - "operationId": "v2.skill.list", + "operationId": "v2.session.permission.list", "parameters": [ { - "name": "location", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "workspace": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, + "type": "string", + "allOf": [ { - "type": "null" + "pattern": "^ses" } ] }, - "required": false, - "style": "deepObject", - "explode": true + "required": true } ], "security": [], @@ -7463,18 +7815,14 @@ "schema": { "type": "object", "properties": { - "location": { - "$ref": "#/components/schemas/Location.Info" - }, "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Skill.Info" + "$ref": "#/components/schemas/PermissionV2.Request" } } }, "required": [ - "location", "data" ], "additionalProperties": false @@ -7501,127 +7849,80 @@ } } } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } } }, - "description": "Retrieve currently registered skills.", - "summary": "List skills" + "description": "Retrieve pending permission requests owned by a session.", + "summary": "List session permission requests" } }, - "/api/event": { + "/api/session/{sessionID}/permission/{requestID}": { "get": { "tags": [ - "event" + "permission" ], - "operationId": "v2.event.subscribe", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "text/event-stream": { - "schema": { - "type": "object", - "properties": { - "id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "event": { - "type": "string" - }, + "operationId": "v2.session.permission.get", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "data": { - "$ref": "#/components/schemas/V2EventStream" + "$ref": "#/components/schemas/PermissionV2.Request" } }, "required": [ - "id", - "event", "data" ], "additionalProperties": false - }, - "x-effect-stream": { - "encoding": "sse", - "causeSchema": { - "type": "array", - "items": { - "anyOf": [ - { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "Fail" - ] - }, - "error": { - "not": {} - } - }, - "required": [ - "_tag", - "error" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "Die" - ] - }, - "defect": {} - }, - "required": [ - "_tag", - "defect" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "Interrupt" - ] - }, - "fiberId": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "_tag", - "fiberId" - ], - "additionalProperties": false - } - ] - } - }, - "errorSchema": { - "not": {} - }, - "failureEvent": "effect/httpapi/stream/failure" } } } @@ -7645,87 +7946,70 @@ } } } + }, + "404": { + "description": "SessionNotFoundError | PermissionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/PermissionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } } }, - "description": "Subscribe to native event payloads for the server. Volatile by contract: a slow consumer overflows and fails the stream, and events during disconnection are missed.", - "summary": "Subscribe to events" + "description": "Retrieve a pending permission request owned by a session.", + "summary": "Get permission request" } }, - "/api/pty": { - "get": { + "/api/session/{sessionID}/permission/{requestID}/reply": { + "post": { "tags": [ - "pty" + "permission" ], - "operationId": "v2.pty.list", + "operationId": "v2.session.permission.reply", "parameters": [ { - "name": "location", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "anyOf": [ + "type": "string", + "allOf": [ { - "type": "object", - "properties": { - "directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "workspace": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ { - "type": "null" + "pattern": "^per" } ] }, - "required": false, - "style": "deepObject", - "explode": true + "required": true } ], "security": [], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/Location.Info" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pty" - } - } - }, - "required": [ - "location", - "data" - ], - "additionalProperties": false - } - } - } + "204": { + "description": "" }, "400": { "description": "InvalidRequestError", @@ -7746,16 +8030,67 @@ } } } + }, + "404": { + "description": "SessionNotFoundError | PermissionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/PermissionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } } }, - "description": "List PTY sessions for a location, including exited sessions retained until removal.", - "summary": "List PTY sessions" - }, - "post": { + "description": "Respond to a pending permission request owned by a session.", + "summary": "Reply to pending permission request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "reply": { + "$ref": "#/components/schemas/PermissionV2.Reply" + }, + "message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "reply" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/fs/read/*": { + "get": { "tags": [ - "pty" + "filesystem" ], - "operationId": "v2.pty.create", + "operationId": "v2.fs.read", "parameters": [ { "name": "location", @@ -7803,22 +8138,10 @@ "200": { "description": "Success", "content": { - "application/json": { + "application/octet-stream": { "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/Location.Info" - }, - "data": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": [ - "location", - "data" - ], - "additionalProperties": false + "type": "string", + "format": "binary" } } } @@ -7844,64 +8167,17 @@ } } }, - "description": "Create a pseudo-terminal session for a location.", - "summary": "Create PTY session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "command": { - "type": "string" - }, - "args": { - "type": "array", - "items": { - "type": "string" - } - }, - "cwd": { - "type": "string" - }, - "title": { - "type": "string" - }, - "env": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "additionalProperties": false - } - } - }, - "required": true - } + "description": "Serve one file relative to the requested location.", + "summary": "Read file" } }, - "/api/pty/{ptyID}": { + "/api/fs/list": { "get": { "tags": [ - "pty" + "filesystem" ], - "operationId": "v2.pty.get", + "operationId": "v2.fs.list", "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^pty" - } - ] - }, - "required": true - }, { "name": "location", "in": "query", @@ -7941,6 +8217,21 @@ "required": false, "style": "deepObject", "explode": true + }, + { + "name": "path", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false } ], "security": [], @@ -7956,7 +8247,10 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/Pty" + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystem.Entry" + } } }, "required": [ @@ -7987,40 +8281,19 @@ } } } - }, - "404": { - "description": "PtyNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } } }, - "description": "Get one PTY session, including its exit code once exited.", - "summary": "Get PTY session" - }, - "put": { + "description": "List direct children of one directory relative to the requested location.", + "summary": "List directory" + } + }, + "/api/fs/find": { + "get": { "tags": [ - "pty" + "filesystem" ], - "operationId": "v2.pty.update", + "operationId": "v2.fs.find", "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^pty" - } - ] - }, - "required": true - }, { "name": "location", "in": "query", @@ -8060,6 +8333,41 @@ "required": false, "style": "deepObject", "explode": true + }, + { + "name": "query", + "in": "query", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "file", + "directory" + ] + }, + "required": false + }, + { + "name": "limit", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false } ], "security": [], @@ -8075,7 +8383,10 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/Pty" + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystem.Entry" + } } }, "required": [ @@ -8106,82 +8417,19 @@ } } } - }, - "404": { - "description": "PtyNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } } }, - "description": "Update the title or viewport size of one PTY session.", - "summary": "Update PTY session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "size": { - "type": "object", - "properties": { - "rows": { - "type": "integer", - "allOf": [ - { - "exclusiveMinimum": 0 - } - ] - }, - "cols": { - "type": "integer", - "allOf": [ - { - "exclusiveMinimum": 0 - } - ] - } - }, - "required": [ - "rows", - "cols" - ], - "additionalProperties": false - } - }, - "additionalProperties": false - } - } - }, - "required": true - } - }, - "delete": { + "description": "Find recursively ranked filesystem entries relative to the requested location.", + "summary": "Find files" + } + }, + "/api/command": { + "get": { "tags": [ - "pty" + "command" ], - "operationId": "v2.pty.remove", + "operationId": "v2.command.list", "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^pty" - } - ] - }, - "required": true - }, { "name": "location", "in": "query", @@ -8225,64 +8473,64 @@ ], "security": [], "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", + "200": { + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidRequestError" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Command.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false } } } }, - "401": { - "description": "UnauthorizedError", + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnauthorizedError" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "404": { - "description": "PtyNotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Terminate and remove one PTY session.", - "summary": "Remove PTY session" + "description": "Retrieve currently registered commands.", + "summary": "List commands" } }, - "/api/pty/{ptyID}/connect-token": { - "post": { + "/api/skill": { + "get": { "tags": [ - "pty" + "skill" ], - "operationId": "v2.pty.connect.token", + "operationId": "v2.skill.list", "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^pty" - } - ] - }, - "required": true - }, { "name": "location", "in": "query", @@ -8337,7 +8585,10 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/PtyTicket.ConnectToken" + "type": "array", + "items": { + "$ref": "#/components/schemas/Skill.Info" + } } }, "required": [ @@ -8368,151 +8619,168 @@ } } } + } + }, + "description": "Retrieve currently registered skills.", + "summary": "List skills" + } + }, + "/api/event": { + "get": { + "tags": [ + "event" + ], + "operationId": "v2.event.subscribe", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "text/event-stream": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "event": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/V2EventJsonString" + } + }, + "required": [ + "id", + "event", + "data" + ], + "additionalProperties": false + }, + "x-effect-stream": { + "encoding": "sse", + "causeSchema": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Fail" + ] + }, + "error": { + "not": {} + } + }, + "required": [ + "_tag", + "error" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Die" + ] + }, + "defect": {} + }, + "required": [ + "_tag", + "defect" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Interrupt" + ] + }, + "fiberId": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "fiberId" + ], + "additionalProperties": false + } + ] + } + }, + "errorSchema": { + "not": {} + }, + "failureEvent": "effect/httpapi/stream/failure" + } + } + } }, - "403": { - "description": "ForbiddenError", + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ForbiddenError" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "404": { - "description": "PtyNotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Create a short-lived single-use ticket for opening a PTY WebSocket connection.", - "summary": "Create PTY WebSocket token" + "description": "Subscribe to native event payloads for the server. Volatile by contract: a slow consumer overflows and fails the stream, and events during disconnection are missed.", + "summary": "Subscribe to events" } }, - "/api/pty/{ptyID}/connect": { + "/api/pty": { "get": { "tags": [ "pty" ], - "operationId": "v2.pty.connect", + "operationId": "v2.pty.list", "parameters": [ { - "name": "ptyID", - "in": "path", + "name": "location", + "in": "query", "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^pty" - } - ] - }, - "required": true - }, - { - "in": "query", - "name": "location[directory]", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "location[workspace]", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "cursor", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "ticket", - "schema": { - "type": "string" - } - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "403": { - "description": "ForbiddenError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ForbiddenError" - } - } - } - }, - "404": { - "description": "PtyNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } - } - }, - "description": "Establish a WebSocket connection streaming PTY output and accepting terminal input.", - "summary": "Connect to PTY session", - "x-websocket": true - } - }, - "/api/shell": { - "get": { - "tags": [ - "shell" - ], - "operationId": "v2.shell.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "anyOf": [ + "anyOf": [ { "type": "object", "properties": { @@ -8564,7 +8832,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Shell1" + "$ref": "#/components/schemas/Pty" } } }, @@ -8598,14 +8866,14 @@ } } }, - "description": "List currently running shell commands for a location. Exited commands are not included.", - "summary": "List running shell commands" + "description": "List PTY sessions for a location, including exited sessions retained until removal.", + "summary": "List PTY sessions" }, "post": { "tags": [ - "shell" + "pty" ], - "operationId": "v2.shell.create", + "operationId": "v2.pty.create", "parameters": [ { "name": "location", @@ -8661,7 +8929,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/Shell1" + "$ref": "#/components/schemas/Pty" } }, "required": [ @@ -8694,8 +8962,8 @@ } } }, - "description": "Spawn one non-interactive shell command for a location. Combined stdout/stderr is captured to a file pageable via output.", - "summary": "Run shell command", + "description": "Create a pseudo-terminal session for a location.", + "summary": "Create PTY session", "requestBody": { "content": { "application/json": { @@ -8705,25 +8973,25 @@ "command": { "type": "string" }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, "cwd": { "type": "string" }, - "timeout": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] + "title": { + "type": "string" }, - "metadata": { - "type": "object" + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } } }, - "required": [ - "command", - "timeout" - ], "additionalProperties": false } } @@ -8732,21 +9000,21 @@ } } }, - "/api/shell/{id}": { + "/api/pty/{ptyID}": { "get": { "tags": [ - "shell" + "pty" ], - "operationId": "v2.shell.get", + "operationId": "v2.pty.get", "parameters": [ { - "name": "id", + "name": "ptyID", "in": "path", "schema": { "type": "string", "allOf": [ { - "pattern": "^sh_" + "pattern": "^pty" } ] }, @@ -8806,7 +9074,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/Shell1" + "$ref": "#/components/schemas/Pty" } }, "required": [ @@ -8839,33 +9107,33 @@ } }, "404": { - "description": "ShellNotFoundError", + "description": "PtyNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ShellNotFoundError" + "$ref": "#/components/schemas/PtyNotFoundError" } } } } }, - "description": "Get one shell command, including its status and exit code once exited.", - "summary": "Get shell command" + "description": "Get one PTY session, including its exit code once exited.", + "summary": "Get PTY session" }, - "delete": { + "put": { "tags": [ - "shell" + "pty" ], - "operationId": "v2.shell.remove", + "operationId": "v2.pty.update", "parameters": [ { - "name": "id", + "name": "ptyID", "in": "path", "schema": { "type": "string", "allOf": [ { - "pattern": "^sh_" + "pattern": "^pty" } ] }, @@ -8914,8 +9182,28 @@ ], "security": [], "responses": { - "204": { - "description": "" + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Pty" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } }, "400": { "description": "InvalidRequestError", @@ -8938,35 +9226,75 @@ } }, "404": { - "description": "ShellNotFoundError", + "description": "PtyNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ShellNotFoundError" + "$ref": "#/components/schemas/PtyNotFoundError" } } } } }, - "description": "Terminate and remove one shell command and its retained output.", - "summary": "Remove shell command" - } - }, - "/api/shell/{id}/timeout": { - "patch": { + "description": "Update the title or viewport size of one PTY session.", + "summary": "Update PTY session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "size": { + "type": "object", + "properties": { + "rows": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "cols": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + } + }, + "required": [ + "rows", + "cols" + ], + "additionalProperties": false + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { "tags": [ - "shell" + "pty" ], - "operationId": "v2.shell.timeout", + "operationId": "v2.pty.remove", "parameters": [ { - "name": "id", + "name": "ptyID", "in": "path", "schema": { "type": "string", "allOf": [ { - "pattern": "^sh_" + "pattern": "^pty" } ] }, @@ -9015,28 +9343,8 @@ ], "security": [], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/Location.Info" - }, - "data": { - "$ref": "#/components/schemas/Shell1" - } - }, - "required": [ - "location", - "data" - ], - "additionalProperties": false - } - } - } + "204": { + "description": "" }, "400": { "description": "InvalidRequestError", @@ -9059,59 +9367,35 @@ } }, "404": { - "description": "ShellNotFoundError", + "description": "PtyNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ShellNotFoundError" + "$ref": "#/components/schemas/PtyNotFoundError" } } } } }, - "description": "Replace a running shell command's timeout from now, or clear it with zero.", - "summary": "Update shell timeout", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "timeout": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - } - }, - "required": [ - "timeout" - ], - "additionalProperties": false - } - } - }, - "required": true - } + "description": "Terminate and remove one PTY session.", + "summary": "Remove PTY session" } }, - "/api/shell/{id}/output": { - "get": { + "/api/pty/{ptyID}/connect-token": { + "post": { "tags": [ - "shell" + "pty" ], - "operationId": "v2.shell.output", + "operationId": "v2.pty.connect.token", "parameters": [ { - "name": "id", + "name": "ptyID", "in": "path", "schema": { "type": "string", "allOf": [ { - "pattern": "^sh_" + "pattern": "^pty" } ] }, @@ -9156,32 +9440,6 @@ "required": false, "style": "deepObject", "explode": true - }, - { - "name": "cursor", - "in": "query", - "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" - } - ] - }, - "required": false - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" - } - ] - }, - "required": false } ], "security": [], @@ -9197,38 +9455,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "type": "object", - "properties": { - "output": { - "type": "string" - }, - "cursor": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "size": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "truncated": { - "type": "boolean" - } - }, - "required": [ - "output", - "cursor", - "size", - "truncated" - ], - "additionalProperties": false + "$ref": "#/components/schemas/PtyTicket.ConnectToken" } }, "required": [ @@ -9260,67 +9487,78 @@ } } }, + "403": { + "description": "ForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, "404": { - "description": "ShellNotFoundError", + "description": "PtyNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ShellNotFoundError" + "$ref": "#/components/schemas/PtyNotFoundError" } } } } }, - "description": "Page through captured combined output by absolute byte cursor.", - "summary": "Read shell output" + "description": "Create a short-lived single-use ticket for opening a PTY WebSocket connection.", + "summary": "Create PTY WebSocket token" } }, - "/api/question/request": { + "/api/pty/{ptyID}/connect": { "get": { "tags": [ - "question" + "pty" ], - "operationId": "v2.question.request.list", + "operationId": "v2.pty.connect", "parameters": [ { - "name": "location", - "in": "query", + "name": "ptyID", + "in": "path", "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "workspace": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, + "type": "string", + "allOf": [ { - "type": "null" + "pattern": "^pty" } ] }, - "required": false, - "style": "deepObject", - "explode": true + "required": true + }, + { + "in": "query", + "name": "location[directory]", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "location[workspace]", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "cursor", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "ticket", + "schema": { + "type": "string" + } } ], "security": [], @@ -9330,23 +9568,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/Location.Info" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2.Request" - } - } - }, - "required": [ - "location", - "data" - ], - "additionalProperties": false + "type": "boolean" } } } @@ -9370,31 +9592,79 @@ } } } - } - }, - "description": "Retrieve pending question requests for a location.", - "summary": "List pending question requests" - } - }, - "/api/session/{sessionID}/question": { - "get": { - "tags": [ - "question" - ], - "operationId": "v2.session.question.list", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "allOf": [ + }, + "403": { + "description": "ForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, + "404": { + "description": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } + } + }, + "description": "Establish a WebSocket connection streaming PTY output and accepting terminal input.", + "summary": "Connect to PTY session", + "x-websocket": true + } + }, + "/api/shell": { + "get": { + "tags": [ + "shell" + ], + "operationId": "v2.shell.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ { - "pattern": "^ses" + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" } ] }, - "required": true + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], @@ -9406,14 +9676,18 @@ "schema": { "type": "object", "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, "data": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Request" + "$ref": "#/components/schemas/Shell.Info1" } } }, "required": [ + "location", "data" ], "additionalProperties": false @@ -9440,116 +9714,135 @@ } } } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } } }, - "description": "Retrieve pending question requests owned by a session.", - "summary": "List session question requests" - } - }, - "/api/session/{sessionID}/question/{requestID}/reply": { + "description": "List currently running shell commands for a location. Exited commands are not included.", + "summary": "List running shell commands" + }, "post": { "tags": [ - "question" + "shell" ], - "operationId": "v2.session.question.reply", + "operationId": "v2.shell.create", "parameters": [ { - "name": "sessionID", - "in": "path", + "name": "location", + "in": "query", "schema": { - "type": "string", - "allOf": [ + "anyOf": [ { - "pattern": "^ses" - } - ] - }, - "required": true - }, - { - "name": "requestID", - "in": "path", - "schema": { - "type": "string", - "allOf": [ + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, { - "pattern": "^que" + "type": "null" } ] }, - "required": true + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", + "200": { + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidRequestError" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Shell.Info1" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false } } } }, - "401": { - "description": "UnauthorizedError", + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnauthorizedError" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "404": { - "description": "SessionNotFoundError | QuestionNotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/QuestionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Answer a pending question request owned by a session.", - "summary": "Reply to pending question request", + "description": "Spawn one non-interactive shell command for a location. Combined stdout/stderr is captured to a file pageable via output.", + "summary": "Run shell command", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QuestionV2.Reply" + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "timeout": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "command", + "timeout" + ], + "additionalProperties": false } } }, @@ -9557,102 +9850,31 @@ } } }, - "/api/session/{sessionID}/question/{requestID}/reject": { - "post": { + "/api/shell/{id}": { + "get": { "tags": [ - "question" + "shell" ], - "operationId": "v2.session.question.reject", + "operationId": "v2.shell.get", "parameters": [ { - "name": "sessionID", + "name": "id", "in": "path", "schema": { "type": "string", "allOf": [ { - "pattern": "^ses" + "pattern": "^sh_" } ] }, "required": true }, { - "name": "requestID", - "in": "path", + "name": "location", + "in": "query", "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^que" - } - ] - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | QuestionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/QuestionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Reject a pending question request owned by a session.", - "summary": "Reject pending question request" - } - }, - "/api/reference": { - "get": { - "tags": [ - "reference" - ], - "operationId": "v2.reference.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "anyOf": [ + "anyOf": [ { "type": "object", "properties": { @@ -9702,10 +9924,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Reference.Info" - } + "$ref": "#/components/schemas/Shell.Info1" } }, "required": [ @@ -9736,24 +9955,37 @@ } } } + }, + "404": { + "description": "ShellNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShellNotFoundError" + } + } + } } }, - "description": "List references available in the requested location.", - "summary": "List references" - } - }, - "/experimental/project/{projectID}/copy": { - "post": { + "description": "Get one shell command, including its status and exit code once exited.", + "summary": "Get shell command" + }, + "delete": { "tags": [ - "projectCopy" + "shell" ], - "operationId": "v2.projectCopy.create", + "operationId": "v2.shell.remove", "parameters": [ { - "name": "projectID", + "name": "id", "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] }, "required": true }, @@ -9800,82 +10032,61 @@ ], "security": [], "responses": { - "200": { - "description": "ProjectCopy.Copy", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProjectCopy.Copy" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "ProjectCopyError | InvalidRequestError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProjectCopyError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/UnauthorizedError" } } } }, - "401": { - "description": "UnauthorizedError", + "404": { + "description": "ShellNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnauthorizedError" + "$ref": "#/components/schemas/ShellNotFoundError" } } } } }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "strategy": { - "type": "string" - }, - "directory": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": [ - "strategy", - "directory" - ], - "additionalProperties": false - } - } - }, - "required": true - } - }, - "delete": { + "description": "Terminate and remove one shell command and its retained output.", + "summary": "Remove shell command" + } + }, + "/api/shell/{id}/timeout": { + "patch": { "tags": [ - "projectCopy" + "shell" ], - "operationId": "v2.projectCopy.remove", + "operationId": "v2.shell.timeout", "parameters": [ { - "name": "projectID", + "name": "id", "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] }, "required": true }, @@ -9922,22 +10133,35 @@ ], "security": [], "responses": { - "204": { - "description": "" - }, - "400": { - "description": "ProjectCopyError | InvalidRequestError", + "200": { + "description": "Success", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProjectCopyError" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" }, - { - "$ref": "#/components/schemas/InvalidRequestError" + "data": { + "$ref": "#/components/schemas/Shell.Info1" } - ] + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" } } } @@ -9951,24 +10175,37 @@ } } } + }, + "404": { + "description": "ShellNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShellNotFoundError" + } + } + } } }, + "description": "Replace a running shell command's timeout from now, or clear it with zero.", + "summary": "Update shell timeout", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "directory": { - "type": "string" - }, - "force": { - "type": "boolean" + "timeout": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] } }, "required": [ - "directory", - "force" + "timeout" ], "additionalProperties": false } @@ -9978,18 +10215,23 @@ } } }, - "/experimental/project/{projectID}/copy/refresh": { - "post": { + "/api/shell/{id}/output": { + "get": { "tags": [ - "projectCopy" + "shell" ], - "operationId": "v2.projectCopy.refresh", + "operationId": "v2.shell.output", "parameters": [ { - "name": "projectID", + "name": "id", "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] }, "required": true }, @@ -10032,89 +10274,32 @@ "required": false, "style": "deepObject", "explode": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" }, - "400": { - "description": "ProjectCopyError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProjectCopyError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + { + "name": "cursor", + "in": "query", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" } - } - } + ] + }, + "required": false }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - } - } - }, - "/api/vcs/status": { - "get": { - "tags": [ - "vcs" - ], - "operationId": "v2.vcs.status", - "parameters": [ { - "name": "location", + "name": "limit", "in": "query", "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "workspace": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, + "type": "string", + "allOf": [ { - "type": "null" + "pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" } ] }, - "required": false, - "style": "deepObject", - "explode": true + "required": false } ], "security": [], @@ -10130,10 +10315,38 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Vcs.FileStatus" - } + "type": "object", + "properties": { + "output": { + "type": "string" + }, + "cursor": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "size": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "truncated": { + "type": "boolean" + } + }, + "required": [ + "output", + "cursor", + "size", + "truncated" + ], + "additionalProperties": false } }, "required": [ @@ -10164,18 +10377,28 @@ } } } + }, + "404": { + "description": "ShellNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShellNotFoundError" + } + } + } } }, - "description": "List uncommitted working-copy changes relative to the requested location.", - "summary": "VCS status" + "description": "Page through captured combined output by absolute byte cursor.", + "summary": "Read shell output" } }, - "/api/vcs/diff": { + "/api/question/request": { "get": { "tags": [ - "vcs" + "question" ], - "operationId": "v2.vcs.diff", + "operationId": "v2.question.request.list", "parameters": [ { "name": "location", @@ -10216,29 +10439,6 @@ "required": false, "style": "deepObject", "explode": true - }, - { - "name": "mode", - "in": "query", - "schema": { - "$ref": "#/components/schemas/Vcs.Mode" - }, - "required": true - }, - { - "name": "context", - "in": "query", - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "required": false } ], "security": [], @@ -10256,7 +10456,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/FileDiff.Info" + "$ref": "#/components/schemas/QuestionV2.Request" } } }, @@ -10290,17 +10490,31 @@ } } }, - "description": "Diff the working copy against HEAD (mode git) or the default-branch merge base (mode branch) for the requested location.", - "summary": "VCS diff" + "description": "Retrieve pending question requests for a location.", + "summary": "List pending question requests" } }, - "/api/debug/location": { + "/api/session/{sessionID}/question": { "get": { "tags": [ - "debug" + "question" + ], + "operationId": "v2.session.question.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } ], - "operationId": "v2.debug.location.list", - "parameters": [], "security": [], "responses": { "200": { @@ -10308,10 +10522,19 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Location.Ref" - } + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Request" + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false } } } @@ -10335,31 +10558,228 @@ } } } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } } }, - "description": "List locations currently loaded by the server.", - "summary": "List loaded locations" - }, - "delete": { + "description": "Retrieve pending question requests owned by a session.", + "summary": "List session question requests" + } + }, + "/api/session/{sessionID}/question/{requestID}/reply": { + "post": { "tags": [ - "debug" + "question" ], - "operationId": "v2.debug.location.evict", + "operationId": "v2.session.question.reply", "parameters": [ { - "name": "location", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "anyOf": [ + "type": "string", + "allOf": [ { - "type": "object", - "properties": { - "directory": { - "anyOf": [ - { - "type": "string" - }, - { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | QuestionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Answer a pending question request owned by a session.", + "summary": "Reply to pending question request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QuestionV2.Reply" + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/question/{requestID}/reject": { + "post": { + "tags": [ + "question" + ], + "operationId": "v2.session.question.reject", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | QuestionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Reject a pending question request owned by a session.", + "summary": "Reject pending question request" + } + }, + "/api/reference": { + "get": { + "tags": [ + "reference" + ], + "operationId": "v2.reference.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { "type": "null" } ] @@ -10389,8 +10809,31 @@ ], "security": [], "responses": { - "204": { - "description": "" + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Reference.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } }, "400": { "description": "InvalidRequestError", @@ -10413,914 +10856,1006 @@ } } }, - "description": "Dispose the requested location's cached services so its next use boots them fresh.", - "summary": "Evict a loaded location" + "description": "List references available in the requested location.", + "summary": "List references" } - } - }, - "components": { - "schemas": { - "UnauthorizedError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "UnauthorizedError" - ] - }, - "message": { - "type": "string" - } - }, - "required": [ - "_tag", - "message" + }, + "/experimental/project/{projectID}/copy": { + "post": { + "tags": [ + "projectCopy" ], - "additionalProperties": false - }, - "InvalidRequestError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "InvalidRequestError" - ] - }, - "message": { - "type": "string" - }, - "kind": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "operationId": "v2.projectCopy.create", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true }, - "field": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true } - }, - "required": [ - "_tag", - "message" ], - "additionalProperties": false - }, - "Location.Info": { - "type": "object", - "properties": { - "directory": { - "type": "string" + "security": [], + "responses": { + "200": { + "description": "ProjectCopy.Copy", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectCopy.Copy" + } + } + } }, - "workspaceID": { - "type": "string", - "allOf": [ - { - "pattern": "^wrk" + "400": { + "description": "ProjectCopyError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } } - ] + } }, - "project": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "directory": { - "type": "string" + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } } - }, - "required": [ - "id", - "directory" - ], - "additionalProperties": false + } } }, - "required": [ - "directory", - "project" - ], - "additionalProperties": false - }, - "Model.Ref": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "providerID": { - "type": "string" + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "strategy": { + "type": "string" + }, + "directory": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "strategy", + "directory" + ], + "additionalProperties": false + } + } }, - "variant": { - "type": "string" - } - }, - "required": [ - "id", - "providerID" - ], - "additionalProperties": false - }, - "Provider.Settings": { - "type": "object" + "required": true + } }, - "Provider.Request": { - "type": "object", - "properties": { - "settings": { - "$ref": "#/components/schemas/Provider.Settings" - }, - "headers": { - "type": "object", - "additionalProperties": { + "delete": { + "tags": [ + "projectCopy" + ], + "operationId": "v2.projectCopy.remove", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { "type": "string" - } + }, + "required": true }, - "body": { - "type": "object" - } - }, - "required": [ - "settings", - "headers", - "body" - ], - "additionalProperties": false - }, - "Agent.Color": { - "type": "string", - "allOf": [ { - "pattern": "^#[0-9a-fA-F]{6}$" + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true } - ] - }, - "PermissionV2.Effect": { - "type": "string", - "enum": [ - "allow", - "deny", - "ask" - ] - }, - "PermissionV2.Rule": { - "type": "object", - "properties": { - "action": { - "type": "string" - }, - "resource": { - "type": "string" - }, - "effect": { - "$ref": "#/components/schemas/PermissionV2.Effect" - } - }, - "required": [ - "action", - "resource", - "effect" ], - "additionalProperties": false - }, - "PermissionV2.Ruleset": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionV2.Rule" - } - }, - "Agent.Info": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/Model.Ref" - }, - "request": { - "$ref": "#/components/schemas/Provider.Request" - }, - "system": { - "type": "string" - }, - "description": { - "type": "string" - }, - "mode": { - "type": "string", - "enum": [ - "subagent", - "primary", - "all" - ] - }, - "hidden": { - "type": "boolean" - }, - "color": { - "$ref": "#/components/schemas/Agent.Color" + "security": [], + "responses": { + "204": { + "description": "" }, - "steps": { - "type": "integer", - "allOf": [ - { - "exclusiveMinimum": 0 + "400": { + "description": "ProjectCopyError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } } - ] + } }, - "permissions": { - "$ref": "#/components/schemas/PermissionV2.Ruleset" + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } } }, - "required": [ - "id", - "name", - "request", - "mode", - "hidden", - "permissions" + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "force": { + "type": "boolean" + } + }, + "required": [ + "directory", + "force" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/experimental/project/{projectID}/copy/refresh": { + "post": { + "tags": [ + "projectCopy" ], - "additionalProperties": false - }, - "Plugin.Info": { - "type": "object", - "properties": { - "id": { - "type": "string" + "operationId": "v2.projectCopy.refresh", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true } - }, - "required": [ - "id" ], - "additionalProperties": false - }, - "Money.USD": { - "type": "number" - }, - "TokenUsage.Info": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" + "security": [], + "responses": { + "204": { + "description": "" }, - "reasoning": { - "type": "number" + "400": { + "description": "ProjectCopyError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } } - }, - "required": [ - "read", - "write" - ], - "additionalProperties": false + } } - }, - "required": [ - "input", - "output", - "reasoning", - "cache" + } + } + }, + "/api/vcs/status": { + "get": { + "tags": [ + "vcs" ], - "additionalProperties": false - }, - "Location.Ref": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspaceID": { - "type": "string", - "allOf": [ - { - "pattern": "^wrk" - } - ] + "operationId": "v2.vcs.status", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true } - }, - "required": [ - "directory" ], - "additionalProperties": false - }, - "FileDiff.Info": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "patch": { - "type": "string" - }, - "additions": { - "type": "integer", - "allOf": [ - { - "minimum": 0 + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Vcs.FileStatus" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } } - ] + } }, - "deletions": { - "type": "integer", - "allOf": [ - { - "minimum": 0 + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } } - ] + } }, - "status": { - "type": "string", - "enum": [ - "added", - "deleted", - "modified" - ] - } - }, - "required": [ - "file", - "patch", - "additions", - "deletions", - "status" - ], - "additionalProperties": false - }, - "Session.Revert": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } } - ] - }, - "partID": { - "type": "string" - }, - "snapshot": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileDiff.Info" } } }, - "required": [ - "messageID" + "description": "List uncommitted working-copy changes relative to the requested location.", + "summary": "VCS status" + } + }, + "/api/vcs/diff": { + "get": { + "tags": [ + "vcs" ], - "additionalProperties": false - }, - "Session.Info": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] + "operationId": "v2.vcs.diff", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true }, - "parentID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] + { + "name": "mode", + "in": "query", + "schema": { + "$ref": "#/components/schemas/Vcs.Mode" + }, + "required": true }, - "fork": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "messageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - } - }, - "required": [ - "sessionID" - ], - "additionalProperties": false - }, - "projectID": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/Model.Ref" - }, - "cost": { - "$ref": "#/components/schemas/Money.USD" - }, - "tokens": { - "$ref": "#/components/schemas/TokenUsage.Info" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - }, - "updated": { - "type": "number" - }, - "archived": { - "type": "number" - } + { + "name": "context", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, - "required": [ - "created", - "updated" - ], - "additionalProperties": false - }, - "title": { - "type": "string" - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "subpath": { - "type": "string" - }, - "revert": { - "$ref": "#/components/schemas/Session.Revert" + "required": false } - }, - "required": [ - "id", - "projectID", - "cost", - "tokens", - "time", - "title", - "location" ], - "additionalProperties": false - }, - "SessionsResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Session.Info" + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileDiff.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } } }, - "cursor": { - "type": "object", - "properties": { - "previous": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "next": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } } - }, - "additionalProperties": false - } - }, - "required": [ - "data", - "cursor" - ], - "additionalProperties": false - }, - "InvalidCursorError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "InvalidCursorError" - ] + } }, - "message": { - "type": "string" + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } } }, - "required": [ - "_tag", - "message" + "description": "Diff the working copy against HEAD (mode git) or the default-branch merge base (mode branch) for the requested location.", + "summary": "VCS diff" + } + }, + "/api/debug/location": { + "get": { + "tags": [ + "debug" ], - "additionalProperties": false - }, - "InvalidRequestError1": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "InvalidRequestError" - ] - }, - "message": { - "type": "string" + "operationId": "v2.debug.location.list", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Location.Ref" + } + } + } + } }, - "kind": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } } - ] + } }, - "field": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } } - ] + } } }, - "required": [ - "_tag", - "message" - ], - "additionalProperties": false + "description": "List locations currently loaded by the server.", + "summary": "List loaded locations" }, - "SessionActive": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "running" - ] - } - }, - "required": [ - "type" + "delete": { + "tags": [ + "debug" ], - "additionalProperties": false - }, - "SessionNotFoundError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "SessionNotFoundError" - ] - }, - "sessionID": { - "type": "string" - }, - "message": { - "type": "string" + "operationId": "v2.debug.location.evict", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true } - }, - "required": [ - "_tag", - "sessionID", - "message" ], - "additionalProperties": false - }, - "MessageNotFoundError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "MessageNotFoundError" - ] - }, - "sessionID": { - "type": "string" + "security": [], + "responses": { + "204": { + "description": "" }, - "messageID": { - "type": "string" + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } }, - "message": { - "type": "string" + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } } }, - "required": [ - "_tag", - "sessionID", - "messageID", - "message" + "description": "Dispose the requested location's cached services so its next use boots them fresh.", + "summary": "Evict a loaded location" + } + }, + "/api/websearch/provider": { + "get": { + "tags": [ + "websearch" ], - "additionalProperties": false - }, - "Prompt.Mention": { - "type": "object", - "properties": { - "start": { - "type": "number" - }, - "end": { - "type": "number" - }, - "text": { - "type": "string" + "operationId": "v2.websearch.providers", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true } - }, - "required": [ - "start", - "end", - "text" ], - "additionalProperties": false - }, - "PromptInput.FileAttachment": { - "type": "object", - "properties": { - "uri": { - "type": "string" + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebSearch.Provider" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } }, - "name": { - "type": "string" + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } }, - "description": { - "type": "string" + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } }, - "mention": { - "$ref": "#/components/schemas/Prompt.Mention" + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } } }, - "required": [ - "uri" + "description": "Return the registered web search providers.", + "summary": "List web search providers" + } + }, + "/api/websearch": { + "post": { + "tags": [ + "websearch" ], - "additionalProperties": false - }, - "Prompt.AgentAttachment": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "mention": { - "$ref": "#/components/schemas/Prompt.Mention" + "operationId": "v2.websearch.query", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true } - }, - "required": [ - "name" ], - "additionalProperties": false - }, - "PromptInput": { - "type": "object", - "properties": { - "text": { - "type": "string" + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/WebSearch.Response" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } }, - "files": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PromptInput.FileAttachment" + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } } }, - "agents": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Prompt.AgentAttachment" + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } } } }, - "required": [ - "text" - ], - "additionalProperties": false - }, - "Prompt.Base64": { - "type": "string", - "allOf": [ - { - "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$" - } - ] - }, - "Prompt.FileSource": { - "anyOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "inline" - ] + "description": "Run one web search through the selected provider. Specify a provider to override the configured default.", + "summary": "Search the web", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "query": { + "type": "string" + }, + "providerID": { + "type": "string" + } + }, + "required": [ + "query" + ], + "additionalProperties": false } - }, - "required": [ - "type" - ], - "additionalProperties": false + } }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "uri" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "type", - "uri" - ], - "additionalProperties": false - } - ] - }, - "Prompt.FileAttachment": { + "required": true + } + } + } + }, + "components": { + "schemas": { + "ServiceHealth": { "type": "object", "properties": { - "data": { - "$ref": "#/components/schemas/Prompt.Base64" - }, - "mime": { - "type": "string" - }, - "source": { - "$ref": "#/components/schemas/Prompt.FileSource" - }, - "name": { - "type": "string" + "healthy": { + "type": "boolean", + "enum": [ + true + ] }, - "description": { + "version": { "type": "string" }, - "mention": { - "$ref": "#/components/schemas/Prompt.Mention" + "pid": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] } }, "required": [ - "data", - "mime", - "source" + "healthy", + "version", + "pid" ], "additionalProperties": false }, - "Prompt": { + "UnauthorizedError": { "type": "object", "properties": { - "text": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Prompt.FileAttachment" - } + "_tag": { + "type": "string", + "enum": [ + "UnauthorizedError" + ] }, - "agents": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Prompt.AgentAttachment" - } + "message": { + "type": "string" } }, "required": [ - "text" + "_tag", + "message" ], "additionalProperties": false }, - "SessionInput.Admitted": { + "InvalidRequestError": { "type": "object", "properties": { - "admittedSeq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "prompt": { - "$ref": "#/components/schemas/Prompt" - }, - "delivery": { + "_tag": { "type": "string", "enum": [ - "steer", - "queue" + "InvalidRequestError" ] }, - "timeCreated": { - "type": "number" + "message": { + "type": "string" }, - "promotedSeq": { - "type": "integer", - "allOf": [ + "kind": { + "anyOf": [ { - "minimum": 0 + "type": "string" + }, + { + "type": "null" } ] - } - }, - "required": [ - "admittedSeq", - "id", - "sessionID", - "prompt", - "delivery", - "timeCreated" - ], - "additionalProperties": false - }, - "ConflictError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "ConflictError" - ] - }, - "message": { - "type": "string" }, - "resource": { + "field": { "anyOf": [ { "type": "string" @@ -11337,305 +11872,325 @@ ], "additionalProperties": false }, - "CommandNotFoundError": { + "ServiceStopRequest": { "type": "object", "properties": { - "_tag": { - "type": "string", - "enum": [ - "CommandNotFoundError" - ] - }, - "command": { - "type": "string" - }, - "message": { + "instanceID": { "type": "string" } }, "required": [ - "_tag", - "command", - "message" + "instanceID" ], "additionalProperties": false }, - "CommandEvaluationError": { + "ServiceStopResponse": { "type": "object", "properties": { - "_tag": { - "type": "string", - "enum": [ - "CommandEvaluationError" - ] - }, - "command": { - "type": "string" - }, - "message": { - "type": "string" + "accepted": { + "type": "boolean" } }, "required": [ - "_tag", - "command", - "message" + "accepted" ], "additionalProperties": false }, - "SkillNotFoundError": { + "Location.Info": { "type": "object", "properties": { - "_tag": { + "directory": { + "type": "string" + }, + "workspaceID": { "type": "string", - "enum": [ - "SkillNotFoundError" + "allOf": [ + { + "pattern": "^wrk" + } ] }, - "skill": { - "type": "string" - }, - "message": { - "type": "string" + "project": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "directory": { + "type": "string" + } + }, + "required": [ + "id", + "directory" + ], + "additionalProperties": false } }, "required": [ - "_tag", - "skill", - "message" + "directory", + "project" ], "additionalProperties": false }, - "SessionInput.Compaction": { + "Model.Ref": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": [ - "compaction" - ] - }, - "admittedSeq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, "id": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] + "type": "string" }, - "timeCreated": { - "type": "number" + "providerID": { + "type": "string" }, - "handledSeq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] + "variant": { + "type": "string" } }, "required": [ - "type", - "admittedSeq", "id", - "sessionID", - "timeCreated" + "providerID" ], "additionalProperties": false }, - "ServiceUnavailableError": { + "Provider.Settings": { + "type": "object" + }, + "Provider.Request": { "type": "object", "properties": { - "_tag": { - "type": "string", - "enum": [ - "ServiceUnavailableError" - ] + "settings": { + "$ref": "#/components/schemas/Provider.Settings" }, - "message": { - "type": "string" + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } }, - "service": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "body": { + "type": "object" } }, "required": [ - "_tag", - "message" + "settings", + "headers", + "body" ], "additionalProperties": false }, - "SessionBusyError": { + "Agent.Color": { + "type": "string" + }, + "PermissionV2.Effect": { + "type": "string", + "enum": [ + "allow", + "deny", + "ask" + ] + }, + "PermissionV2.Rule": { "type": "object", "properties": { - "_tag": { - "type": "string", - "enum": [ - "SessionBusyError" - ] - }, - "sessionID": { + "action": { "type": "string" }, - "message": { + "resource": { "type": "string" + }, + "effect": { + "$ref": "#/components/schemas/PermissionV2.Effect" } }, "required": [ - "_tag", - "sessionID", - "message" + "action", + "resource", + "effect" ], "additionalProperties": false }, - "UnknownError": { + "PermissionV2.Ruleset": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionV2.Rule" + } + }, + "Agent.Info": { "type": "object", "properties": { - "_tag": { - "type": "string", - "enum": [ - "UnknownError" - ] + "id": { + "type": "string" }, - "message": { + "name": { "type": "string" }, - "ref": { - "anyOf": [ - { - "type": "string" - }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "request": { + "$ref": "#/components/schemas/Provider.Request" + }, + "system": { + "type": "string" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "hidden": { + "type": "boolean" + }, + "color": { + "$ref": "#/components/schemas/Agent.Color" + }, + "steps": { + "type": "integer", + "allOf": [ { - "type": "null" + "exclusiveMinimum": 0 } ] + }, + "permissions": { + "$ref": "#/components/schemas/PermissionV2.Ruleset" } }, "required": [ - "_tag", - "message" + "id", + "name", + "request", + "mode", + "hidden", + "permissions" ], "additionalProperties": false }, - "Session.Message.AgentSelected": { + "Plugin.Info": { "type": "object", "properties": { "id": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "Money.USD": { + "type": "number" + }, + "TokenUsage.Info": { + "type": "object", + "properties": { + "input": { + "type": "number" }, - "metadata": { - "type": "object" + "output": { + "type": "number" }, - "time": { + "reasoning": { + "type": "number" + }, + "cache": { "type": "object", "properties": { - "created": { + "read": { + "type": "number" + }, + "write": { "type": "number" } }, "required": [ - "created" + "read", + "write" ], "additionalProperties": false + } + }, + "required": [ + "input", + "output", + "reasoning", + "cache" + ], + "additionalProperties": false + }, + "Location.Ref": { + "type": "object", + "properties": { + "directory": { + "type": "string" }, - "type": { + "workspaceID": { "type": "string", - "enum": [ - "agent-switched" + "allOf": [ + { + "pattern": "^wrk" + } ] - }, - "agent": { - "type": "string" } }, "required": [ - "id", - "time", - "type", - "agent" + "directory" ], "additionalProperties": false }, - "Session.Message.ModelSelected": { + "FileDiff.Info": { "type": "object", "properties": { - "id": { - "type": "string", + "file": { + "type": "string" + }, + "patch": { + "type": "string" + }, + "additions": { + "type": "integer", "allOf": [ { - "pattern": "^msg_" + "minimum": 0 } ] }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" + "deletions": { + "type": "integer", + "allOf": [ + { + "minimum": 0 } - }, - "required": [ - "created" - ], - "additionalProperties": false + ] }, - "type": { + "status": { "type": "string", "enum": [ - "model-switched" + "added", + "deleted", + "modified" ] - }, - "model": { - "$ref": "#/components/schemas/Model.Ref" - }, - "previous": { - "$ref": "#/components/schemas/Model.Ref" } }, "required": [ - "id", - "time", - "type", - "model" + "file", + "patch", + "additions", + "deletions", + "status" ], "additionalProperties": false }, - "Session.Message.User": { + "Session.Revert": { "type": "object", "properties": { - "id": { + "messageID": { "type": "string", "allOf": [ { @@ -11643,672 +12198,460 @@ } ] }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - } - }, - "required": [ - "created" - ], - "additionalProperties": false + "partID": { + "type": "string" }, - "text": { + "snapshot": { "type": "string" }, "files": { "type": "array", "items": { - "$ref": "#/components/schemas/Prompt.FileAttachment" - } - }, - "agents": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Prompt.AgentAttachment" + "$ref": "#/components/schemas/FileDiff.Info" } - }, - "type": { - "type": "string", - "enum": [ - "user" - ] } }, "required": [ - "id", - "time", - "text", - "type" + "messageID" ], "additionalProperties": false }, - "Session.Message.Synthetic": { + "Session.Info": { "type": "object", "properties": { "id": { "type": "string", "allOf": [ { - "pattern": "^msg_" + "pattern": "^ses" } ] }, - "metadata": { - "type": "object" + "parentID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "time": { + "fork": { "type": "object", "properties": { - "created": { - "type": "number" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] } }, "required": [ - "created" + "sessionID" ], "additionalProperties": false }, - "text": { + "projectID": { "type": "string" }, - "description": { + "agent": { "type": "string" }, - "type": { - "type": "string", - "enum": [ - "synthetic" - ] - } - }, - "required": [ - "id", - "time", - "text", - "type" - ], - "additionalProperties": false - }, - "Session.Message.System": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - } - }, - "required": [ - "created" - ], - "additionalProperties": false - }, - "type": { - "type": "string", - "enum": [ - "system" - ] + "model": { + "$ref": "#/components/schemas/Model.Ref" }, - "text": { - "type": "string" - } - }, - "required": [ - "id", - "time", - "type", - "text" - ], - "additionalProperties": false - }, - "Session.Message.Skill": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] + "cost": { + "$ref": "#/components/schemas/Money.USD" }, - "metadata": { - "type": "object" + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" }, "time": { "type": "object", "properties": { "created": { "type": "number" + }, + "updated": { + "type": "number" + }, + "archived": { + "type": "number" } }, "required": [ - "created" + "created", + "updated" ], "additionalProperties": false }, - "type": { - "type": "string", - "enum": [ - "skill" - ] - }, - "skill": { + "title": { "type": "string" }, - "name": { - "type": "string" + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "text": { + "subpath": { "type": "string" + }, + "revert": { + "$ref": "#/components/schemas/Session.Revert" } }, "required": [ "id", + "projectID", + "cost", + "tokens", "time", - "type", - "skill", - "name", - "text" + "title", + "location" ], "additionalProperties": false }, - "Session.Message.Shell": { + "SessionsResponse": { "type": "object", "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "metadata": { - "type": "object" + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Session.Info" + } }, - "time": { + "cursor": { "type": "object", "properties": { - "created": { - "type": "number" - }, - "completed": { - "type": "number" - } - }, - "required": [ - "created" - ], - "additionalProperties": false - }, - "type": { - "type": "string", - "enum": [ - "shell" - ] - }, - "shellID": { - "type": "string", - "allOf": [ - { - "pattern": "^sh_" - } - ] - }, - "command": { - "type": "string" - }, - "status": { - "type": "string", - "enum": [ - "running", - "exited", - "timeout", - "killed" - ] - }, - "exit": { - "anyOf": [ - { + "previous": { "anyOf": [ { - "type": "number" - }, - { - "type": "string", - "enum": [ - "NaN" - ] - }, - { - "type": "string", - "enum": [ - "Infinity" - ] + "type": "string" }, { - "type": "string", - "enum": [ - "-Infinity" - ] + "type": "null" } ] }, - { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] - } - ] - }, - "output": { - "type": "object", - "properties": { - "output": { - "type": "string" - }, - "cursor": { - "type": "integer", - "allOf": [ + "next": { + "anyOf": [ { - "minimum": 0 - } - ] - }, - "size": { - "type": "integer", - "allOf": [ + "type": "string" + }, { - "minimum": 0 + "type": "null" } ] - }, - "truncated": { - "type": "boolean" } }, - "required": [ - "output", - "cursor", - "size", - "truncated" - ], "additionalProperties": false } }, "required": [ - "id", - "time", - "type", - "shellID", - "command", - "status" + "data", + "cursor" ], "additionalProperties": false }, - "Session.Message.Assistant.Text": { + "InvalidCursorError": { "type": "object", "properties": { - "type": { + "_tag": { "type": "string", "enum": [ - "text" + "InvalidCursorError" ] }, - "text": { + "message": { "type": "string" } }, "required": [ - "type", - "text" + "_tag", + "message" ], "additionalProperties": false }, - "Session.Message.ProviderState": { - "type": "object" - }, - "Session.Message.Assistant.Reasoning": { + "InvalidRequestError1": { "type": "object", "properties": { - "type": { + "_tag": { "type": "string", "enum": [ - "reasoning" + "InvalidRequestError" ] }, - "text": { + "message": { "type": "string" }, - "state": { - "$ref": "#/components/schemas/Session.Message.ProviderState" + "kind": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" + "field": { + "anyOf": [ + { + "type": "string" }, - "completed": { - "type": "number" + { + "type": "null" } - }, - "required": [ - "created" - ], - "additionalProperties": false + ] } }, "required": [ - "type", - "text" + "_tag", + "message" ], "additionalProperties": false }, - "Session.Message.ToolState.Streaming": { + "SessionActive": { "type": "object", "properties": { - "status": { + "type": { "type": "string", "enum": [ - "streaming" + "running" ] - }, - "input": { - "type": "string" } }, "required": [ - "status", - "input" + "type" ], "additionalProperties": false }, - "Tool.TextContent": { + "SessionNotFoundError": { "type": "object", "properties": { - "type": { + "_tag": { "type": "string", "enum": [ - "text" + "SessionNotFoundError" ] }, - "text": { + "sessionID": { + "type": "string" + }, + "message": { "type": "string" } }, "required": [ - "type", - "text" + "_tag", + "sessionID", + "message" ], "additionalProperties": false }, - "Tool.FileContent": { + "MessageNotFoundError": { "type": "object", "properties": { - "type": { + "_tag": { "type": "string", "enum": [ - "file" + "MessageNotFoundError" ] }, - "uri": { + "sessionID": { "type": "string" }, - "mime": { + "messageID": { "type": "string" }, - "name": { + "message": { "type": "string" } }, "required": [ - "type", - "uri", - "mime" + "_tag", + "sessionID", + "messageID", + "message" ], "additionalProperties": false }, - "LLM.ToolContent": { - "anyOf": [ - { - "$ref": "#/components/schemas/Tool.TextContent" - }, - { - "$ref": "#/components/schemas/Tool.FileContent" - } - ] - }, - "Session.Message.ToolState.Running": { + "Prompt.Mention": { "type": "object", "properties": { - "status": { - "type": "string", - "enum": [ - "running" - ] - }, - "input": { - "type": "object" + "start": { + "type": "number" }, - "structured": { - "type": "object" + "end": { + "type": "number" }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLM.ToolContent" - } + "text": { + "type": "string" } }, "required": [ - "status", - "input", - "structured", - "content" + "start", + "end", + "text" ], "additionalProperties": false }, - "Session.Message.ToolState.Completed": { + "PromptInput.FileAttachment": { "type": "object", "properties": { - "status": { - "type": "string", - "enum": [ - "completed" - ] - }, - "input": { - "type": "object" + "uri": { + "type": "string" }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLM.ToolContent" - } + "name": { + "type": "string" }, - "structured": { - "type": "object" + "description": { + "type": "string" }, - "result": {} + "mention": { + "$ref": "#/components/schemas/Prompt.Mention" + } }, "required": [ - "status", - "input", - "content", - "structured" + "uri" ], "additionalProperties": false }, - "Session.StructuredError": { + "Prompt.AgentAttachment": { "type": "object", "properties": { - "type": { + "name": { "type": "string" }, - "message": { - "type": "string" + "mention": { + "$ref": "#/components/schemas/Prompt.Mention" } }, "required": [ - "type", - "message" + "name" ], "additionalProperties": false }, - "Session.Message.ToolState.Error": { + "Prompt.Base64": { + "type": "string", + "allOf": [ + { + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$" + } + ] + }, + "Prompt.FileSource": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "inline" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "uri" + ] + }, + "uri": { + "type": "string" + } + }, + "required": [ + "type", + "uri" + ], + "additionalProperties": false + } + ] + }, + "Prompt.FileAttachment": { "type": "object", "properties": { - "status": { - "type": "string", - "enum": [ - "error" - ] + "data": { + "$ref": "#/components/schemas/Prompt.Base64" }, - "input": { - "type": "object" + "mime": { + "type": "string" }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLM.ToolContent" - } + "source": { + "$ref": "#/components/schemas/Prompt.FileSource" }, - "structured": { - "type": "object" + "name": { + "type": "string" }, - "error": { - "$ref": "#/components/schemas/Session.StructuredError" + "description": { + "type": "string" }, - "result": {} + "mention": { + "$ref": "#/components/schemas/Prompt.Mention" + } }, "required": [ - "status", - "input", - "content", - "structured", - "error" + "data", + "mime", + "source" ], "additionalProperties": false }, - "Session.Message.Assistant.Tool": { + "SessionPending.UserData": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": [ - "tool" - ] - }, - "id": { - "type": "string" - }, - "name": { + "text": { "type": "string" }, - "executed": { - "type": "boolean" - }, - "providerState": { - "$ref": "#/components/schemas/Session.Message.ProviderState" - }, - "providerResultState": { - "$ref": "#/components/schemas/Session.Message.ProviderState" + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.FileAttachment" + } }, - "state": { - "anyOf": [ - { - "$ref": "#/components/schemas/Session.Message.ToolState.Streaming" - }, - { - "$ref": "#/components/schemas/Session.Message.ToolState.Running" - }, - { - "$ref": "#/components/schemas/Session.Message.ToolState.Completed" - }, - { - "$ref": "#/components/schemas/Session.Message.ToolState.Error" - } - ] + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - }, - "ran": { - "type": "number" - }, - "completed": { - "type": "number" - } - }, - "required": [ - "created" - ], - "additionalProperties": false + "metadata": { + "type": "object" } }, "required": [ - "type", - "id", - "name", - "state", - "time" + "text" ], "additionalProperties": false }, - "Session.Message.Assistant.Retry": { + "SessionPending.User": { "type": "object", "properties": { - "attempt": { + "admittedSeq": { "type": "integer", "allOf": [ { - "exclusiveMinimum": 0 + "minimum": 0 } ] }, - "at": { - "type": "number" - }, - "error": { - "$ref": "#/components/schemas/Session.StructuredError" - } - }, - "required": [ - "attempt", - "at", - "error" - ], - "additionalProperties": false - }, - "Session.Message.Assistant": { - "type": "object", - "properties": { "id": { "type": "string", "allOf": [ @@ -12317,174 +12660,170 @@ } ] }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - }, - "completed": { - "type": "number" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" } - }, - "required": [ - "created" - ], - "additionalProperties": false + ] + }, + "timeCreated": { + "type": "number" }, "type": { "type": "string", "enum": [ - "assistant" + "user" ] }, - "agent": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/Model.Ref" - }, - "content": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/Session.Message.Assistant.Text" - }, - { - "$ref": "#/components/schemas/Session.Message.Assistant.Reasoning" - }, - { - "$ref": "#/components/schemas/Session.Message.Assistant.Tool" - } - ] - } - }, - "snapshot": { - "type": "object", - "properties": { - "start": { - "type": "string" - }, - "end": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false + "data": { + "$ref": "#/components/schemas/SessionPending.UserData" }, - "finish": { + "delivery": { "type": "string", "enum": [ - "stop", - "length", - "tool-calls", - "content-filter", - "error", - "unknown" + "steer", + "queue" ] - }, - "cost": { - "$ref": "#/components/schemas/Money.USD" - }, - "tokens": { - "$ref": "#/components/schemas/TokenUsage.Info" - }, - "error": { - "$ref": "#/components/schemas/Session.StructuredError" - }, - "retry": { - "$ref": "#/components/schemas/Session.Message.Assistant.Retry" } }, "required": [ + "admittedSeq", "id", - "time", + "sessionID", + "timeCreated", "type", - "agent", - "model", - "content" + "data", + "delivery" ], "additionalProperties": false }, - "Session.Message.Compaction.Running": { + "ConflictError": { "type": "object", "properties": { - "type": { + "_tag": { "type": "string", "enum": [ - "compaction" - ] - }, - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } + "ConflictError" ] }, - "metadata": { - "type": "object" + "message": { + "type": "string" }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" + "resource": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" } - }, - "required": [ - "created" - ], - "additionalProperties": false - }, - "status": { - "type": "string", - "enum": [ - "running" ] - }, - "reason": { + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "CommandNotFoundError": { + "type": "object", + "properties": { + "_tag": { "type": "string", "enum": [ - "auto", - "manual" + "CommandNotFoundError" ] }, - "summary": { + "command": { "type": "string" }, - "recent": { + "message": { "type": "string" } }, "required": [ - "type", - "id", - "time", - "status", - "reason", - "summary", - "recent" + "_tag", + "command", + "message" ], "additionalProperties": false }, - "Session.Message.Compaction.Completed": { + "CommandEvaluationError": { "type": "object", "properties": { - "type": { + "_tag": { "type": "string", "enum": [ - "compaction" + "CommandEvaluationError" + ] + }, + "command": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "command", + "message" + ], + "additionalProperties": false + }, + "SkillNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "SkillNotFoundError" + ] + }, + "skill": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "skill", + "message" + ], + "additionalProperties": false + }, + "SessionPending.SyntheticData": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "description": { + "type": "string" + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "text" + ], + "additionalProperties": false + }, + "SessionPending.Synthetic": { + "type": "object", + "properties": { + "admittedSeq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } ] }, "id": { @@ -12495,61 +12834,175 @@ } ] }, - "metadata": { - "type": "object" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" + "timeCreated": { + "type": "number" + }, + "type": { + "type": "string", + "enum": [ + "synthetic" + ] + }, + "data": { + "$ref": "#/components/schemas/SessionPending.SyntheticData" + }, + "delivery": { + "type": "string", + "enum": [ + "steer", + "queue" + ] + } + }, + "required": [ + "admittedSeq", + "id", + "sessionID", + "timeCreated", + "type", + "data", + "delivery" + ], + "additionalProperties": false + }, + "SessionPending.Compaction": { + "type": "object", + "properties": { + "admittedSeq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 } - }, - "required": [ - "created" - ], - "additionalProperties": false + ] }, - "status": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "timeCreated": { + "type": "number" + }, + "type": { "type": "string", "enum": [ - "completed" + "compaction" + ] + } + }, + "required": [ + "admittedSeq", + "id", + "sessionID", + "timeCreated", + "type" + ], + "additionalProperties": false + }, + "ServiceUnavailableError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "ServiceUnavailableError" ] }, - "reason": { + "message": { + "type": "string" + }, + "service": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "SessionBusyError": { + "type": "object", + "properties": { + "_tag": { "type": "string", "enum": [ - "auto", - "manual" + "SessionBusyError" ] }, - "summary": { + "sessionID": { "type": "string" }, - "recent": { + "message": { "type": "string" } }, "required": [ - "type", - "id", - "time", - "status", - "reason", - "summary", - "recent" + "_tag", + "sessionID", + "message" ], "additionalProperties": false }, - "Session.Message.Compaction.Failed": { + "UnknownError": { "type": "object", "properties": { - "type": { + "_tag": { "type": "string", "enum": [ - "compaction" + "UnknownError" ] }, + "message": { + "type": "string" + }, + "ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "Session.Message.AgentSelected": { + "type": "object", + "properties": { "id": { "type": "string", "allOf": [ @@ -12573,1482 +13026,997 @@ ], "additionalProperties": false }, - "status": { - "type": "string", - "enum": [ - "failed" - ] - }, - "reason": { + "type": { "type": "string", "enum": [ - "auto", - "manual" + "agent-switched" ] }, - "error": { - "$ref": "#/components/schemas/Session.StructuredError" + "agent": { + "type": "string" } }, "required": [ - "type", "id", "time", - "status", - "reason", - "error" + "type", + "agent" ], "additionalProperties": false }, - "Session.Message.Compaction": { - "anyOf": [ - { - "$ref": "#/components/schemas/Session.Message.Compaction.Running" - }, - { - "$ref": "#/components/schemas/Session.Message.Compaction.Completed" - }, - { - "$ref": "#/components/schemas/Session.Message.Compaction.Failed" - } - ] - }, - "Session.Message.Info": { - "anyOf": [ - { - "$ref": "#/components/schemas/Session.Message.AgentSelected" - }, - { - "$ref": "#/components/schemas/Session.Message.ModelSelected" - }, - { - "$ref": "#/components/schemas/Session.Message.User" - }, - { - "$ref": "#/components/schemas/Session.Message.Synthetic" - }, - { - "$ref": "#/components/schemas/Session.Message.System" - }, - { - "$ref": "#/components/schemas/Session.Message.Skill" - }, - { - "$ref": "#/components/schemas/Session.Message.Shell" - }, - { - "$ref": "#/components/schemas/Session.Message.Assistant" - }, - { - "$ref": "#/components/schemas/Session.Message.Compaction" - } - ] - }, - "InstructionEntry.Key": { - "type": "string", - "allOf": [ - { - "pattern": "^[a-z0-9][a-z0-9._-]*$", - "description": "Instruction entry key (lowercase alphanumerics plus . _ -)" - } - ] - }, - "InstructionEntry.Info": { - "type": "object", - "properties": { - "key": { - "$ref": "#/components/schemas/InstructionEntry.Key" - }, - "value": {} - }, - "required": [ - "key", - "value" - ], - "additionalProperties": false - }, - "session.agent.selected": { + "Session.Message.ModelSelected": { "type": "object", "properties": { "id": { "type": "string", "allOf": [ { - "pattern": "^evt_" + "pattern": "^msg_" } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, - "type": { - "type": "string", - "enum": [ - "session.agent.selected" - ] - }, - "durable": { + "time": { "type": "object", "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] + "created": { + "type": "number" } }, "required": [ - "aggregateID", - "seq", - "version" + "created" ], "additionalProperties": false }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + "type": { + "type": "string", + "enum": [ + "model-switched" + ] }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "agent": { - "type": "string" - } - }, - "required": [ - "sessionID", - "agent" - ], - "additionalProperties": false + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "previous": { + "$ref": "#/components/schemas/Model.Ref" } }, "required": [ "id", - "created", + "time", "type", - "durable", - "data" + "model" ], "additionalProperties": false }, - "session.model.selected": { + "Session.Message.User": { "type": "object", "properties": { "id": { "type": "string", "allOf": [ { - "pattern": "^evt_" + "pattern": "^msg_" } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, - "type": { - "type": "string", - "enum": [ - "session.model.selected" - ] - }, - "durable": { + "time": { "type": "object", "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] + "created": { + "type": "number" } }, "required": [ - "aggregateID", - "seq", - "version" + "created" ], "additionalProperties": false }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + "text": { + "type": "string" }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "model": { - "$ref": "#/components/schemas/Model.Ref" - } - }, - "required": [ - "sessionID", - "model" - ], - "additionalProperties": false + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + }, + "type": { + "type": "string", + "enum": [ + "user" + ] } }, "required": [ "id", - "created", - "type", - "durable", - "data" + "time", + "text", + "type" ], "additionalProperties": false }, - "session.moved": { + "Session.Message.Synthetic": { "type": "object", "properties": { "id": { "type": "string", "allOf": [ { - "pattern": "^evt_" + "pattern": "^msg_" } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "text": { + "type": "string" + }, + "description": { + "type": "string" + }, "type": { "type": "string", "enum": [ - "session.moved" + "synthetic" ] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "subpath": { - "type": "string" - } - }, - "required": [ - "sessionID", - "location" - ], - "additionalProperties": false } }, "required": [ "id", - "created", - "type", - "durable", - "data" + "time", + "text", + "type" ], "additionalProperties": false }, - "session.renamed": { + "Session.Message.System": { "type": "object", "properties": { "id": { "type": "string", "allOf": [ { - "pattern": "^evt_" + "pattern": "^msg_" } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, - "type": { - "type": "string", - "enum": [ - "session.renamed" - ] - }, - "durable": { + "time": { "type": "object", "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] + "created": { + "type": "number" } }, "required": [ - "aggregateID", - "seq", - "version" + "created" ], "additionalProperties": false }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + "type": { + "type": "string", + "enum": [ + "system" + ] }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "title": { - "type": "string" - } - }, - "required": [ - "sessionID", - "title" - ], - "additionalProperties": false + "text": { + "type": "string" } }, "required": [ "id", - "created", + "time", "type", - "durable", - "data" + "text" ], "additionalProperties": false }, - "session.deleted": { + "Session.Message.Skill": { "type": "object", "properties": { "id": { "type": "string", "allOf": [ { - "pattern": "^evt_" + "pattern": "^msg_" } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, - "type": { - "type": "string", - "enum": [ - "session.deleted" - ] - }, - "durable": { + "time": { "type": "object", "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 2 - ] + "created": { + "type": "number" } }, "required": [ - "aggregateID", - "seq", - "version" + "created" ], "additionalProperties": false }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + "type": { + "type": "string", + "enum": [ + "skill" + ] }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - } - }, - "required": [ - "sessionID" - ], - "additionalProperties": false + "skill": { + "type": "string" + }, + "name": { + "type": "string" + }, + "text": { + "type": "string" } }, "required": [ "id", - "created", + "time", "type", - "durable", - "data" + "skill", + "name", + "text" ], "additionalProperties": false }, - "session.forked": { + "Session.Message.Shell": { "type": "object", "properties": { "id": { "type": "string", "allOf": [ { - "pattern": "^evt_" + "pattern": "^msg_" } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, - "type": { - "type": "string", - "enum": [ - "session.forked" - ] - }, - "durable": { + "time": { "type": "object", "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] + "created": { + "type": "number" }, - "version": { - "type": "number", - "enum": [ - 1 - ] + "completed": { + "type": "number" } }, "required": [ - "aggregateID", - "seq", - "version" + "created" ], "additionalProperties": false }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + "type": { + "type": "string", + "enum": [ + "shell" + ] }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "parentID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "from": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - } - }, - "required": [ - "sessionID", - "parentID" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], - "additionalProperties": false - }, - "session.prompt.promoted": { - "type": "object", - "properties": { - "id": { + "shellID": { "type": "string", "allOf": [ { - "pattern": "^evt_" + "pattern": "^sh_" } ] }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" + "command": { + "type": "string" }, - "type": { + "status": { "type": "string", "enum": [ - "session.prompt.promoted" + "running", + "exited", + "timeout", + "killed" ] }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ + "exit": { + "anyOf": [ + { + "anyOf": [ { - "minimum": 0 + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] } ] }, - "version": { - "type": "number", + { + "type": "string", "enum": [ - 1 + "Infinity", + "-Infinity", + "NaN" ] } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + ] }, - "data": { + "output": { "type": "object", "properties": { - "sessionID": { - "type": "string", + "output": { + "type": "string" + }, + "cursor": { + "type": "integer", "allOf": [ { - "pattern": "^ses" + "minimum": 0 } ] }, - "inputID": { - "type": "string", + "size": { + "type": "integer", "allOf": [ { - "pattern": "^msg_" + "minimum": 0 } ] + }, + "truncated": { + "type": "boolean" } }, "required": [ - "sessionID", - "inputID" + "output", + "cursor", + "size", + "truncated" ], "additionalProperties": false } }, "required": [ "id", - "created", + "time", "type", - "durable", - "data" + "shellID", + "command", + "status" ], "additionalProperties": false }, - "session.prompt.admitted": { + "Session.Message.ProviderState": { + "type": "object" + }, + "Session.Message.Assistant.Text": { "type": "object", "properties": { - "id": { + "type": { "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } + "enum": [ + "text" ] }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" + "text": { + "type": "string" }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState" + } + }, + "required": [ + "type", + "text" + ], + "additionalProperties": false + }, + "Session.Message.Assistant.Reasoning": { + "type": "object", + "properties": { "type": { "type": "string", "enum": [ - "session.prompt.admitted" + "reasoning" ] }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false + "text": { + "type": "string" }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState" }, - "data": { + "time": { "type": "object", "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "inputID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "prompt": { - "$ref": "#/components/schemas/Prompt" + "created": { + "type": "number" }, - "delivery": { - "type": "string", - "enum": [ - "steer", - "queue" - ] + "completed": { + "type": "number" } }, "required": [ - "sessionID", - "inputID", - "prompt", - "delivery" + "created" ], "additionalProperties": false } }, "required": [ - "id", - "created", "type", - "durable", - "data" + "text" ], "additionalProperties": false }, - "session.execution.started": { + "Session.Message.ToolState.Streaming": { "type": "object", "properties": { - "id": { + "status": { "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } + "enum": [ + "streaming" ] }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "session.execution.started" - ] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - } - }, - "required": [ - "sessionID" - ], - "additionalProperties": false + "input": { + "type": "string" } }, "required": [ - "id", - "created", - "type", - "durable", - "data" + "status", + "input" ], "additionalProperties": false }, - "session.execution.succeeded": { + "Session.Message.ToolState.Running": { "type": "object", "properties": { - "id": { + "status": { "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } + "enum": [ + "running" ] }, - "created": { - "type": "number" + "input": { + "type": "object" }, "metadata": { "type": "object" - }, + } + }, + "required": [ + "status", + "input", + "metadata" + ], + "additionalProperties": false + }, + "Tool.TextContent": { + "type": "object", + "properties": { "type": { "type": "string", "enum": [ - "session.execution.succeeded" + "text" ] }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - } - }, - "required": [ - "sessionID" - ], - "additionalProperties": false + "text": { + "type": "string" } }, "required": [ - "id", - "created", "type", - "durable", - "data" + "text" ], "additionalProperties": false }, - "session.execution.failed": { + "Tool.FileContent": { "type": "object", "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, "type": { "type": "string", "enum": [ - "session.execution.failed" + "file" ] }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false + "uri": { + "type": "string" }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + "mime": { + "type": "string" }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "error": { - "$ref": "#/components/schemas/Session.StructuredError" - } - }, - "required": [ - "sessionID", - "error" - ], - "additionalProperties": false + "name": { + "type": "string" } }, "required": [ - "id", - "created", "type", - "durable", - "data" + "uri", + "mime" ], "additionalProperties": false }, - "session.execution.interrupted": { + "LLM.ToolContent": { + "anyOf": [ + { + "$ref": "#/components/schemas/Tool.TextContent" + }, + { + "$ref": "#/components/schemas/Tool.FileContent" + } + ] + }, + "Session.Message.ToolState.Completed": { "type": "object", "properties": { - "id": { + "status": { "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } + "enum": [ + "completed" ] }, - "created": { - "type": "number" + "input": { + "type": "object" + }, + "content": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/LLM.ToolContent" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/LLM.ToolContent" + } }, "metadata": { "type": "object" - }, + } + }, + "required": [ + "status", + "input", + "content" + ], + "additionalProperties": false + }, + "Session.StructuredError": { + "type": "object", + "properties": { "type": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "type", + "message" + ], + "additionalProperties": false + }, + "Session.Message.ToolState.Error": { + "type": "object", + "properties": { + "status": { "type": "string", "enum": [ - "session.execution.interrupted" + "error" ] }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false + "input": { + "type": "object" }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + "error": { + "$ref": "#/components/schemas/Session.StructuredError" }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "reason": { - "type": "string", - "enum": [ - "user", - "shutdown", - "superseded" - ] + "content": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/LLM.ToolContent" } - }, - "required": [ - "sessionID", - "reason" ], - "additionalProperties": false + "minItems": 1, + "items": { + "$ref": "#/components/schemas/LLM.ToolContent" + } + }, + "metadata": { + "type": "object" } }, "required": [ - "id", - "created", - "type", - "durable", - "data" + "status", + "input", + "error" ], "additionalProperties": false }, - "session.instructions.updated": { + "Session.Message.Assistant.Tool": { "type": "object", "properties": { - "id": { + "type": { "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } + "enum": [ + "tool" ] }, - "created": { - "type": "number" + "id": { + "type": "string" }, - "metadata": { - "type": "object" + "name": { + "type": "string" }, - "type": { - "type": "string", - "enum": [ - "session.instructions.updated" - ] + "executed": { + "type": "boolean" }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" + "providerState": { + "$ref": "#/components/schemas/Session.Message.ProviderState" + }, + "providerResultState": { + "$ref": "#/components/schemas/Session.Message.ProviderState" + }, + "state": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.ToolState.Streaming" }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] + { + "$ref": "#/components/schemas/Session.Message.ToolState.Running" }, - "version": { - "type": "number", - "enum": [ - 1 - ] + { + "$ref": "#/components/schemas/Session.Message.ToolState.Completed" + }, + { + "$ref": "#/components/schemas/Session.Message.ToolState.Error" } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + ] }, - "data": { + "time": { "type": "object", "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] + "created": { + "type": "number" }, - "text": { - "type": "string" + "ran": { + "type": "number" + }, + "completed": { + "type": "number" } }, "required": [ - "sessionID", - "text" + "created" ], "additionalProperties": false } }, "required": [ - "id", - "created", "type", - "durable", - "data" + "id", + "name", + "state", + "time" ], "additionalProperties": false }, - "session.synthetic": { + "Session.Message.Assistant.Retry": { "type": "object", "properties": { - "id": { - "type": "string", + "attempt": { + "type": "integer", "allOf": [ { - "pattern": "^evt_" + "exclusiveMinimum": 0 } ] }, - "created": { + "at": { "type": "number" }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + } + }, + "required": [ + "attempt", + "at", + "error" + ], + "additionalProperties": false + }, + "Session.Message.Assistant": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, "metadata": { "type": "object" }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "completed": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, "type": { "type": "string", "enum": [ - "session.synthetic" + "assistant" ] }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false + "agent": { + "type": "string" }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + "model": { + "$ref": "#/components/schemas/Model.Ref" }, - "data": { + "content": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.Assistant.Text" + }, + { + "$ref": "#/components/schemas/Session.Message.Assistant.Reasoning" + }, + { + "$ref": "#/components/schemas/Session.Message.Assistant.Tool" + } + ] + } + }, + "snapshot": { "type": "object", "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "text": { + "start": { "type": "string" }, - "description": { + "end": { "type": "string" }, - "metadata": { - "type": "object" + "files": { + "type": "array", + "items": { + "type": "string" + } } }, - "required": [ - "sessionID", - "text" - ], "additionalProperties": false + }, + "finish": { + "type": "string", + "enum": [ + "stop", + "length", + "tool-calls", + "content-filter", + "error", + "unknown" + ] + }, + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + }, + "retry": { + "$ref": "#/components/schemas/Session.Message.Assistant.Retry" } }, "required": [ "id", - "created", + "time", "type", - "durable", - "data" + "agent", + "model", + "content" ], "additionalProperties": false }, - "session.skill.activated": { + "Session.Message.Compaction.Running": { "type": "object", "properties": { + "type": { + "type": "string", + "enum": [ + "compaction" + ] + }, "id": { "type": "string", "allOf": [ { - "pattern": "^evt_" + "pattern": "^msg_" } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, - "type": { - "type": "string", - "enum": [ - "session.skill.activated" - ] - }, - "durable": { + "time": { "type": "object", "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] + "created": { + "type": "number" } }, "required": [ - "aggregateID", - "seq", - "version" + "created" ], "additionalProperties": false }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + "status": { + "type": "string", + "enum": [ + "running" + ] }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "text": { - "type": "string" - } - }, - "required": [ - "sessionID", - "id", - "name", - "text" - ], - "additionalProperties": false + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "summary": { + "type": "string" + }, + "recent": { + "type": "string" } }, "required": [ - "id", - "created", "type", - "durable", - "data" + "id", + "time", + "status", + "reason", + "summary", + "recent" ], "additionalProperties": false }, - "Shell": { + "Session.Message.Compaction.Completed": { "type": "object", "properties": { + "type": { + "type": "string", + "enum": [ + "compaction" + ] + }, "id": { "type": "string", "allOf": [ { - "pattern": "^sh_" + "pattern": "^msg_" } ] }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, "status": { "type": "string", "enum": [ - "running", - "exited", - "timeout", - "killed" + "completed" ] }, - "command": { - "type": "string" - }, - "cwd": { - "type": "string" + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] }, - "shell": { + "summary": { "type": "string" }, - "file": { + "recent": { "type": "string" - }, - "pid": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "exit": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "NaN" - ] - }, - { - "type": "string", - "enum": [ - "Infinity" - ] - }, + } + }, + "required": [ + "type", + "id", + "time", + "status", + "reason", + "summary", + "recent" + ], + "additionalProperties": false + }, + "Session.Message.Compaction.Failed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "compaction" + ] + }, + "id": { + "type": "string", + "allOf": [ { - "type": "string", - "enum": [ - "-Infinity" - ] + "pattern": "^msg_" } ] }, @@ -14058,76 +14026,171 @@ "time": { "type": "object", "properties": { - "started": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "NaN" - ] - }, - { - "type": "string", - "enum": [ - "Infinity" - ] - }, - { - "type": "string", - "enum": [ - "-Infinity" - ] - } - ] - }, - "completed": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "NaN" - ] - }, - { - "type": "string", - "enum": [ - "Infinity" - ] - }, - { - "type": "string", - "enum": [ - "-Infinity" - ] - } - ] + "created": { + "type": "number" } }, "required": [ - "started" + "created" ], "additionalProperties": false + }, + "status": { + "type": "string", + "enum": [ + "failed" + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" } }, "required": [ + "type", "id", + "time", "status", - "command", - "cwd", - "shell", - "file", - "metadata", - "time" + "reason", + "error" ], "additionalProperties": false }, - "session.shell.started": { + "Session.Message.Compaction": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.Compaction.Running" + }, + { + "$ref": "#/components/schemas/Session.Message.Compaction.Completed" + }, + { + "$ref": "#/components/schemas/Session.Message.Compaction.Failed" + } + ] + }, + "Session.Message.Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.AgentSelected" + }, + { + "$ref": "#/components/schemas/Session.Message.ModelSelected" + }, + { + "$ref": "#/components/schemas/Session.Message.User" + }, + { + "$ref": "#/components/schemas/Session.Message.Synthetic" + }, + { + "$ref": "#/components/schemas/Session.Message.System" + }, + { + "$ref": "#/components/schemas/Session.Message.Skill" + }, + { + "$ref": "#/components/schemas/Session.Message.Shell" + }, + { + "$ref": "#/components/schemas/Session.Message.Assistant" + }, + { + "$ref": "#/components/schemas/Session.Message.Compaction" + } + ] + }, + "SessionPending.Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionPending.User" + }, + { + "$ref": "#/components/schemas/SessionPending.Synthetic" + }, + { + "$ref": "#/components/schemas/SessionPending.Compaction" + } + ] + }, + "InstructionEntry.Key": { + "type": "string", + "allOf": [ + { + "pattern": "^[a-z0-9][a-z0-9._-]*$", + "description": "Instruction entry key (lowercase alphanumerics plus . _ -)" + } + ] + }, + "InstructionEntry.Info": { + "type": "object", + "properties": { + "key": { + "$ref": "#/components/schemas/InstructionEntry.Key" + }, + "value": {} + }, + "required": [ + "key", + "value" + ], + "additionalProperties": false + }, + "InstructionEntryValueTooLargeError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "InstructionEntryValueTooLargeError" + ] + }, + "actualBytes": { + "type": "integer" + }, + "maxBytes": { + "type": "integer" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "actualBytes", + "maxBytes", + "message" + ], + "additionalProperties": false + }, + "SessionGenerateResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": [ + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "data" + ], + "additionalProperties": false + }, + "session.agent.selected": { "type": "object", "properties": { "id": { @@ -14147,7 +14210,7 @@ "type": { "type": "string", "enum": [ - "session.shell.started" + "session.agent.selected" ] }, "durable": { @@ -14192,13 +14255,13 @@ } ] }, - "shell": { - "$ref": "#/components/schemas/Shell" + "agent": { + "type": "string" } }, "required": [ "sessionID", - "shell" + "agent" ], "additionalProperties": false } @@ -14212,7 +14275,7 @@ ], "additionalProperties": false }, - "session.shell.ended": { + "session.model.selected": { "type": "object", "properties": { "id": { @@ -14232,7 +14295,7 @@ "type": { "type": "string", "enum": [ - "session.shell.ended" + "session.model.selected" ] }, "durable": { @@ -14277,48 +14340,13 @@ } ] }, - "shell": { - "$ref": "#/components/schemas/Shell" - }, - "output": { - "type": "object", - "properties": { - "output": { - "type": "string" - }, - "cursor": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "size": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "truncated": { - "type": "boolean" - } - }, - "required": [ - "output", - "cursor", - "size", - "truncated" - ], - "additionalProperties": false + "model": { + "$ref": "#/components/schemas/Model.Ref" } }, "required": [ "sessionID", - "shell", - "output" + "model" ], "additionalProperties": false } @@ -14332,7 +14360,7 @@ ], "additionalProperties": false }, - "session.step.started": { + "session.moved": { "type": "object", "properties": { "id": { @@ -14352,7 +14380,7 @@ "type": { "type": "string", "enum": [ - "session.step.started" + "session.moved" ] }, "durable": { @@ -14397,29 +14425,19 @@ } ] }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "agent": { + "projectID": { "type": "string" }, - "model": { - "$ref": "#/components/schemas/Model.Ref" - }, - "snapshot": { + "subpath": { "type": "string" } }, "required": [ "sessionID", - "assistantMessageID", - "agent", - "model" + "location" ], "additionalProperties": false } @@ -14433,7 +14451,7 @@ ], "additionalProperties": false }, - "session.step.ended": { + "session.renamed": { "type": "object", "properties": { "id": { @@ -14453,7 +14471,7 @@ "type": { "type": "string", "enum": [ - "session.step.ended" + "session.renamed" ] }, "durable": { @@ -14498,47 +14516,13 @@ } ] }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "finish": { - "type": "string", - "enum": [ - "stop", - "length", - "tool-calls", - "content-filter", - "error", - "unknown" - ] - }, - "cost": { - "$ref": "#/components/schemas/Money.USD" - }, - "tokens": { - "$ref": "#/components/schemas/TokenUsage.Info" - }, - "snapshot": { + "title": { "type": "string" - }, - "files": { - "type": "array", - "items": { - "type": "string" - } } }, "required": [ "sessionID", - "assistantMessageID", - "finish", - "cost", - "tokens" + "title" ], "additionalProperties": false } @@ -14552,7 +14536,7 @@ ], "additionalProperties": false }, - "session.step.failed": { + "session.deleted": { "type": "object", "properties": { "id": { @@ -14572,7 +14556,7 @@ "type": { "type": "string", "enum": [ - "session.step.failed" + "session.deleted" ] }, "durable": { @@ -14592,7 +14576,7 @@ "version": { "type": "number", "enum": [ - 1 + 2 ] } }, @@ -14616,29 +14600,10 @@ "pattern": "^ses" } ] - }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "error": { - "$ref": "#/components/schemas/Session.StructuredError" - }, - "cost": { - "$ref": "#/components/schemas/Money.USD" - }, - "tokens": { - "$ref": "#/components/schemas/TokenUsage.Info" } }, "required": [ - "sessionID", - "assistantMessageID", - "error" + "sessionID" ], "additionalProperties": false } @@ -14652,7 +14617,7 @@ ], "additionalProperties": false }, - "session.text.started": { + "session.forked": { "type": "object", "properties": { "id": { @@ -14672,7 +14637,7 @@ "type": { "type": "string", "enum": [ - "session.text.started" + "session.forked" ] }, "durable": { @@ -14692,7 +14657,7 @@ "version": { "type": "number", "enum": [ - 1 + 2 ] } }, @@ -14717,27 +14682,35 @@ } ] }, - "assistantMessageID": { + "parentID": { "type": "string", "allOf": [ { - "pattern": "^msg_" + "pattern": "^ses" } ] }, - "ordinal": { + "parentSeq": { "type": "integer", "allOf": [ { - "minimum": 0 + "minimum": -1 + } + ] + }, + "from": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" } ] } }, "required": [ "sessionID", - "assistantMessageID", - "ordinal" + "parentID", + "parentSeq" ], "additionalProperties": false } @@ -14751,7 +14724,7 @@ ], "additionalProperties": false }, - "session.text.ended": { + "session.input.promoted": { "type": "object", "properties": { "id": { @@ -14771,7 +14744,7 @@ "type": { "type": "string", "enum": [ - "session.text.ended" + "session.input.promoted" ] }, "durable": { @@ -14816,31 +14789,18 @@ } ] }, - "assistantMessageID": { + "inputID": { "type": "string", "allOf": [ { "pattern": "^msg_" } ] - }, - "ordinal": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "text": { - "type": "string" } }, "required": [ "sessionID", - "assistantMessageID", - "ordinal", - "text" + "inputID" ], "additionalProperties": false } @@ -14854,47 +14814,153 @@ ], "additionalProperties": false }, - "Session.Message.ProviderState3": { - "type": "object" + "SessionPending.UserData1": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "text" + ], + "additionalProperties": false }, - "session.reasoning.started": { + "SessionPending.UserMessage": { "type": "object", "properties": { - "id": { + "type": { "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } + "enum": [ + "user" ] }, - "created": { - "type": "number" + "data": { + "$ref": "#/components/schemas/SessionPending.UserData1" + }, + "delivery": { + "type": "string", + "enum": [ + "steer", + "queue" + ] + } + }, + "required": [ + "type", + "data", + "delivery" + ], + "additionalProperties": false + }, + "SessionPending.SyntheticData1": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "description": { + "type": "string" }, "metadata": { "type": "object" - }, + } + }, + "required": [ + "text" + ], + "additionalProperties": false + }, + "SessionPending.SyntheticMessage": { + "type": "object", + "properties": { "type": { "type": "string", "enum": [ - "session.reasoning.started" + "synthetic" ] }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { + "data": { + "$ref": "#/components/schemas/SessionPending.SyntheticData1" + }, + "delivery": { + "type": "string", + "enum": [ + "steer", + "queue" + ] + } + }, + "required": [ + "type", + "data", + "delivery" + ], + "additionalProperties": false + }, + "SessionPending.Message": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionPending.UserMessage" + }, + { + "$ref": "#/components/schemas/SessionPending.SyntheticMessage" + } + ] + }, + "session.input.admitted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.input.admitted" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { "type": "number", "enum": [ 1 @@ -14922,7 +14988,7 @@ } ] }, - "assistantMessageID": { + "inputID": { "type": "string", "allOf": [ { @@ -14930,22 +14996,14 @@ } ] }, - "ordinal": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "state": { - "$ref": "#/components/schemas/Session.Message.ProviderState3" + "input": { + "$ref": "#/components/schemas/SessionPending.Message" } }, "required": [ "sessionID", - "assistantMessageID", - "ordinal" + "inputID", + "input" ], "additionalProperties": false } @@ -14959,10 +15017,7 @@ ], "additionalProperties": false }, - "Session.Message.ProviderState4": { - "type": "object" - }, - "session.reasoning.ended": { + "session.execution.started": { "type": "object", "properties": { "id": { @@ -14982,7 +15037,7 @@ "type": { "type": "string", "enum": [ - "session.reasoning.ended" + "session.execution.started" ] }, "durable": { @@ -15026,35 +15081,10 @@ "pattern": "^ses" } ] - }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "ordinal": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "text": { - "type": "string" - }, - "state": { - "$ref": "#/components/schemas/Session.Message.ProviderState4" } }, "required": [ - "sessionID", - "assistantMessageID", - "ordinal", - "text" + "sessionID" ], "additionalProperties": false } @@ -15068,7 +15098,7 @@ ], "additionalProperties": false }, - "session.tool.input.started": { + "session.execution.succeeded": { "type": "object", "properties": { "id": { @@ -15088,7 +15118,7 @@ "type": { "type": "string", "enum": [ - "session.tool.input.started" + "session.execution.succeeded" ] }, "durable": { @@ -15132,27 +15162,10 @@ "pattern": "^ses" } ] - }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "callID": { - "type": "string" - }, - "name": { - "type": "string" } }, "required": [ - "sessionID", - "assistantMessageID", - "callID", - "name" + "sessionID" ], "additionalProperties": false } @@ -15166,7 +15179,7 @@ ], "additionalProperties": false }, - "session.tool.input.ended": { + "session.execution.failed": { "type": "object", "properties": { "id": { @@ -15186,7 +15199,7 @@ "type": { "type": "string", "enum": [ - "session.tool.input.ended" + "session.execution.failed" ] }, "durable": { @@ -15231,26 +15244,13 @@ } ] }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "callID": { - "type": "string" - }, - "text": { - "type": "string" + "error": { + "$ref": "#/components/schemas/Session.StructuredError" } }, "required": [ "sessionID", - "assistantMessageID", - "callID", - "text" + "error" ], "additionalProperties": false } @@ -15264,10 +15264,7 @@ ], "additionalProperties": false }, - "Session.Message.ProviderState5": { - "type": "object" - }, - "session.tool.called": { + "session.execution.interrupted": { "type": "object", "properties": { "id": { @@ -15287,7 +15284,7 @@ "type": { "type": "string", "enum": [ - "session.tool.called" + "session.execution.interrupted" ] }, "durable": { @@ -15332,33 +15329,18 @@ } ] }, - "assistantMessageID": { + "reason": { "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } + "enum": [ + "user", + "shutdown", + "superseded" ] - }, - "callID": { - "type": "string" - }, - "input": { - "type": "object" - }, - "executed": { - "type": "boolean" - }, - "state": { - "$ref": "#/components/schemas/Session.Message.ProviderState5" } }, "required": [ "sessionID", - "assistantMessageID", - "callID", - "input", - "executed" + "reason" ], "additionalProperties": false } @@ -15372,7 +15354,7 @@ ], "additionalProperties": false }, - "session.tool.progress": { + "session.instructions.updated": { "type": "object", "properties": { "id": { @@ -15392,7 +15374,7 @@ "type": { "type": "string", "enum": [ - "session.tool.progress" + "session.instructions.updated" ] }, "durable": { @@ -15412,7 +15394,7 @@ "version": { "type": "number", "enum": [ - 1 + 2 ] } }, @@ -15437,33 +15419,31 @@ } ] }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "callID": { - "type": "string" - }, - "structured": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLM.ToolContent" + "delta": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^[a-f0-9]{64}$" + } + ] + }, + { + "type": "string", + "enum": [ + "removed" + ] + } + ] } } }, "required": [ "sessionID", - "assistantMessageID", - "callID", - "structured", - "content" + "delta" ], "additionalProperties": false } @@ -15477,10 +15457,7 @@ ], "additionalProperties": false }, - "Session.Message.ProviderState6": { - "type": "object" - }, - "session.tool.success": { + "session.synthetic": { "type": "object", "properties": { "id": { @@ -15500,7 +15477,7 @@ "type": { "type": "string", "enum": [ - "session.tool.success" + "session.synthetic" ] }, "durable": { @@ -15545,41 +15522,19 @@ } ] }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] + "text": { + "type": "string" }, - "callID": { + "description": { "type": "string" }, - "structured": { + "metadata": { "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLM.ToolContent" - } - }, - "result": {}, - "executed": { - "type": "boolean" - }, - "resultState": { - "$ref": "#/components/schemas/Session.Message.ProviderState6" } }, "required": [ "sessionID", - "assistantMessageID", - "callID", - "structured", - "content", - "executed" + "text" ], "additionalProperties": false } @@ -15593,10 +15548,7 @@ ], "additionalProperties": false }, - "Session.Message.ProviderState7": { - "type": "object" - }, - "session.tool.failed": { + "session.skill.activated": { "type": "object", "properties": { "id": { @@ -15616,7 +15568,7 @@ "type": { "type": "string", "enum": [ - "session.tool.failed" + "session.skill.activated" ] }, "durable": { @@ -15661,34 +15613,21 @@ } ] }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "callID": { + "id": { "type": "string" }, - "error": { - "$ref": "#/components/schemas/Session.StructuredError" - }, - "result": {}, - "executed": { - "type": "boolean" + "name": { + "type": "string" }, - "resultState": { - "$ref": "#/components/schemas/Session.Message.ProviderState7" + "text": { + "type": "string" } }, "required": [ "sessionID", - "assistantMessageID", - "callID", - "error", - "executed" + "id", + "name", + "text" ], "additionalProperties": false } @@ -15702,7 +15641,81 @@ ], "additionalProperties": false }, - "session.retry.scheduled": { + "Shell.Info": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "status": { + "type": "string", + "enum": [ + "running", + "exited", + "timeout", + "killed" + ] + }, + "command": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "shell": { + "type": "string" + }, + "file": { + "type": "string" + }, + "pid": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "exit": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "started": { + "type": "number" + }, + "completed": { + "type": "number" + } + }, + "required": [ + "started" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "status", + "command", + "cwd", + "shell", + "file", + "metadata", + "time" + ], + "additionalProperties": false + }, + "session.shell.started": { "type": "object", "properties": { "id": { @@ -15722,7 +15735,7 @@ "type": { "type": "string", "enum": [ - "session.retry.scheduled" + "session.shell.started" ] }, "durable": { @@ -15767,40 +15780,13 @@ } ] }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "attempt": { - "type": "integer", - "allOf": [ - { - "exclusiveMinimum": 0 - } - ] - }, - "at": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "error": { - "$ref": "#/components/schemas/Session.StructuredError" + "shell": { + "$ref": "#/components/schemas/Shell.Info" } }, "required": [ "sessionID", - "assistantMessageID", - "attempt", - "at", - "error" + "shell" ], "additionalProperties": false } @@ -15814,7 +15800,7 @@ ], "additionalProperties": false }, - "session.compaction.admitted": { + "session.shell.ended": { "type": "object", "properties": { "id": { @@ -15834,7 +15820,7 @@ "type": { "type": "string", "enum": [ - "session.compaction.admitted" + "session.shell.ended" ] }, "durable": { @@ -15879,18 +15865,48 @@ } ] }, - "inputID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" + "shell": { + "$ref": "#/components/schemas/Shell.Info" + }, + "output": { + "type": "object", + "properties": { + "output": { + "type": "string" + }, + "cursor": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "size": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "truncated": { + "type": "boolean" } - ] + }, + "required": [ + "output", + "cursor", + "size", + "truncated" + ], + "additionalProperties": false } }, "required": [ "sessionID", - "inputID" + "shell", + "output" ], "additionalProperties": false } @@ -15904,7 +15920,7 @@ ], "additionalProperties": false }, - "session.compaction.started": { + "session.step.started": { "type": "object", "properties": { "id": { @@ -15924,7 +15940,7 @@ "type": { "type": "string", "enum": [ - "session.compaction.started" + "session.step.started" ] }, "durable": { @@ -15969,29 +15985,29 @@ } ] }, - "reason": { - "type": "string", - "enum": [ - "auto", - "manual" - ] - }, - "recent": { - "type": "string" - }, - "inputID": { + "assistantMessageID": { "type": "string", "allOf": [ { "pattern": "^msg_" } ] + }, + "agent": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "snapshot": { + "type": "string" } }, "required": [ "sessionID", - "reason", - "recent" + "assistantMessageID", + "agent", + "model" ], "additionalProperties": false } @@ -16005,7 +16021,7 @@ ], "additionalProperties": false }, - "session.compaction.ended": { + "session.step.ended": { "type": "object", "properties": { "id": { @@ -16025,7 +16041,7 @@ "type": { "type": "string", "enum": [ - "session.compaction.ended" + "session.step.ended" ] }, "durable": { @@ -16070,25 +16086,47 @@ } ] }, - "reason": { + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "finish": { "type": "string", "enum": [ - "auto", - "manual" + "stop", + "length", + "tool-calls", + "content-filter", + "error", + "unknown" ] }, - "text": { - "type": "string" + "cost": { + "$ref": "#/components/schemas/Money.USD" }, - "recent": { + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" + }, + "snapshot": { "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ "sessionID", - "reason", - "text", - "recent" + "assistantMessageID", + "finish", + "cost", + "tokens" ], "additionalProperties": false } @@ -16102,7 +16140,7 @@ ], "additionalProperties": false }, - "session.compaction.failed": { + "session.step.failed": { "type": "object", "properties": { "id": { @@ -16122,7 +16160,7 @@ "type": { "type": "string", "enum": [ - "session.compaction.failed" + "session.step.failed" ] }, "durable": { @@ -16167,29 +16205,136 @@ } ] }, - "reason": { + "assistantMessageID": { "type": "string", - "enum": [ - "auto", - "manual" + "allOf": [ + { + "pattern": "^msg_" + } ] }, "error": { "$ref": "#/components/schemas/Session.StructuredError" }, - "inputID": { + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" + }, + "snapshot": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "error" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.text.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.text.started" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { "type": "string", "allOf": [ { "pattern": "^msg_" } ] + }, + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] } }, "required": [ "sessionID", - "reason", - "error" + "assistantMessageID", + "ordinal" ], "additionalProperties": false } @@ -16203,7 +16348,10 @@ ], "additionalProperties": false }, - "session.revert.staged": { + "Session.Message.ProviderState4": { + "type": "object" + }, + "session.text.ended": { "type": "object", "properties": { "id": { @@ -16223,7 +16371,7 @@ "type": { "type": "string", "enum": [ - "session.revert.staged" + "session.text.ended" ] }, "durable": { @@ -16268,13 +16416,34 @@ } ] }, - "revert": { - "$ref": "#/components/schemas/Session.Revert" + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "text": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState4" } }, "required": [ "sessionID", - "revert" + "assistantMessageID", + "ordinal", + "text" ], "additionalProperties": false } @@ -16288,7 +16457,10 @@ ], "additionalProperties": false }, - "session.revert.cleared": { + "Session.Message.ProviderState5": { + "type": "object" + }, + "session.reasoning.started": { "type": "object", "properties": { "id": { @@ -16308,7 +16480,7 @@ "type": { "type": "string", "enum": [ - "session.revert.cleared" + "session.reasoning.started" ] }, "durable": { @@ -16352,10 +16524,31 @@ "pattern": "^ses" } ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState5" } }, "required": [ - "sessionID" + "sessionID", + "assistantMessageID", + "ordinal" ], "additionalProperties": false } @@ -16369,7 +16562,10 @@ ], "additionalProperties": false }, - "session.revert.committed": { + "Session.Message.ProviderState6": { + "type": "object" + }, + "session.reasoning.ended": { "type": "object", "properties": { "id": { @@ -16389,7 +16585,7 @@ "type": { "type": "string", "enum": [ - "session.revert.committed" + "session.reasoning.ended" ] }, "durable": { @@ -16434,18 +16630,34 @@ } ] }, - "to": { + "assistantMessageID": { "type": "string", "allOf": [ { "pattern": "^msg_" } ] + }, + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "text": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState6" } }, "required": [ "sessionID", - "to" + "assistantMessageID", + "ordinal", + "text" ], "additionalProperties": false } @@ -16459,797 +16671,2766 @@ ], "additionalProperties": false }, - "Session.Event.Durable": { - "oneOf": [ - { - "$ref": "#/components/schemas/session.agent.selected" + "session.tool.input.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] }, - { - "$ref": "#/components/schemas/session.model.selected" + "created": { + "type": "number" }, - { - "$ref": "#/components/schemas/session.moved" - }, - { - "$ref": "#/components/schemas/session.renamed" - }, - { - "$ref": "#/components/schemas/session.deleted" - }, - { - "$ref": "#/components/schemas/session.forked" - }, - { - "$ref": "#/components/schemas/session.prompt.promoted" - }, - { - "$ref": "#/components/schemas/session.prompt.admitted" - }, - { - "$ref": "#/components/schemas/session.execution.started" - }, - { - "$ref": "#/components/schemas/session.execution.succeeded" - }, - { - "$ref": "#/components/schemas/session.execution.failed" - }, - { - "$ref": "#/components/schemas/session.execution.interrupted" - }, - { - "$ref": "#/components/schemas/session.instructions.updated" - }, - { - "$ref": "#/components/schemas/session.synthetic" - }, - { - "$ref": "#/components/schemas/session.skill.activated" - }, - { - "$ref": "#/components/schemas/session.shell.started" - }, - { - "$ref": "#/components/schemas/session.shell.ended" - }, - { - "$ref": "#/components/schemas/session.step.started" - }, - { - "$ref": "#/components/schemas/session.step.ended" - }, - { - "$ref": "#/components/schemas/session.step.failed" - }, - { - "$ref": "#/components/schemas/session.text.started" - }, - { - "$ref": "#/components/schemas/session.text.ended" - }, - { - "$ref": "#/components/schemas/session.reasoning.started" - }, - { - "$ref": "#/components/schemas/session.reasoning.ended" - }, - { - "$ref": "#/components/schemas/session.tool.input.started" - }, - { - "$ref": "#/components/schemas/session.tool.input.ended" - }, - { - "$ref": "#/components/schemas/session.tool.called" - }, - { - "$ref": "#/components/schemas/session.tool.progress" - }, - { - "$ref": "#/components/schemas/session.tool.success" - }, - { - "$ref": "#/components/schemas/session.tool.failed" - }, - { - "$ref": "#/components/schemas/session.retry.scheduled" - }, - { - "$ref": "#/components/schemas/session.compaction.admitted" - }, - { - "$ref": "#/components/schemas/session.compaction.started" - }, - { - "$ref": "#/components/schemas/session.compaction.ended" - }, - { - "$ref": "#/components/schemas/session.compaction.failed" - }, - { - "$ref": "#/components/schemas/session.revert.staged" - }, - { - "$ref": "#/components/schemas/session.revert.cleared" + "metadata": { + "type": "object" }, - { - "$ref": "#/components/schemas/session.revert.committed" - } - ] - }, - "EventLog.Synced": { - "type": "object", - "properties": { "type": { "type": "string", "enum": [ - "log.synced" + "session.tool.input.started" ] }, - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] } - ] - } - }, - "required": [ - "type", - "aggregateID" - ], - "additionalProperties": false, - "description": "Marker emitted once when a log read reaches its captured watermark. The reader holds every event committed at or below seq." - }, - "SessionLogItem": { - "anyOf": [ - { - "$ref": "#/components/schemas/Session.Event.Durable" + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false }, - { - "$ref": "#/components/schemas/EventLog.Synced" - } - ] - }, - "SessionLogItemStream": { - "type": "string", - "contentSchema": { - "$ref": "#/components/schemas/SessionLogItem" - }, - "contentMediaType": "application/json" - }, - "SessionMessagesResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Session.Message.Info" - } + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "cursor": { + "data": { "type": "object", "properties": { - "previous": { - "anyOf": [ - { - "type": "string" - }, + "sessionID": { + "type": "string", + "allOf": [ { - "type": "null" + "pattern": "^ses" } ] }, - "next": { - "anyOf": [ - { - "type": "string" - }, + "assistantMessageID": { + "type": "string", + "allOf": [ { - "type": "null" + "pattern": "^msg_" } ] + }, + "callID": { + "type": "string" + }, + "name": { + "type": "string" } }, + "required": [ + "sessionID", + "assistantMessageID", + "callID", + "name" + ], "additionalProperties": false } }, "required": [ - "data", - "cursor" + "id", + "created", + "type", + "durable", + "data" ], "additionalProperties": false }, - "Model.Capabilities": { + "session.tool.input.ended": { "type": "object", "properties": { - "tools": { - "type": "boolean" + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] }, - "input": { - "type": "array", - "items": { - "type": "string" - } + "created": { + "type": "number" }, - "output": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "tools", - "input", - "output" - ], - "additionalProperties": false - }, - "Model.Variant": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "settings": { + "metadata": { "type": "object" }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } + "type": { + "type": "string", + "enum": [ + "session.tool.input.ended" + ] }, - "body": { - "type": "object" - } - }, - "required": [ - "id" - ], - "additionalProperties": false - }, - "Money.USDPerMillionTokens": { - "type": "number" - }, - "Model.Cost": { - "type": "object", - "properties": { - "tier": { + "durable": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": [ - "context" + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } ] }, - "size": { - "type": "integer" + "version": { + "type": "number", + "enum": [ + 1 + ] } }, "required": [ - "type", - "size" + "aggregateID", + "seq", + "version" ], "additionalProperties": false }, - "input": { - "$ref": "#/components/schemas/Money.USDPerMillionTokens" - }, - "output": { - "$ref": "#/components/schemas/Money.USDPerMillionTokens" + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "cache": { + "data": { "type": "object", "properties": { - "read": { - "$ref": "#/components/schemas/Money.USDPerMillionTokens" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "write": { - "$ref": "#/components/schemas/Money.USDPerMillionTokens" + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "text": { + "type": "string" } }, "required": [ - "read", - "write" + "sessionID", + "assistantMessageID", + "callID", + "text" ], "additionalProperties": false } }, "required": [ - "input", - "output", - "cache" + "id", + "created", + "type", + "durable", + "data" ], "additionalProperties": false }, - "Model.Info": { + "Session.Message.ProviderState7": { + "type": "object" + }, + "session.tool.called": { "type": "object", "properties": { "id": { - "type": "string" - }, - "modelID": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "family": { - "type": "string" - }, - "name": { - "type": "string" - }, - "package": { - "type": "string" - }, - "settings": { - "type": "object" + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } + "created": { + "type": "number" }, - "body": { + "metadata": { "type": "object" }, - "capabilities": { - "$ref": "#/components/schemas/Model.Capabilities" - }, - "variants": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Model.Variant" - } + "type": { + "type": "string", + "enum": [ + "session.tool.called" + ] }, - "time": { + "durable": { "type": "object", "properties": { - "released": { - "type": "number" + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] } }, "required": [ - "released" + "aggregateID", + "seq", + "version" ], "additionalProperties": false }, - "cost": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Model.Cost" - } - }, - "status": { - "type": "string", - "enum": [ - "alpha", - "beta", - "deprecated", - "active" - ] - }, - "enabled": { - "type": "boolean" + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "limit": { + "data": { "type": "object", "properties": { - "context": { - "type": "integer" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" }, "input": { - "type": "integer" + "type": "object" }, - "output": { - "type": "integer" + "executed": { + "type": "boolean" + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState7" } }, "required": [ - "context", - "output" + "sessionID", + "assistantMessageID", + "callID", + "input", + "executed" ], "additionalProperties": false } }, "required": [ "id", - "modelID", - "providerID", - "name", - "capabilities", - "variants", - "time", - "cost", - "status", - "enabled", - "limit" + "created", + "type", + "durable", + "data" ], "additionalProperties": false }, - "GenerateTextResponse": { + "Session.Message.ProviderState8": { + "type": "object" + }, + "session.tool.success": { "type": "object", "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.tool.success" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 2 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, "data": { "type": "object", "properties": { - "text": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { "type": "string" + }, + "content": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/LLM.ToolContent" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/LLM.ToolContent" + } + }, + "metadata": { + "type": "object" + }, + "executed": { + "type": "boolean" + }, + "resultState": { + "$ref": "#/components/schemas/Session.Message.ProviderState8" } }, "required": [ - "text" + "sessionID", + "assistantMessageID", + "callID", + "content", + "executed" ], "additionalProperties": false } }, "required": [ + "id", + "created", + "type", + "durable", "data" ], "additionalProperties": false }, - "ProviderV2.Info": { + "Session.Message.ProviderState9": { + "type": "object" + }, + "session.tool.failed": { "type": "object", "properties": { "id": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] }, - "integrationID": { - "type": "string" + "created": { + "type": "number" }, - "name": { - "type": "string" + "metadata": { + "type": "object" }, - "disabled": { - "type": "boolean" + "type": { + "type": "string", + "enum": [ + "session.tool.failed" + ] }, - "package": { - "type": "string" + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 2 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false }, - "settings": { - "type": "object" + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "headers": { + "data": { "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "body": { - "type": "object" + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + }, + "content": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/LLM.ToolContent" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/LLM.ToolContent" + } + }, + "metadata": { + "type": "object" + }, + "executed": { + "type": "boolean" + }, + "resultState": { + "$ref": "#/components/schemas/Session.Message.ProviderState9" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "callID", + "error", + "executed" + ], + "additionalProperties": false } }, "required": [ "id", - "name", - "package" + "created", + "type", + "durable", + "data" ], "additionalProperties": false }, - "ProviderNotFoundError": { + "session.retry.scheduled": { "type": "object", "properties": { - "_tag": { + "id": { "type": "string", - "enum": [ - "ProviderNotFoundError" + "allOf": [ + { + "pattern": "^evt_" + } ] }, - "providerID": { - "type": "string" + "created": { + "type": "number" }, - "message": { - "type": "string" - } - }, - "required": [ - "_tag", - "providerID", - "message" - ], - "additionalProperties": false - }, - "Integration.When": { - "type": "object", - "properties": { - "key": { - "type": "string" + "metadata": { + "type": "object" }, - "op": { + "type": { "type": "string", "enum": [ - "eq", - "neq" + "session.retry.scheduled" ] }, - "value": { - "type": "string" + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "attempt": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "at": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "attempt", + "at", + "error" + ], + "additionalProperties": false } }, "required": [ - "key", - "op", - "value" + "id", + "created", + "type", + "durable", + "data" ], "additionalProperties": false }, - "Integration.TextPrompt": { + "session.compaction.admitted": { "type": "object", "properties": { - "type": { + "id": { "type": "string", - "enum": [ - "text" + "allOf": [ + { + "pattern": "^evt_" + } ] }, - "key": { - "type": "string" - }, - "message": { - "type": "string" + "created": { + "type": "number" }, - "placeholder": { - "type": "string" + "metadata": { + "type": "object" }, - "when": { - "$ref": "#/components/schemas/Integration.When" - } - }, - "required": [ - "type", - "key", - "message" - ], - "additionalProperties": false - }, - "Integration.SelectPrompt": { - "type": "object", - "properties": { "type": { "type": "string", "enum": [ - "select" + "session.compaction.admitted" ] }, - "key": { - "type": "string" + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false }, - "message": { - "type": "string" + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "options": { - "type": "array", - "items": { - "type": "object", - "properties": { - "label": { - "type": "string" - }, - "value": { - "type": "string" - }, - "hint": { - "type": "string" - } + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "required": [ - "label", - "value" - ], - "additionalProperties": false - } - }, - "when": { - "$ref": "#/components/schemas/Integration.When" + "inputID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": [ + "sessionID", + "inputID" + ], + "additionalProperties": false } }, "required": [ + "id", + "created", "type", - "key", - "message", - "options" + "durable", + "data" ], "additionalProperties": false }, - "Integration.OAuthMethod": { + "session.compaction.started": { "type": "object", "properties": { "id": { - "type": "string" - }, - "type": { "type": "string", - "enum": [ - "oauth" + "allOf": [ + { + "pattern": "^evt_" + } ] }, - "label": { - "type": "string" + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, - "prompts": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/Integration.TextPrompt" - }, - { - "$ref": "#/components/schemas/Integration.SelectPrompt" - } - ] - } - } - }, - "required": [ - "id", - "type", - "label" - ], - "additionalProperties": false - }, - "Integration.KeyMethod": { - "type": "object", - "properties": { "type": { "type": "string", "enum": [ - "key" + "session.compaction.started" ] }, - "label": { - "type": "string" + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "recent": { + "type": "string" + }, + "inputID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": [ + "sessionID", + "reason", + "recent" + ], + "additionalProperties": false } }, "required": [ - "type" + "id", + "created", + "type", + "durable", + "data" ], "additionalProperties": false }, - "Integration.EnvMethod": { + "session.compaction.ended": { "type": "object", "properties": { - "type": { + "id": { "type": "string", - "enum": [ - "env" + "allOf": [ + { + "pattern": "^evt_" + } ] }, - "names": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "type", - "names" - ], - "additionalProperties": false - }, - "Integration.Method": { - "anyOf": [ - { - "$ref": "#/components/schemas/Integration.OAuthMethod" + "created": { + "type": "number" }, - { - "$ref": "#/components/schemas/Integration.KeyMethod" + "metadata": { + "type": "object" }, - { - "$ref": "#/components/schemas/Integration.EnvMethod" - } - ] - }, - "Connection.CredentialInfo": { - "type": "object", - "properties": { "type": { "type": "string", "enum": [ - "credential" + "session.compaction.ended" ] }, - "id": { - "type": "string" + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false }, - "label": { - "type": "string" + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "text": { + "type": "string" + }, + "recent": { + "type": "string" + } + }, + "required": [ + "sessionID", + "reason", + "text", + "recent" + ], + "additionalProperties": false } }, "required": [ - "type", "id", - "label" + "created", + "type", + "durable", + "data" ], "additionalProperties": false }, - "Connection.EnvInfo": { + "session.compaction.failed": { "type": "object", "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, "type": { "type": "string", "enum": [ - "env" + "session.compaction.failed" ] }, - "name": { - "type": "string" - } - }, - "required": [ - "type", - "name" - ], - "additionalProperties": false - }, - "Connection.Info": { - "anyOf": [ - { - "$ref": "#/components/schemas/Connection.CredentialInfo" + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false }, - { - "$ref": "#/components/schemas/Connection.EnvInfo" + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + }, + "inputID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": [ + "sessionID", + "reason", + "error" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.revert.staged": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.revert.staged" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "revert": { + "$ref": "#/components/schemas/Session.Revert" + } + }, + "required": [ + "sessionID", + "revert" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.revert.cleared": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.revert.cleared" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + } + }, + "required": [ + "sessionID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.revert.committed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.revert.committed" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "to": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": [ + "sessionID", + "to" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.usage.recorded": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.usage.recorded" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "source": { + "type": "string", + "enum": [ + "title", + "compaction" + ] + }, + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" + } + }, + "required": [ + "sessionID", + "source", + "cost", + "tokens" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "Session.Event.Durable": { + "oneOf": [ + { + "$ref": "#/components/schemas/session.agent.selected" + }, + { + "$ref": "#/components/schemas/session.model.selected" + }, + { + "$ref": "#/components/schemas/session.moved" + }, + { + "$ref": "#/components/schemas/session.renamed" + }, + { + "$ref": "#/components/schemas/session.deleted" + }, + { + "$ref": "#/components/schemas/session.forked" + }, + { + "$ref": "#/components/schemas/session.input.promoted" + }, + { + "$ref": "#/components/schemas/session.input.admitted" + }, + { + "$ref": "#/components/schemas/session.execution.started" + }, + { + "$ref": "#/components/schemas/session.execution.succeeded" + }, + { + "$ref": "#/components/schemas/session.execution.failed" + }, + { + "$ref": "#/components/schemas/session.execution.interrupted" + }, + { + "$ref": "#/components/schemas/session.instructions.updated" + }, + { + "$ref": "#/components/schemas/session.synthetic" + }, + { + "$ref": "#/components/schemas/session.skill.activated" + }, + { + "$ref": "#/components/schemas/session.shell.started" + }, + { + "$ref": "#/components/schemas/session.shell.ended" + }, + { + "$ref": "#/components/schemas/session.step.started" + }, + { + "$ref": "#/components/schemas/session.step.ended" + }, + { + "$ref": "#/components/schemas/session.step.failed" + }, + { + "$ref": "#/components/schemas/session.text.started" + }, + { + "$ref": "#/components/schemas/session.text.ended" + }, + { + "$ref": "#/components/schemas/session.reasoning.started" + }, + { + "$ref": "#/components/schemas/session.reasoning.ended" + }, + { + "$ref": "#/components/schemas/session.tool.input.started" + }, + { + "$ref": "#/components/schemas/session.tool.input.ended" + }, + { + "$ref": "#/components/schemas/session.tool.called" + }, + { + "$ref": "#/components/schemas/session.tool.success" + }, + { + "$ref": "#/components/schemas/session.tool.failed" + }, + { + "$ref": "#/components/schemas/session.retry.scheduled" + }, + { + "$ref": "#/components/schemas/session.compaction.admitted" + }, + { + "$ref": "#/components/schemas/session.compaction.started" + }, + { + "$ref": "#/components/schemas/session.compaction.ended" + }, + { + "$ref": "#/components/schemas/session.compaction.failed" + }, + { + "$ref": "#/components/schemas/session.revert.staged" + }, + { + "$ref": "#/components/schemas/session.revert.cleared" + }, + { + "$ref": "#/components/schemas/session.revert.committed" + }, + { + "$ref": "#/components/schemas/session.usage.recorded" + } + ] + }, + "EventLog.Synced": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "log.synced" + ] + }, + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "type", + "aggregateID" + ], + "additionalProperties": false, + "description": "Marker emitted once when a log read reaches its captured watermark. The reader holds every event committed at or below seq." + }, + "SessionLogItem": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Event.Durable" + }, + { + "$ref": "#/components/schemas/EventLog.Synced" + } + ] + }, + "SessionLogItemJsonString": { + "type": "string", + "contentSchema": { + "$ref": "#/components/schemas/SessionLogItem" + }, + "contentMediaType": "application/json" + }, + "SessionMessagesResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Session.Message.Info" + } + }, + "cursor": { + "type": "object", + "properties": { + "previous": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "next": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "data", + "cursor" + ], + "additionalProperties": false + }, + "Model.ReasoningField": { + "anyOf": [ + { + "type": "string", + "enum": [ + "reasoning", + "reasoning_content", + "reasoning_text" + ] + }, + { + "type": "string" + } + ] + }, + "Model.Compatibility": { + "type": "object", + "properties": { + "reasoningField": { + "$ref": "#/components/schemas/Model.ReasoningField" + } + }, + "additionalProperties": false + }, + "Model.Capabilities": { + "type": "object", + "properties": { + "tools": { + "type": "boolean" + }, + "input": { + "type": "array", + "items": { + "type": "string" + } + }, + "output": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "tools", + "input", + "output" + ], + "additionalProperties": false + }, + "Model.Variant": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "settings": { + "type": "object" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "Money.USDPerMillionTokens": { + "type": "number" + }, + "Model.Cost": { + "type": "object", + "properties": { + "tier": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "context" + ] + }, + "size": { + "type": "integer" + } + }, + "required": [ + "type", + "size" + ], + "additionalProperties": false + }, + "input": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + "output": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + "write": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + } + }, + "required": [ + "read", + "write" + ], + "additionalProperties": false + } + }, + "required": [ + "input", + "output", + "cache" + ], + "additionalProperties": false + }, + "Model.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "modelID": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "family": { + "type": "string" + }, + "name": { + "type": "string" + }, + "compatibility": { + "$ref": "#/components/schemas/Model.Compatibility" + }, + "package": { + "type": "string" + }, + "settings": { + "type": "object" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + }, + "capabilities": { + "$ref": "#/components/schemas/Model.Capabilities" + }, + "variants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Model.Variant" + } + }, + "time": { + "type": "object", + "properties": { + "released": { + "type": "number" + } + }, + "required": [ + "released" + ], + "additionalProperties": false + }, + "cost": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Model.Cost" + } + }, + "status": { + "type": "string", + "enum": [ + "alpha", + "beta", + "deprecated", + "active" + ] + }, + "enabled": { + "type": "boolean" + }, + "limit": { + "type": "object", + "properties": { + "context": { + "type": "integer" + }, + "input": { + "type": "integer" + }, + "output": { + "type": "integer" + } + }, + "required": [ + "context", + "output" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "modelID", + "providerID", + "name", + "capabilities", + "variants", + "time", + "cost", + "status", + "enabled", + "limit" + ], + "additionalProperties": false + }, + "GenerateTextResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": [ + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "data" + ], + "additionalProperties": false + }, + "ProviderV2.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "integrationID": { + "type": "string" + }, + "name": { + "type": "string" + }, + "disabled": { + "type": "boolean" + }, + "package": { + "type": "string" + }, + "settings": { + "type": "object" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + } + }, + "required": [ + "id", + "name", + "package" + ], + "additionalProperties": false + }, + "ProviderNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "ProviderNotFoundError" + ] + }, + "providerID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "providerID", + "message" + ], + "additionalProperties": false + }, + "Integration.When": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "op": { + "type": "string", + "enum": [ + "eq", + "neq" + ] + }, + "value": { + "type": "string" + } + }, + "required": [ + "key", + "op", + "value" + ], + "additionalProperties": false + }, + "Integration.TextPrompt": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "text" + ] + }, + "key": { + "type": "string" + }, + "message": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "when": { + "$ref": "#/components/schemas/Integration.When" + } + }, + "required": [ + "type", + "key", + "message" + ], + "additionalProperties": false + }, + "Integration.SelectPrompt": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "select" + ] + }, + "key": { + "type": "string" + }, + "message": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + }, + "hint": { + "type": "string" + } + }, + "required": [ + "label", + "value" + ], + "additionalProperties": false + } + }, + "when": { + "$ref": "#/components/schemas/Integration.When" + } + }, + "required": [ + "type", + "key", + "message", + "options" + ], + "additionalProperties": false + }, + "Integration.OAuthMethod": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "oauth" + ] + }, + "label": { + "type": "string" + }, + "prompts": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Integration.TextPrompt" + }, + { + "$ref": "#/components/schemas/Integration.SelectPrompt" + } + ] + } + } + }, + "required": [ + "id", + "type", + "label" + ], + "additionalProperties": false + }, + "Integration.CommandMethod": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "command" + ] + }, + "label": { + "type": "string" + }, + "command": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "type", + "label", + "command" + ], + "additionalProperties": false + }, + "Integration.KeyMethod": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "key" + ] + }, + "label": { + "type": "string" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "Integration.EnvMethod": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "env" + ] + }, + "names": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "type", + "names" + ], + "additionalProperties": false + }, + "Integration.Method": { + "anyOf": [ + { + "$ref": "#/components/schemas/Integration.OAuthMethod" + }, + { + "$ref": "#/components/schemas/Integration.CommandMethod" + }, + { + "$ref": "#/components/schemas/Integration.KeyMethod" + }, + { + "$ref": "#/components/schemas/Integration.EnvMethod" + } + ] + }, + "Connection.CredentialInfo": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "credential" + ] + }, + "id": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": [ + "type", + "id", + "label" + ], + "additionalProperties": false + }, + "Connection.EnvInfo": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "env" + ] + }, + "name": { + "type": "string" + } + }, + "required": [ + "type", + "name" + ], + "additionalProperties": false + }, + "Connection.Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/Connection.CredentialInfo" + }, + { + "$ref": "#/components/schemas/Connection.EnvInfo" + } + ] + }, + "Integration.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "methods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Integration.Method" + } + }, + "connections": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Connection.Info" + } + } + }, + "required": [ + "id", + "name", + "methods", + "connections" + ], + "additionalProperties": false + }, + "Integration.Attempt": { + "type": "object", + "properties": { + "attemptID": { + "type": "string" + }, + "url": { + "type": "string" + }, + "instructions": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "auto", + "code" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "attemptID", + "url", + "instructions", + "mode", + "time" + ], + "additionalProperties": false + }, + "Integration.AttemptStatus": { + "anyOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "pending" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "time" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "complete" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "time" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "failed" + ] + }, + "message": { + "type": "string" + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "message", + "time" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "expired" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "time" + ], + "additionalProperties": false } ] }, - "Integration.Info": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "methods": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Integration.Method" - } - }, - "connections": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Connection.Info" - } - } - }, - "required": [ - "id", - "name", - "methods", - "connections" - ], - "additionalProperties": false - }, - "Integration.Attempt": { + "Integration.CommandAttempt": { "type": "object", "properties": { "attemptID": { "type": "string" }, - "url": { - "type": "string" - }, - "instructions": { - "type": "string" - }, - "mode": { - "type": "string", - "enum": [ - "auto", - "code" - ] - }, "time": { "type": "object", "properties": { @@ -17337,14 +19518,11 @@ }, "required": [ "attemptID", - "url", - "instructions", - "mode", "time" ], "additionalProperties": false }, - "Integration.AttemptStatus": { + "Integration.CommandAttemptStatus": { "anyOf": [ { "type": "object", @@ -17355,6 +19533,9 @@ "pending" ] }, + "message": { + "type": "string" + }, "time": { "type": "object", "properties": { @@ -17777,114 +19958,413 @@ ] } }, - "required": [ - "status" - ], + "required": [ + "status" + ], + "additionalProperties": false + }, + "Mcp.Status.Disabled": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "disabled" + ] + } + }, + "required": [ + "status" + ], + "additionalProperties": false + }, + "Mcp.Status.Failed": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "failed" + ] + }, + "error": { + "type": "string" + } + }, + "required": [ + "status", + "error" + ], + "additionalProperties": false + }, + "Mcp.Status.NeedsAuth": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "needs_auth" + ] + } + }, + "required": [ + "status" + ], + "additionalProperties": false + }, + "Mcp.Status.NeedsClientRegistration": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "needs_client_registration" + ] + }, + "error": { + "type": "string" + } + }, + "required": [ + "status", + "error" + ], + "additionalProperties": false + }, + "Mcp.Server": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "status": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.Status.Connected" + }, + { + "$ref": "#/components/schemas/Mcp.Status.Pending" + }, + { + "$ref": "#/components/schemas/Mcp.Status.Disabled" + }, + { + "$ref": "#/components/schemas/Mcp.Status.Failed" + }, + { + "$ref": "#/components/schemas/Mcp.Status.NeedsAuth" + }, + { + "$ref": "#/components/schemas/Mcp.Status.NeedsClientRegistration" + } + ] + }, + "integrationID": { + "type": "string" + } + }, + "required": [ + "name", + "status" + ], + "additionalProperties": false + }, + "Mcp.TimeoutConfig": { + "type": "object", + "properties": { + "startup": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ], + "description": "Maximum time in milliseconds to establish and initialize the MCP server." + }, + "catalog": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ], + "description": "Maximum time in milliseconds to wait for MCP discovery requests such as tools/list and prompts/list." + }, + "execution": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ], + "description": "Maximum time in milliseconds to wait for MCP tool and prompt execution." + } + }, "additionalProperties": false }, - "Mcp.Status.Disabled": { + "Mcp.LocalConfig": { "type": "object", "properties": { - "status": { + "type": { "type": "string", "enum": [ - "disabled" + "local" ] - } - }, - "required": [ - "status" - ], - "additionalProperties": false - }, - "Mcp.Status.Failed": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "failed" + }, + "command": { + "type": "array", + "items": { + "type": "string" + } + }, + "cwd": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Working directory for the MCP server process. Relative paths resolve from the workspace directory." + }, + "environment": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } ] }, - "error": { - "type": "string" + "disabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "codemode": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Expose this server's tools through Code Mode. Defaults to true." + }, + "timeout": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.TimeoutConfig" + }, + { + "type": "null" + } + ] } }, "required": [ - "status", - "error" + "type", + "command" ], "additionalProperties": false }, - "Mcp.Status.NeedsAuth": { + "Mcp.OAuthConfig": { "type": "object", "properties": { - "status": { - "type": "string", - "enum": [ - "needs_auth" + "client_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "client_secret": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "scope": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "callback_port": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 1, + "maximum": 65535 + } + ] + }, + { + "type": "null" + } + ] + }, + "redirect_uri": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } ] } }, - "required": [ - "status" - ], "additionalProperties": false }, - "Mcp.Status.NeedsClientRegistration": { + "Mcp.RemoteConfig": { "type": "object", "properties": { - "status": { + "type": { "type": "string", "enum": [ - "needs_client_registration" + "remote" ] }, - "error": { - "type": "string" - } - }, - "required": [ - "status", - "error" - ], - "additionalProperties": false - }, - "Mcp.Server": { - "type": "object", - "properties": { - "name": { + "url": { "type": "string" }, - "status": { + "headers": { "anyOf": [ { - "$ref": "#/components/schemas/Mcp.Status.Connected" + "type": "object", + "additionalProperties": { + "type": "string" + } }, { - "$ref": "#/components/schemas/Mcp.Status.Pending" + "type": "null" + } + ] + }, + "oauth": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.OAuthConfig" + }, + { + "type": "boolean", + "enum": [ + false + ] + } + ] }, { - "$ref": "#/components/schemas/Mcp.Status.Disabled" + "type": "null" + } + ] + }, + "disabled": { + "anyOf": [ + { + "type": "boolean" }, { - "$ref": "#/components/schemas/Mcp.Status.Failed" + "type": "null" + } + ] + }, + "codemode": { + "anyOf": [ + { + "type": "boolean" }, { - "$ref": "#/components/schemas/Mcp.Status.NeedsAuth" + "type": "null" + } + ], + "description": "Expose this server's tools through Code Mode. Defaults to true." + }, + "timeout": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.TimeoutConfig" }, { - "$ref": "#/components/schemas/Mcp.Status.NeedsClientRegistration" + "type": "null" } ] + } + }, + "required": [ + "type", + "url" + ], + "additionalProperties": false + }, + "McpServerNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "McpServerNotFoundError" + ] }, - "integrationID": { + "server": { + "type": "string" + }, + "message": { "type": "string" } }, "required": [ - "name", - "status" + "_tag", + "server", + "message" ], "additionalProperties": false }, @@ -18664,65 +21144,70 @@ ], "additionalProperties": false }, - "Form.FormInfo": { + "Form.ExternalField": { "type": "object", "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^frm_" - } - ] - }, - "sessionID": { - "type": "string" - }, - "title": { + "key": { "type": "string" }, - "metadata": { - "$ref": "#/components/schemas/Form.Metadata" - }, - "mode": { + "type": { "type": "string", "enum": [ - "form" + "external" ] }, - "fields": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/Form.StringField" - }, - { - "$ref": "#/components/schemas/Form.NumberField" - }, - { - "$ref": "#/components/schemas/Form.IntegerField" - }, - { - "$ref": "#/components/schemas/Form.BooleanField" - }, - { - "$ref": "#/components/schemas/Form.MultiselectField" - } - ] - } + "url": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" } }, "required": [ - "id", - "sessionID", - "title", - "mode", - "fields" + "key", + "type", + "url" ], "additionalProperties": false }, - "Form.UrlInfo": { + "Form.Field": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.StringField" + }, + { + "$ref": "#/components/schemas/Form.NumberField" + }, + { + "$ref": "#/components/schemas/Form.IntegerField" + }, + { + "$ref": "#/components/schemas/Form.BooleanField" + }, + { + "$ref": "#/components/schemas/Form.MultiselectField" + }, + { + "$ref": "#/components/schemas/Form.ExternalField" + } + ] + }, + "Form.Fields": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/Form.Field" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/Form.Field" + } + }, + "Form.Info": { "type": "object", "properties": { "id": { @@ -18742,22 +21227,15 @@ "metadata": { "$ref": "#/components/schemas/Form.Metadata" }, - "mode": { - "type": "string", - "enum": [ - "url" - ] - }, - "url": { - "type": "string" + "fields": { + "$ref": "#/components/schemas/Form.Fields" } }, "required": [ "id", "sessionID", "title", - "mode", - "url" + "fields" ], "additionalProperties": false }, @@ -18785,56 +21263,13 @@ "metadata": { "$ref": "#/components/schemas/Form.Metadata" }, - "mode": { - "type": "string", - "enum": [ - "form", - "url" - ] - }, "fields": { - "anyOf": [ - { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/Form.StringField" - }, - { - "$ref": "#/components/schemas/Form.NumberField" - }, - { - "$ref": "#/components/schemas/Form.IntegerField" - }, - { - "$ref": "#/components/schemas/Form.BooleanField" - }, - { - "$ref": "#/components/schemas/Form.MultiselectField" - } - ] - } - }, - { - "type": "null" - } - ] - }, - "url": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "$ref": "#/components/schemas/Form.Fields" } }, "required": [ "title", - "mode" + "fields" ], "additionalProperties": false }, @@ -22811,6 +25246,75 @@ ], "additionalProperties": false }, + "session.tool.progress": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.tool.progress" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "callID", + "metadata" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, "session.compaction.delta": { "type": "object", "properties": { @@ -23689,7 +26193,7 @@ "type": "object", "properties": { "info": { - "$ref": "#/components/schemas/Shell" + "$ref": "#/components/schemas/Shell.Info" } }, "required": [ @@ -23744,29 +26248,7 @@ ] }, "exit": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "NaN" - ] - }, - { - "type": "string", - "enum": [ - "Infinity" - ] - }, - { - "type": "string", - "enum": [ - "-Infinity" - ] - } - ] + "type": "number" }, "status": { "type": "string", @@ -24567,73 +27049,49 @@ "type": "string" } } - }, - "required": [ - "key", - "type", - "options" - ], - "additionalProperties": false - }, - "Form.FormInfo1": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^frm_" - } - ] - }, - "sessionID": { - "type": "string" - }, - "title": { - "type": "string" - }, - "metadata": { - "$ref": "#/components/schemas/Form.Metadata1" - }, - "mode": { - "type": "string", - "enum": [ - "form" - ] - }, - "fields": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/Form.StringField1" - }, - { - "$ref": "#/components/schemas/Form.NumberField1" - }, - { - "$ref": "#/components/schemas/Form.IntegerField1" - }, - { - "$ref": "#/components/schemas/Form.BooleanField1" - }, - { - "$ref": "#/components/schemas/Form.MultiselectField1" - } - ] - } - } - }, - "required": [ - "id", - "sessionID", - "title", - "mode", - "fields" + }, + "required": [ + "key", + "type", + "options" ], "additionalProperties": false }, - "Form.UrlInfo1": { + "Form.Field1": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.StringField1" + }, + { + "$ref": "#/components/schemas/Form.NumberField1" + }, + { + "$ref": "#/components/schemas/Form.IntegerField1" + }, + { + "$ref": "#/components/schemas/Form.BooleanField1" + }, + { + "$ref": "#/components/schemas/Form.MultiselectField1" + }, + { + "$ref": "#/components/schemas/Form.ExternalField" + } + ] + }, + "Form.Fields1": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/Form.Field1" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/Form.Field1" + } + }, + "Form.Info1": { "type": "object", "properties": { "id": { @@ -24653,22 +27111,15 @@ "metadata": { "$ref": "#/components/schemas/Form.Metadata1" }, - "mode": { - "type": "string", - "enum": [ - "url" - ] - }, - "url": { - "type": "string" + "fields": { + "$ref": "#/components/schemas/Form.Fields1" } }, "required": [ "id", "sessionID", "title", - "mode", - "url" + "fields" ], "additionalProperties": false }, @@ -24702,14 +27153,7 @@ "type": "object", "properties": { "form": { - "anyOf": [ - { - "$ref": "#/components/schemas/Form.FormInfo1" - }, - { - "$ref": "#/components/schemas/Form.UrlInfo1" - } - ] + "$ref": "#/components/schemas/Form.Info1" } }, "required": [ @@ -24889,6 +27333,51 @@ ], "additionalProperties": false }, + "websearch.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "websearch.updated" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, "SessionStatus": { "anyOf": [ { @@ -26262,10 +28751,10 @@ "$ref": "#/components/schemas/session.forked" }, { - "$ref": "#/components/schemas/session.prompt.promoted" + "$ref": "#/components/schemas/session.input.promoted" }, { - "$ref": "#/components/schemas/session.prompt.admitted" + "$ref": "#/components/schemas/session.input.admitted" }, { "$ref": "#/components/schemas/session.execution.started" @@ -26438,6 +28927,9 @@ { "$ref": "#/components/schemas/form.cancelled" }, + { + "$ref": "#/components/schemas/websearch.updated" + }, { "$ref": "#/components/schemas/session.status" }, @@ -26494,7 +28986,7 @@ } ] }, - "V2EventStream": { + "V2EventJsonString": { "type": "string", "contentSchema": { "$ref": "#/components/schemas/V2Event" @@ -26564,7 +29056,7 @@ ], "additionalProperties": false }, - "Shell1": { + "Shell.Info1": { "type": "object", "properties": { "id": { @@ -26605,41 +29097,7 @@ ] }, "exit": { - "anyOf": [ - { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "NaN" - ] - }, - { - "type": "string", - "enum": [ - "Infinity" - ] - }, - { - "type": "string", - "enum": [ - "-Infinity" - ] - } - ] - }, - { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] - } - ] + "type": "number" }, "metadata": { "type": "object" @@ -26648,78 +29106,10 @@ "type": "object", "properties": { "started": { - "anyOf": [ - { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "NaN" - ] - }, - { - "type": "string", - "enum": [ - "Infinity" - ] - }, - { - "type": "string", - "enum": [ - "-Infinity" - ] - } - ] - }, - { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] - } - ] + "type": "number" }, "completed": { - "anyOf": [ - { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "NaN" - ] - }, - { - "type": "string", - "enum": [ - "Infinity" - ] - }, - { - "type": "string", - "enum": [ - "-Infinity" - ] - } - ] - }, - { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] - } - ] + "type": "number" } }, "required": [ @@ -27023,6 +29413,69 @@ "working", "branch" ] + }, + "WebSearch.Provider": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "additionalProperties": false + }, + "WebSearch.Result": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "title": { + "type": "string" + }, + "content": { + "type": "string" + }, + "time": { + "type": "object", + "properties": { + "published": { + "type": "number" + } + }, + "additionalProperties": false + } + }, + "required": [ + "url", + "time" + ], + "additionalProperties": false + }, + "WebSearch.Response": { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebSearch.Result" + } + } + }, + "required": [ + "providerID", + "results" + ], + "additionalProperties": false } }, "securitySchemes": {} @@ -27130,6 +29583,10 @@ }, { "name": "debug" + }, + { + "name": "websearch", + "description": "Location-scoped web search routes." } ] } diff --git a/packages/www/package.json b/packages/www/package.json index e55370a9eb8c..1774df103538 100644 --- a/packages/www/package.json +++ b/packages/www/package.json @@ -7,8 +7,8 @@ "dev": "bun run generate && blume dev --host --port 3000", "build": "bun run generate && blume build && bun script/prepare-cloudflare.ts", "deploy": "wrangler deploy --config dist/server/wrangler.json", - "generate": "bun script/generate-theme-tokens.ts", - "check:generated": "bun script/generate-theme-tokens.ts --check", + "generate": "bun script/generate-theme-tokens.ts && bun script/generate-openapi.ts", + "check:generated": "bun script/generate-theme-tokens.ts --check && bun script/generate-openapi.ts --check", "typecheck": "blume check", "validate": "blume validate", "doctor": "blume doctor" diff --git a/packages/www/public/openapi.json b/packages/www/public/openapi.json index d430f5d21dd8..1c8b3dc9412d 100644 --- a/packages/www/public/openapi.json +++ b/packages/www/public/openapi.json @@ -16,36 +16,11 @@ "security": [], "responses": { "200": { - "description": "Success", + "description": "ServiceHealth", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "healthy": { - "type": "boolean", - "enum": [ - true - ] - }, - "version": { - "type": "string" - }, - "pid": { - "type": "integer", - "allOf": [ - { - "exclusiveMinimum": 0 - } - ] - } - }, - "required": [ - "healthy", - "version", - "pid" - ], - "additionalProperties": false + "$ref": "#/components/schemas/ServiceHealth" } } } @@ -71,10 +46,64 @@ } } }, - "description": "Check whether the API server is ready to accept requests.", + "description": "Report the owning server process and its application status.", "summary": "Check server health" } }, + "/api/service/stop": { + "post": { + "tags": [ + "health" + ], + "operationId": "v2.health.stop", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "ServiceStopResponse", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceStopResponse" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Request graceful shutdown of one exact managed server instance.", + "summary": "Stop the managed server", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceStopRequest" + } + } + }, + "required": true + } + } + }, "/api/server": { "get": { "tags": [ @@ -1406,35 +1435,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "destination": { - "type": "object", - "properties": { - "directory": { - "type": "string" - } - }, - "required": [ - "directory" - ], - "additionalProperties": false - }, - "moveChanges": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "destination" - ], - "additionalProperties": false + "$ref": "#/components/schemas/Location.Ref" } } }, @@ -1473,7 +1474,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/SessionInput.Admitted" + "$ref": "#/components/schemas/SessionPending.User" } }, "required": [ @@ -1562,8 +1563,23 @@ } ] }, - "prompt": { - "$ref": "#/components/schemas/PromptInput" + "text": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PromptInput.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + }, + "metadata": { + "type": "object" }, "delivery": { "anyOf": [ @@ -1591,7 +1607,7 @@ } }, "required": [ - "prompt" + "text" ], "additionalProperties": false } @@ -1632,7 +1648,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/SessionInput.Admitted" + "$ref": "#/components/schemas/SessionPending.User" } }, "required": [ @@ -1953,8 +1969,24 @@ ], "security": [], "responses": { - "204": { - "description": "" + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SessionPending.Synthetic" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } }, "400": { "description": "InvalidRequestError", @@ -1992,9 +2024,19 @@ } } } + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictError" + } + } + } } }, - "description": "Append a synthetic message to a session and resume execution.", + "description": "Durably admit synthetic session input and schedule execution unless resume is false.", "summary": "Add synthetic message", "requestBody": { "content": { @@ -2002,6 +2044,21 @@ "schema": { "type": "object", "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] + }, "text": { "type": "string" }, @@ -2018,6 +2075,20 @@ "metadata": { "type": "object" }, + "delivery": { + "anyOf": [ + { + "type": "string", + "enum": [ + "steer", + "queue" + ] + }, + { + "type": "null" + } + ] + }, "resume": { "anyOf": [ { @@ -2173,7 +2244,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/SessionInput.Compaction" + "$ref": "#/components/schemas/SessionPending.Compaction" } }, "required": [ @@ -2744,6 +2815,93 @@ "summary": "Get session context" } }, + "/api/session/{sessionID}/pending": { + "get": { + "tags": [ + "session" + ], + "operationId": "v2.session.pending.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionPending.Info" + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "List durable admitted session work not yet visible in projected history, ordered by admission. Includes unpromoted user and synthetic inputs and unhandled compaction barriers. The runner owns consumption; items disappear once promoted or handled.", + "summary": "List pending session work" + } + }, "/api/session/{sessionID}/instructions/entries": { "get": { "tags": [ @@ -2901,6 +3059,16 @@ } } } + }, + "413": { + "description": "InstructionEntryValueTooLargeError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstructionEntryValueTooLargeError" + } + } + } } }, "description": "Attach or replace one durable instruction entry. Changes announce as updates at the next step boundary.", @@ -2998,6 +3166,110 @@ "summary": "Remove instruction entry" } }, + "/api/session/{sessionID}/generate": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.generate", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "SessionGenerateResponse", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionGenerateResponse" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Generate transient text from the current session context without mutating session history.", + "summary": "Generate text from session context", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "prompt": { + "type": "string" + } + }, + "required": [ + "prompt" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, "/api/experimental/session/{sessionID}/log": { "get": { "tags": [ @@ -3076,7 +3348,7 @@ "type": "string" }, "data": { - "$ref": "#/components/schemas/SessionLogItemStream" + "$ref": "#/components/schemas/SessionLogItemJsonString" } }, "required": [ @@ -3694,7 +3966,7 @@ } } }, - "description": "Retrieve available models ordered by release date.", + "description": "Retrieve the current snapshot of available models ordered by release date. The snapshot may precede initial plugin settlement.", "summary": "List models" } }, @@ -4395,6 +4667,110 @@ "summary": "Get integration" } }, + "/api/experimental/integration/wellknown": { + "post": { + "tags": [ + "integration" + ], + "operationId": "v2.experimental.integration.wellknown.add", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Discover and persist an experimental wellknown integration source.", + "summary": "Add wellknown integration", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, "/api/integration/{integrationID}/connect/key": { "post": { "tags": [ @@ -4522,7 +4898,7 @@ "tags": [ "integration" ], - "operationId": "v2.integration.connect.oauth", + "operationId": "v2.integration.oauth.connect", "parameters": [ { "name": "integrationID", @@ -4666,13 +5042,21 @@ } } }, - "/api/integration/attempt/{attemptID}": { + "/api/integration/{integrationID}/connect/oauth/{attemptID}": { "get": { "tags": [ "integration" ], - "operationId": "v2.integration.attempt.status", + "operationId": "v2.integration.oauth.status", "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, { "name": "attemptID", "in": "path", @@ -4775,8 +5159,16 @@ "tags": [ "integration" ], - "operationId": "v2.integration.attempt.cancel", + "operationId": "v2.integration.oauth.cancel", "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, { "name": "attemptID", "in": "path", @@ -4856,13 +5248,21 @@ "summary": "Cancel OAuth connection" } }, - "/api/integration/attempt/{attemptID}/complete": { + "/api/integration/{integrationID}/connect/oauth/{attemptID}/complete": { "post": { "tags": [ "integration" ], - "operationId": "v2.integration.attempt.complete", + "operationId": "v2.integration.oauth.complete", "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, { "name": "attemptID", "in": "path", @@ -4972,13 +5372,21 @@ } } }, - "/api/mcp": { - "get": { + "/api/integration/{integrationID}/connect/command": { + "post": { "tags": [ - "mcp" + "integration" ], - "operationId": "v2.mcp.list", + "operationId": "v2.integration.command.connect", "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, { "name": "location", "in": "query", @@ -5033,10 +5441,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Mcp.Server" - } + "$ref": "#/components/schemas/Integration.CommandAttempt" } }, "required": [ @@ -5053,7 +5458,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] } } } @@ -5069,17 +5481,62 @@ } } }, - "description": "Retrieve configured MCP servers and their connection status.", - "summary": "List MCP servers" - } - }, - "/api/mcp/resource": { - "get": { - "tags": [ - "mcp" - ], - "operationId": "v2.mcp.resource.catalog", + "description": "Start a command authentication attempt.", + "summary": "Begin command connection", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "methodID": { + "type": "string" + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "methodID" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/integration/{integrationID}/connect/command/{attemptID}": { + "get": { + "tags": [ + "integration" + ], + "operationId": "v2.integration.command.status", "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "attemptID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, { "name": "location", "in": "query", @@ -5134,7 +5591,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/Mcp.ResourceCatalog" + "$ref": "#/components/schemas/Integration.CommandAttemptStatus" } }, "required": [ @@ -5167,19 +5624,25 @@ } } }, - "description": "Retrieve resources and resource templates from connected MCP servers.", - "summary": "List MCP resources" - } - }, - "/api/credential/{credentialID}": { - "patch": { + "description": "Poll the current status and output of a command authentication attempt.", + "summary": "Get command attempt status" + }, + "delete": { "tags": [ - "credential" + "integration" ], - "operationId": "v2.credential.update", + "operationId": "v2.integration.command.cancel", "parameters": [ { - "name": "credentialID", + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "attemptID", "in": "path", "schema": { "type": "string" @@ -5253,42 +5716,17 @@ } } }, - "description": "Update a stored credential label.", - "summary": "Update credential", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "label": { - "type": "string" - } - }, - "required": [ - "label" - ], - "additionalProperties": false - } - } - }, - "required": true - } - }, - "delete": { + "description": "Cancel a command authentication attempt and terminate its process.", + "summary": "Cancel command connection" + } + }, + "/api/mcp": { + "get": { "tags": [ - "credential" + "mcp" ], - "operationId": "v2.credential.remove", + "operationId": "v2.mcp.list", "parameters": [ - { - "name": "credentialID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, { "name": "location", "in": "query", @@ -5331,53 +5769,29 @@ } ], "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Remove a stored integration credential.", - "summary": "Remove credential" - } - }, - "/api/project": { - "get": { - "tags": [ - "project" - ], - "operationId": "v2.project.list", - "parameters": [], - "security": [], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Project" - } + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Mcp.Server" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false } } } @@ -5403,17 +5817,25 @@ } } }, - "description": "List known projects.", - "summary": "List projects" + "description": "Retrieve configured MCP servers and their connection status.", + "summary": "List MCP servers" } }, - "/api/project/current": { - "get": { + "/api/mcp/{server}": { + "put": { "tags": [ - "project" + "mcp" ], - "operationId": "v2.project.current", + "operationId": "v2.mcp.add", "parameters": [ + { + "name": "server", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, { "name": "location", "in": "query", @@ -5457,15 +5879,8 @@ ], "security": [], "responses": { - "200": { - "description": "Project.Current", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Project.Current" - } - } - } + "204": { + "description": "" }, "400": { "description": "InvalidRequestError", @@ -5488,19 +5903,43 @@ } } }, - "description": "Resolve the project for the requested location.", - "summary": "Get current project" - } - }, - "/api/project/{projectID}/directories": { - "get": { + "description": "Add an MCP server at runtime or replace an existing one, connecting it immediately.", + "summary": "Add MCP server", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "config": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.LocalConfig" + }, + { + "$ref": "#/components/schemas/Mcp.RemoteConfig" + } + ] + } + }, + "required": [ + "config" + ], + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { "tags": [ - "project" + "mcp" ], - "operationId": "v2.project.directories", + "operationId": "v2.mcp.remove", "parameters": [ { - "name": "projectID", + "name": "server", "in": "path", "schema": { "type": "string" @@ -5550,15 +5989,8 @@ ], "security": [], "responses": { - "200": { - "description": "Project.Directories", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Project.Directories" - } - } - } + "204": { + "description": "" }, "400": { "description": "InvalidRequestError", @@ -5579,19 +6011,37 @@ } } } + }, + "404": { + "description": "McpServerNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/McpServerNotFoundError" + } + } + } } }, - "description": "List known local absolute directories for a project.", - "summary": "List project directories" + "description": "Stop an MCP server and remove it from the runtime set until restart.", + "summary": "Remove MCP server" } }, - "/api/form/request": { - "get": { + "/api/mcp/{server}/connect": { + "post": { "tags": [ - "form" + "mcp" ], - "operationId": "v2.form.request.list", + "operationId": "v2.mcp.connect", "parameters": [ + { + "name": "server", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, { "name": "location", "in": "query", @@ -5635,38 +6085,8 @@ ], "security": [], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/Location.Info" - }, - "data": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/Form.FormInfo" - }, - { - "$ref": "#/components/schemas/Form.UrlInfo" - } - ] - } - } - }, - "required": [ - "location", - "data" - ], - "additionalProperties": false - } - } - } + "204": { + "description": "" }, "400": { "description": "InvalidRequestError", @@ -5687,58 +6107,82 @@ } } } + }, + "404": { + "description": "McpServerNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/McpServerNotFoundError" + } + } + } } }, - "description": "Retrieve pending forms for a location.", - "summary": "List pending form requests" + "description": "Connect an MCP server at runtime, overriding a disabled configuration until restart.", + "summary": "Connect MCP server" } }, - "/api/session/{sessionID}/form": { - "get": { + "/api/mcp/{server}/disconnect": { + "post": { "tags": [ - "form" + "mcp" ], - "operationId": "v2.session.form.list", + "operationId": "v2.mcp.disconnect", "parameters": [ { - "name": "sessionID", + "name": "server", "in": "path", "schema": { "type": "string" }, "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { "type": "object", "properties": { - "data": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/Form.FormInfo" - }, - { - "$ref": "#/components/schemas/Form.UrlInfo" - } - ] - } + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] } }, - "required": [ - "data" - ], "additionalProperties": false + }, + { + "type": "null" } - } - } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" }, "400": { "description": "InvalidRequestError", @@ -5761,39 +6205,66 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "McpServerNotFoundError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/McpServerNotFoundError" } } } } }, - "description": "Retrieve pending forms for a session.", - "summary": "List session forms" - }, - "post": { + "description": "Disconnect an MCP server at runtime, removing its tools until reconnected.", + "summary": "Disconnect MCP server" + } + }, + "/api/mcp/resource": { + "get": { "tags": [ - "form" + "mcp" ], - "operationId": "v2.session.form.create", + "operationId": "v2.mcp.resource.catalog", "parameters": [ { - "name": "sessionID", - "in": "path", + "name": "location", + "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": true + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], @@ -5805,18 +6276,15 @@ "schema": { "type": "object", "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, "data": { - "anyOf": [ - { - "$ref": "#/components/schemas/Form.FormInfo" - }, - { - "$ref": "#/components/schemas/Form.UrlInfo" - } - ] + "$ref": "#/components/schemas/Mcp.ResourceCatalog" } }, "required": [ + "location", "data" ], "additionalProperties": false @@ -5829,14 +6297,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestError1" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } @@ -5850,58 +6311,21 @@ } } } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - }, - "409": { - "description": "ConflictError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConflictError" - } - } - } } }, - "description": "Create a form for a session.", - "summary": "Create session form", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Form.CreatePayload" - } - } - }, - "required": true - } + "description": "Retrieve resources and resource templates from connected MCP servers.", + "summary": "List MCP resources" } }, - "/api/session/{sessionID}/form/{formID}": { - "get": { + "/api/credential/{credentialID}": { + "patch": { "tags": [ - "form" + "credential" ], - "operationId": "v2.session.form.get", + "operationId": "v2.credential.update", "parameters": [ { - "name": "sessionID", + "name": "credentialID", "in": "path", "schema": { "type": "string" @@ -5909,46 +6333,50 @@ "required": true }, { - "name": "formID", - "in": "path", + "name": "location", + "in": "query", "schema": { - "type": "string", - "allOf": [ + "anyOf": [ { - "pattern": "^frm_" - } - ] - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { "type": "object", "properties": { - "data": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { "anyOf": [ { - "$ref": "#/components/schemas/Form.FormInfo" + "type": "string" }, { - "$ref": "#/components/schemas/Form.UrlInfo" + "type": "null" } ] } }, - "required": [ - "data" - ], "additionalProperties": false + }, + { + "type": "null" } - } - } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" }, "400": { "description": "InvalidRequestError", @@ -5969,41 +6397,38 @@ } } } - }, - "404": { - "description": "SessionNotFoundError | FormNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } } }, - "description": "Retrieve a form for a session.", - "summary": "Get session form" - } - }, - "/api/session/{sessionID}/form/{formID}/state": { - "get": { + "description": "Update a stored credential label.", + "summary": "Update credential", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "label": { + "type": "string" + } + }, + "required": [ + "label" + ], + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { "tags": [ - "form" + "credential" ], - "operationId": "v2.session.form.state", + "operationId": "v2.credential.remove", "parameters": [ { - "name": "sessionID", + "name": "credentialID", "in": "path", "schema": { "type": "string" @@ -6011,39 +6436,50 @@ "required": true }, { - "name": "formID", - "in": "path", + "name": "location", + "in": "query", "schema": { - "type": "string", - "allOf": [ + "anyOf": [ { - "pattern": "^frm_" + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" } ] }, - "required": true + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/Form.State" - } - }, - "required": [ - "data" - ], - "additionalProperties": false - } - } - } + "204": { + "description": "" }, "400": { "description": "InvalidRequestError", @@ -6064,79 +6500,40 @@ } } } - }, - "404": { - "description": "SessionNotFoundError | FormNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } } }, - "description": "Retrieve the current state for a form.", - "summary": "Get form state" + "description": "Remove a stored integration credential.", + "summary": "Remove credential" } }, - "/api/session/{sessionID}/form/{formID}/reply": { - "post": { + "/api/project": { + "get": { "tags": [ - "form" - ], - "operationId": "v2.session.form.reply", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "formID", - "in": "path", - "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^frm_" - } - ] - }, - "required": true - } + "project" ], + "operationId": "v2.project.list", + "parameters": [], "security": [], "responses": { - "204": { - "description": "" + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Project" + } + } + } + } }, "400": { - "description": "FormInvalidAnswerError | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormInvalidAnswerError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } @@ -6150,148 +6547,112 @@ } } } - }, - "404": { - "description": "SessionNotFoundError | FormNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - }, - "409": { - "description": "FormAlreadySettledError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FormAlreadySettledError" - } - } - } } }, - "description": "Submit an answer to a pending form.", - "summary": "Reply to form", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Form.Reply" - } - } - }, - "required": true - } + "description": "List known projects.", + "summary": "List projects" } }, - "/api/session/{sessionID}/form/{formID}/cancel": { - "post": { + "/api/project/current": { + "get": { "tags": [ - "form" + "project" ], - "operationId": "v2.session.form.cancel", + "operationId": "v2.project.current", "parameters": [ { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "formID", - "in": "path", + "name": "location", + "in": "query", "schema": { - "type": "string", - "allOf": [ + "anyOf": [ { - "pattern": "^frm_" + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" } ] }, - "required": true + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", + "200": { + "description": "Project.Current", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnauthorizedError" + "$ref": "#/components/schemas/Project.Current" } } } }, - "404": { - "description": "SessionNotFoundError | FormNotFoundError", + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "409": { - "description": "FormAlreadySettledError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FormAlreadySettledError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Cancel a pending form.", - "summary": "Cancel form" + "description": "Resolve the project for the requested location.", + "summary": "Get current project" } }, - "/api/permission/request": { + "/api/project/{projectID}/directories": { "get": { "tags": [ - "permission" + "project" ], - "operationId": "v2.permission.request.list", + "operationId": "v2.project.directories", "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, { "name": "location", "in": "query", @@ -6336,27 +6697,11 @@ "security": [], "responses": { "200": { - "description": "Success", + "description": "Project.Directories", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/Location.Info" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionV2.Request" - } - } - }, - "required": [ - "location", - "data" - ], - "additionalProperties": false + "$ref": "#/components/schemas/Project.Directories" } } } @@ -6382,31 +6727,56 @@ } } }, - "description": "Retrieve pending permission requests for a location.", - "summary": "List pending permission requests" + "description": "List known local absolute directories for a project.", + "summary": "List project directories" } }, - "/api/permission/saved": { + "/api/form/request": { "get": { "tags": [ - "permission" + "form" ], - "operationId": "v2.permission.saved.list", + "operationId": "v2.form.request.list", "parameters": [ { - "name": "projectID", + "name": "location", "in": "query", "schema": { "anyOf": [ { - "type": "string" + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false }, { "type": "null" } ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], @@ -6418,14 +6788,18 @@ "schema": { "type": "object", "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, "data": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionSaved.Info" + "$ref": "#/components/schemas/Form.Info" } } }, "required": [ + "location", "data" ], "additionalProperties": false @@ -6454,19 +6828,19 @@ } } }, - "description": "Retrieve saved permissions, optionally filtered by project.", - "summary": "List saved permissions" + "description": "Retrieve pending forms for a location.", + "summary": "List pending form requests" } }, - "/api/permission/saved/{id}": { - "delete": { + "/api/session/{sessionID}/form": { + "get": { "tags": [ - "permission" + "form" ], - "operationId": "v2.permission.saved.remove", + "operationId": "v2.session.form.list", "parameters": [ { - "name": "id", + "name": "sessionID", "in": "path", "schema": { "type": "string" @@ -6476,8 +6850,27 @@ ], "security": [], "responses": { - "204": { - "description": "" + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Info" + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } }, "400": { "description": "InvalidRequestError", @@ -6498,29 +6891,39 @@ } } } - } - }, - "description": "Remove a saved permission by ID.", - "summary": "Remove saved permission" - } - }, - "/api/session/{sessionID}/permission": { + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve pending forms for a session.", + "summary": "List session forms" + }, "post": { "tags": [ - "permission" + "form" ], - "operationId": "v2.session.permission.create", + "operationId": "v2.session.form.create", "parameters": [ { "name": "sessionID", "in": "path", "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] + "type": "string" }, "required": true } @@ -6535,25 +6938,7 @@ "type": "object", "properties": { "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^per" - } - ] - }, - "effect": { - "$ref": "#/components/schemas/PermissionV2.Effect" - } - }, - "required": [ - "id", - "effect" - ], - "additionalProperties": false + "$ref": "#/components/schemas/Form.Info" } }, "required": [ @@ -6569,7 +6954,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] } } } @@ -6600,88 +6992,55 @@ } } } + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictError" + } + } + } } }, - "description": "Evaluate and, when approval is required, create a permission request for a session.", - "summary": "Create permission request", + "description": "Create a form for a session.", + "summary": "Create session form", "requestBody": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "id": { - "anyOf": [ - { - "type": "string", - "allOf": [ - { - "pattern": "^per" - } - ] - }, - { - "type": "null" - } - ] - }, - "action": { - "type": "string" - }, - "resources": { - "type": "array", - "items": { - "type": "string" - } - }, - "save": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "source": { - "$ref": "#/components/schemas/PermissionV2.Source" - }, - "agent": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "action", - "resources" - ], - "additionalProperties": false + "$ref": "#/components/schemas/Form.CreatePayload" } } }, "required": true } - }, + } + }, + "/api/session/{sessionID}/form/{formID}": { "get": { "tags": [ - "permission" + "form" ], - "operationId": "v2.session.permission.list", + "operationId": "v2.session.form.get", "parameters": [ { "name": "sessionID", "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", "schema": { "type": "string", "allOf": [ { - "pattern": "^ses" + "pattern": "^frm_" } ] }, @@ -6698,10 +7057,7 @@ "type": "object", "properties": { "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionV2.Request" - } + "$ref": "#/components/schemas/Form.Info" } }, "required": [ @@ -6733,11 +7089,14 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | FormNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundError" + }, { "$ref": "#/components/schemas/SessionNotFoundError" }, @@ -6750,38 +7109,33 @@ } } }, - "description": "Retrieve pending permission requests owned by a session.", - "summary": "List session permission requests" + "description": "Retrieve a form for a session.", + "summary": "Get session form" } }, - "/api/session/{sessionID}/permission/{requestID}": { + "/api/session/{sessionID}/form/{formID}/state": { "get": { "tags": [ - "permission" + "form" ], - "operationId": "v2.session.permission.get", + "operationId": "v2.session.form.state", "parameters": [ { "name": "sessionID", "in": "path", "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] + "type": "string" }, "required": true }, { - "name": "requestID", + "name": "formID", "in": "path", "schema": { "type": "string", "allOf": [ { - "pattern": "^per" + "pattern": "^frm_" } ] }, @@ -6798,7 +7152,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/PermissionV2.Request" + "$ref": "#/components/schemas/Form.State" } }, "required": [ @@ -6830,13 +7184,13 @@ } }, "404": { - "description": "SessionNotFoundError | PermissionNotFoundError", + "description": "SessionNotFoundError | FormNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/PermissionNotFoundError" + "$ref": "#/components/schemas/FormNotFoundError" }, { "$ref": "#/components/schemas/SessionNotFoundError" @@ -6850,38 +7204,33 @@ } } }, - "description": "Retrieve a pending permission request owned by a session.", - "summary": "Get permission request" + "description": "Retrieve the current state for a form.", + "summary": "Get form state" } }, - "/api/session/{sessionID}/permission/{requestID}/reply": { + "/api/session/{sessionID}/form/{formID}/reply": { "post": { "tags": [ - "permission" + "form" ], - "operationId": "v2.session.permission.reply", + "operationId": "v2.session.form.reply", "parameters": [ { "name": "sessionID", "in": "path", "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] + "type": "string" }, "required": true }, { - "name": "requestID", + "name": "formID", "in": "path", "schema": { "type": "string", "allOf": [ { - "pattern": "^per" + "pattern": "^frm_" } ] }, @@ -6894,11 +7243,18 @@ "description": "" }, "400": { - "description": "InvalidRequestError", + "description": "FormInvalidAnswerError | InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/FormInvalidAnswerError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] } } } @@ -6914,13 +7270,13 @@ } }, "404": { - "description": "SessionNotFoundError | PermissionNotFoundError", + "description": "SessionNotFoundError | FormNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/PermissionNotFoundError" + "$ref": "#/components/schemas/FormNotFoundError" }, { "$ref": "#/components/schemas/SessionNotFoundError" @@ -6932,34 +7288,25 @@ } } } + }, + "409": { + "description": "FormAlreadySettledError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormAlreadySettledError" + } + } + } } }, - "description": "Respond to a pending permission request owned by a session.", - "summary": "Reply to pending permission request", + "description": "Submit an answer to a pending form.", + "summary": "Reply to form", "requestBody": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "reply": { - "$ref": "#/components/schemas/PermissionV2.Reply" - }, - "message": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "reply" - ], - "additionalProperties": false + "$ref": "#/components/schemas/Form.Reply" } } }, @@ -6967,66 +7314,39 @@ } } }, - "/api/fs/read/*": { - "get": { + "/api/session/{sessionID}/form/{formID}/cancel": { + "post": { "tags": [ - "filesystem" + "form" ], - "operationId": "v2.fs.read", + "operationId": "v2.session.form.cancel", "parameters": [ { - "name": "location", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "workspace": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ { - "type": "null" + "pattern": "^frm_" } ] }, - "required": false, - "style": "deepObject", - "explode": true + "required": true } ], "security": [], "responses": { - "200": { - "description": "Success", - "content": { - "application/octet-stream": { - "schema": { - "type": "string", - "format": "binary" - } - } - } + "204": { + "description": "" }, "400": { "description": "InvalidRequestError", @@ -7047,18 +7367,48 @@ } } } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "FormAlreadySettledError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormAlreadySettledError" + } + } + } } }, - "description": "Serve one file relative to the requested location.", - "summary": "Read file" + "description": "Cancel a pending form.", + "summary": "Cancel form" } }, - "/api/fs/list": { + "/api/permission/request": { "get": { "tags": [ - "filesystem" + "permission" ], - "operationId": "v2.fs.list", + "operationId": "v2.permission.request.list", "parameters": [ { "name": "location", @@ -7099,21 +7449,6 @@ "required": false, "style": "deepObject", "explode": true - }, - { - "name": "path", - "in": "query", - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "required": false } ], "security": [], @@ -7131,7 +7466,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/FileSystem.Entry" + "$ref": "#/components/schemas/PermissionV2.Request" } } }, @@ -7165,79 +7500,19 @@ } } }, - "description": "List direct children of one directory relative to the requested location.", - "summary": "List directory" + "description": "Retrieve pending permission requests for a location.", + "summary": "List pending permission requests" } }, - "/api/fs/find": { + "/api/permission/saved": { "get": { "tags": [ - "filesystem" + "permission" ], - "operationId": "v2.fs.find", + "operationId": "v2.permission.saved.list", "parameters": [ { - "name": "location", - "in": "query", - "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "workspace": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - { - "type": "null" - } - ] - }, - "required": false, - "style": "deepObject", - "explode": true - }, - { - "name": "query", - "in": "query", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "type", - "in": "query", - "schema": { - "type": "string", - "enum": [ - "file", - "directory" - ] - }, - "required": false - }, - { - "name": "limit", + "name": "projectID", "in": "query", "schema": { "anyOf": [ @@ -7261,18 +7536,14 @@ "schema": { "type": "object", "properties": { - "location": { - "$ref": "#/components/schemas/Location.Info" - }, "data": { "type": "array", "items": { - "$ref": "#/components/schemas/FileSystem.Entry" + "$ref": "#/components/schemas/PermissionSaved.Info" } } }, "required": [ - "location", "data" ], "additionalProperties": false @@ -7301,56 +7572,75 @@ } } }, - "description": "Find recursively ranked filesystem entries relative to the requested location.", - "summary": "Find files" + "description": "Retrieve saved permissions, optionally filtered by project.", + "summary": "List saved permissions" } }, - "/api/command": { - "get": { + "/api/permission/saved/{id}": { + "delete": { "tags": [ - "command" + "permission" ], - "operationId": "v2.command.list", + "operationId": "v2.permission.saved.remove", "parameters": [ { - "name": "location", - "in": "query", + "name": "id", + "in": "path", "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "workspace": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - { - "type": "null" - } - ] + "type": "string" }, - "required": false, - "style": "deepObject", - "explode": true + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Remove a saved permission by ID.", + "summary": "Remove saved permission" + } + }, + "/api/session/{sessionID}/permission": { + "post": { + "tags": [ + "permission" + ], + "operationId": "v2.session.permission.create", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true } ], "security": [], @@ -7362,18 +7652,29 @@ "schema": { "type": "object", "properties": { - "location": { - "$ref": "#/components/schemas/Location.Info" - }, "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Command.Info" - } + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "effect": { + "$ref": "#/components/schemas/PermissionV2.Effect" + } + }, + "required": [ + "id", + "effect" + ], + "additionalProperties": false } }, "required": [ - "location", "data" ], "additionalProperties": false @@ -7400,58 +7701,109 @@ } } } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } } }, - "description": "Retrieve currently registered commands.", - "summary": "List commands" - } - }, - "/api/skill": { + "description": "Evaluate and, when approval is required, create a permission request for a session.", + "summary": "Create permission request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + { + "type": "null" + } + ] + }, + "action": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "save": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": { + "type": "object" + }, + "source": { + "$ref": "#/components/schemas/PermissionV2.Source" + }, + "agent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "action", + "resources" + ], + "additionalProperties": false + } + } + }, + "required": true + } + }, "get": { "tags": [ - "skill" + "permission" ], - "operationId": "v2.skill.list", + "operationId": "v2.session.permission.list", "parameters": [ { - "name": "location", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "workspace": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, + "type": "string", + "allOf": [ { - "type": "null" + "pattern": "^ses" } ] }, - "required": false, - "style": "deepObject", - "explode": true + "required": true } ], "security": [], @@ -7463,18 +7815,14 @@ "schema": { "type": "object", "properties": { - "location": { - "$ref": "#/components/schemas/Location.Info" - }, "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Skill.Info" + "$ref": "#/components/schemas/PermissionV2.Request" } } }, "required": [ - "location", "data" ], "additionalProperties": false @@ -7501,127 +7849,80 @@ } } } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } } }, - "description": "Retrieve currently registered skills.", - "summary": "List skills" + "description": "Retrieve pending permission requests owned by a session.", + "summary": "List session permission requests" } }, - "/api/event": { + "/api/session/{sessionID}/permission/{requestID}": { "get": { "tags": [ - "event" + "permission" ], - "operationId": "v2.event.subscribe", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "text/event-stream": { - "schema": { - "type": "object", - "properties": { - "id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "event": { - "type": "string" - }, + "operationId": "v2.session.permission.get", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "data": { - "$ref": "#/components/schemas/V2EventStream" + "$ref": "#/components/schemas/PermissionV2.Request" } }, "required": [ - "id", - "event", "data" ], "additionalProperties": false - }, - "x-effect-stream": { - "encoding": "sse", - "causeSchema": { - "type": "array", - "items": { - "anyOf": [ - { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "Fail" - ] - }, - "error": { - "not": {} - } - }, - "required": [ - "_tag", - "error" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "Die" - ] - }, - "defect": {} - }, - "required": [ - "_tag", - "defect" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "Interrupt" - ] - }, - "fiberId": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "_tag", - "fiberId" - ], - "additionalProperties": false - } - ] - } - }, - "errorSchema": { - "not": {} - }, - "failureEvent": "effect/httpapi/stream/failure" } } } @@ -7645,87 +7946,70 @@ } } } + }, + "404": { + "description": "SessionNotFoundError | PermissionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/PermissionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } } }, - "description": "Subscribe to native event payloads for the server. Volatile by contract: a slow consumer overflows and fails the stream, and events during disconnection are missed.", - "summary": "Subscribe to events" + "description": "Retrieve a pending permission request owned by a session.", + "summary": "Get permission request" } }, - "/api/pty": { - "get": { + "/api/session/{sessionID}/permission/{requestID}/reply": { + "post": { "tags": [ - "pty" + "permission" ], - "operationId": "v2.pty.list", + "operationId": "v2.session.permission.reply", "parameters": [ { - "name": "location", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "anyOf": [ + "type": "string", + "allOf": [ { - "type": "object", - "properties": { - "directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "workspace": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ { - "type": "null" + "pattern": "^per" } ] }, - "required": false, - "style": "deepObject", - "explode": true + "required": true } ], "security": [], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/Location.Info" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pty" - } - } - }, - "required": [ - "location", - "data" - ], - "additionalProperties": false - } - } - } + "204": { + "description": "" }, "400": { "description": "InvalidRequestError", @@ -7746,16 +8030,67 @@ } } } + }, + "404": { + "description": "SessionNotFoundError | PermissionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/PermissionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } } }, - "description": "List PTY sessions for a location, including exited sessions retained until removal.", - "summary": "List PTY sessions" - }, - "post": { + "description": "Respond to a pending permission request owned by a session.", + "summary": "Reply to pending permission request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "reply": { + "$ref": "#/components/schemas/PermissionV2.Reply" + }, + "message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "reply" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/fs/read/*": { + "get": { "tags": [ - "pty" + "filesystem" ], - "operationId": "v2.pty.create", + "operationId": "v2.fs.read", "parameters": [ { "name": "location", @@ -7803,22 +8138,10 @@ "200": { "description": "Success", "content": { - "application/json": { + "application/octet-stream": { "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/Location.Info" - }, - "data": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": [ - "location", - "data" - ], - "additionalProperties": false + "type": "string", + "format": "binary" } } } @@ -7844,64 +8167,17 @@ } } }, - "description": "Create a pseudo-terminal session for a location.", - "summary": "Create PTY session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "command": { - "type": "string" - }, - "args": { - "type": "array", - "items": { - "type": "string" - } - }, - "cwd": { - "type": "string" - }, - "title": { - "type": "string" - }, - "env": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "additionalProperties": false - } - } - }, - "required": true - } + "description": "Serve one file relative to the requested location.", + "summary": "Read file" } }, - "/api/pty/{ptyID}": { + "/api/fs/list": { "get": { "tags": [ - "pty" + "filesystem" ], - "operationId": "v2.pty.get", + "operationId": "v2.fs.list", "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^pty" - } - ] - }, - "required": true - }, { "name": "location", "in": "query", @@ -7941,6 +8217,21 @@ "required": false, "style": "deepObject", "explode": true + }, + { + "name": "path", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false } ], "security": [], @@ -7956,7 +8247,10 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/Pty" + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystem.Entry" + } } }, "required": [ @@ -7987,40 +8281,19 @@ } } } - }, - "404": { - "description": "PtyNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } } }, - "description": "Get one PTY session, including its exit code once exited.", - "summary": "Get PTY session" - }, - "put": { + "description": "List direct children of one directory relative to the requested location.", + "summary": "List directory" + } + }, + "/api/fs/find": { + "get": { "tags": [ - "pty" + "filesystem" ], - "operationId": "v2.pty.update", + "operationId": "v2.fs.find", "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^pty" - } - ] - }, - "required": true - }, { "name": "location", "in": "query", @@ -8060,6 +8333,41 @@ "required": false, "style": "deepObject", "explode": true + }, + { + "name": "query", + "in": "query", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "file", + "directory" + ] + }, + "required": false + }, + { + "name": "limit", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false } ], "security": [], @@ -8075,7 +8383,10 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/Pty" + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystem.Entry" + } } }, "required": [ @@ -8106,82 +8417,19 @@ } } } - }, - "404": { - "description": "PtyNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } } }, - "description": "Update the title or viewport size of one PTY session.", - "summary": "Update PTY session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "size": { - "type": "object", - "properties": { - "rows": { - "type": "integer", - "allOf": [ - { - "exclusiveMinimum": 0 - } - ] - }, - "cols": { - "type": "integer", - "allOf": [ - { - "exclusiveMinimum": 0 - } - ] - } - }, - "required": [ - "rows", - "cols" - ], - "additionalProperties": false - } - }, - "additionalProperties": false - } - } - }, - "required": true - } - }, - "delete": { + "description": "Find recursively ranked filesystem entries relative to the requested location.", + "summary": "Find files" + } + }, + "/api/command": { + "get": { "tags": [ - "pty" + "command" ], - "operationId": "v2.pty.remove", + "operationId": "v2.command.list", "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^pty" - } - ] - }, - "required": true - }, { "name": "location", "in": "query", @@ -8225,64 +8473,64 @@ ], "security": [], "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", + "200": { + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidRequestError" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Command.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false } } } }, - "401": { - "description": "UnauthorizedError", + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnauthorizedError" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "404": { - "description": "PtyNotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Terminate and remove one PTY session.", - "summary": "Remove PTY session" + "description": "Retrieve currently registered commands.", + "summary": "List commands" } }, - "/api/pty/{ptyID}/connect-token": { - "post": { + "/api/skill": { + "get": { "tags": [ - "pty" + "skill" ], - "operationId": "v2.pty.connect.token", + "operationId": "v2.skill.list", "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^pty" - } - ] - }, - "required": true - }, { "name": "location", "in": "query", @@ -8337,7 +8585,10 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/PtyTicket.ConnectToken" + "type": "array", + "items": { + "$ref": "#/components/schemas/Skill.Info" + } } }, "required": [ @@ -8368,151 +8619,168 @@ } } } + } + }, + "description": "Retrieve currently registered skills.", + "summary": "List skills" + } + }, + "/api/event": { + "get": { + "tags": [ + "event" + ], + "operationId": "v2.event.subscribe", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "text/event-stream": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "event": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/V2EventJsonString" + } + }, + "required": [ + "id", + "event", + "data" + ], + "additionalProperties": false + }, + "x-effect-stream": { + "encoding": "sse", + "causeSchema": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Fail" + ] + }, + "error": { + "not": {} + } + }, + "required": [ + "_tag", + "error" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Die" + ] + }, + "defect": {} + }, + "required": [ + "_tag", + "defect" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Interrupt" + ] + }, + "fiberId": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "fiberId" + ], + "additionalProperties": false + } + ] + } + }, + "errorSchema": { + "not": {} + }, + "failureEvent": "effect/httpapi/stream/failure" + } + } + } }, - "403": { - "description": "ForbiddenError", + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ForbiddenError" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "404": { - "description": "PtyNotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Create a short-lived single-use ticket for opening a PTY WebSocket connection.", - "summary": "Create PTY WebSocket token" + "description": "Subscribe to native event payloads for the server. Volatile by contract: a slow consumer overflows and fails the stream, and events during disconnection are missed.", + "summary": "Subscribe to events" } }, - "/api/pty/{ptyID}/connect": { + "/api/pty": { "get": { "tags": [ "pty" ], - "operationId": "v2.pty.connect", + "operationId": "v2.pty.list", "parameters": [ { - "name": "ptyID", - "in": "path", + "name": "location", + "in": "query", "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^pty" - } - ] - }, - "required": true - }, - { - "in": "query", - "name": "location[directory]", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "location[workspace]", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "cursor", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "ticket", - "schema": { - "type": "string" - } - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "403": { - "description": "ForbiddenError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ForbiddenError" - } - } - } - }, - "404": { - "description": "PtyNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } - } - }, - "description": "Establish a WebSocket connection streaming PTY output and accepting terminal input.", - "summary": "Connect to PTY session", - "x-websocket": true - } - }, - "/api/shell": { - "get": { - "tags": [ - "shell" - ], - "operationId": "v2.shell.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "anyOf": [ + "anyOf": [ { "type": "object", "properties": { @@ -8564,7 +8832,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Shell1" + "$ref": "#/components/schemas/Pty" } } }, @@ -8598,14 +8866,14 @@ } } }, - "description": "List currently running shell commands for a location. Exited commands are not included.", - "summary": "List running shell commands" + "description": "List PTY sessions for a location, including exited sessions retained until removal.", + "summary": "List PTY sessions" }, "post": { "tags": [ - "shell" + "pty" ], - "operationId": "v2.shell.create", + "operationId": "v2.pty.create", "parameters": [ { "name": "location", @@ -8661,7 +8929,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/Shell1" + "$ref": "#/components/schemas/Pty" } }, "required": [ @@ -8694,8 +8962,8 @@ } } }, - "description": "Spawn one non-interactive shell command for a location. Combined stdout/stderr is captured to a file pageable via output.", - "summary": "Run shell command", + "description": "Create a pseudo-terminal session for a location.", + "summary": "Create PTY session", "requestBody": { "content": { "application/json": { @@ -8705,25 +8973,25 @@ "command": { "type": "string" }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, "cwd": { "type": "string" }, - "timeout": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] + "title": { + "type": "string" }, - "metadata": { - "type": "object" + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } } }, - "required": [ - "command", - "timeout" - ], "additionalProperties": false } } @@ -8732,21 +9000,21 @@ } } }, - "/api/shell/{id}": { + "/api/pty/{ptyID}": { "get": { "tags": [ - "shell" + "pty" ], - "operationId": "v2.shell.get", + "operationId": "v2.pty.get", "parameters": [ { - "name": "id", + "name": "ptyID", "in": "path", "schema": { "type": "string", "allOf": [ { - "pattern": "^sh_" + "pattern": "^pty" } ] }, @@ -8806,7 +9074,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/Shell1" + "$ref": "#/components/schemas/Pty" } }, "required": [ @@ -8839,33 +9107,33 @@ } }, "404": { - "description": "ShellNotFoundError", + "description": "PtyNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ShellNotFoundError" + "$ref": "#/components/schemas/PtyNotFoundError" } } } } }, - "description": "Get one shell command, including its status and exit code once exited.", - "summary": "Get shell command" + "description": "Get one PTY session, including its exit code once exited.", + "summary": "Get PTY session" }, - "delete": { + "put": { "tags": [ - "shell" + "pty" ], - "operationId": "v2.shell.remove", + "operationId": "v2.pty.update", "parameters": [ { - "name": "id", + "name": "ptyID", "in": "path", "schema": { "type": "string", "allOf": [ { - "pattern": "^sh_" + "pattern": "^pty" } ] }, @@ -8914,8 +9182,28 @@ ], "security": [], "responses": { - "204": { - "description": "" + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Pty" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } }, "400": { "description": "InvalidRequestError", @@ -8938,35 +9226,75 @@ } }, "404": { - "description": "ShellNotFoundError", + "description": "PtyNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ShellNotFoundError" + "$ref": "#/components/schemas/PtyNotFoundError" } } } } }, - "description": "Terminate and remove one shell command and its retained output.", - "summary": "Remove shell command" - } - }, - "/api/shell/{id}/timeout": { - "patch": { + "description": "Update the title or viewport size of one PTY session.", + "summary": "Update PTY session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "size": { + "type": "object", + "properties": { + "rows": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "cols": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + } + }, + "required": [ + "rows", + "cols" + ], + "additionalProperties": false + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { "tags": [ - "shell" + "pty" ], - "operationId": "v2.shell.timeout", + "operationId": "v2.pty.remove", "parameters": [ { - "name": "id", + "name": "ptyID", "in": "path", "schema": { "type": "string", "allOf": [ { - "pattern": "^sh_" + "pattern": "^pty" } ] }, @@ -9015,28 +9343,8 @@ ], "security": [], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/Location.Info" - }, - "data": { - "$ref": "#/components/schemas/Shell1" - } - }, - "required": [ - "location", - "data" - ], - "additionalProperties": false - } - } - } + "204": { + "description": "" }, "400": { "description": "InvalidRequestError", @@ -9059,59 +9367,35 @@ } }, "404": { - "description": "ShellNotFoundError", + "description": "PtyNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ShellNotFoundError" + "$ref": "#/components/schemas/PtyNotFoundError" } } } } }, - "description": "Replace a running shell command's timeout from now, or clear it with zero.", - "summary": "Update shell timeout", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "timeout": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - } - }, - "required": [ - "timeout" - ], - "additionalProperties": false - } - } - }, - "required": true - } + "description": "Terminate and remove one PTY session.", + "summary": "Remove PTY session" } }, - "/api/shell/{id}/output": { - "get": { + "/api/pty/{ptyID}/connect-token": { + "post": { "tags": [ - "shell" + "pty" ], - "operationId": "v2.shell.output", + "operationId": "v2.pty.connect.token", "parameters": [ { - "name": "id", + "name": "ptyID", "in": "path", "schema": { "type": "string", "allOf": [ { - "pattern": "^sh_" + "pattern": "^pty" } ] }, @@ -9156,32 +9440,6 @@ "required": false, "style": "deepObject", "explode": true - }, - { - "name": "cursor", - "in": "query", - "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" - } - ] - }, - "required": false - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" - } - ] - }, - "required": false } ], "security": [], @@ -9197,38 +9455,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "type": "object", - "properties": { - "output": { - "type": "string" - }, - "cursor": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "size": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "truncated": { - "type": "boolean" - } - }, - "required": [ - "output", - "cursor", - "size", - "truncated" - ], - "additionalProperties": false + "$ref": "#/components/schemas/PtyTicket.ConnectToken" } }, "required": [ @@ -9260,67 +9487,78 @@ } } }, + "403": { + "description": "ForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, "404": { - "description": "ShellNotFoundError", + "description": "PtyNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ShellNotFoundError" + "$ref": "#/components/schemas/PtyNotFoundError" } } } } }, - "description": "Page through captured combined output by absolute byte cursor.", - "summary": "Read shell output" + "description": "Create a short-lived single-use ticket for opening a PTY WebSocket connection.", + "summary": "Create PTY WebSocket token" } }, - "/api/question/request": { + "/api/pty/{ptyID}/connect": { "get": { "tags": [ - "question" + "pty" ], - "operationId": "v2.question.request.list", + "operationId": "v2.pty.connect", "parameters": [ { - "name": "location", - "in": "query", + "name": "ptyID", + "in": "path", "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "workspace": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, + "type": "string", + "allOf": [ { - "type": "null" + "pattern": "^pty" } ] }, - "required": false, - "style": "deepObject", - "explode": true + "required": true + }, + { + "in": "query", + "name": "location[directory]", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "location[workspace]", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "cursor", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "ticket", + "schema": { + "type": "string" + } } ], "security": [], @@ -9330,23 +9568,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/Location.Info" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2.Request" - } - } - }, - "required": [ - "location", - "data" - ], - "additionalProperties": false + "type": "boolean" } } } @@ -9370,31 +9592,79 @@ } } } - } - }, - "description": "Retrieve pending question requests for a location.", - "summary": "List pending question requests" - } - }, - "/api/session/{sessionID}/question": { - "get": { - "tags": [ - "question" - ], - "operationId": "v2.session.question.list", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "allOf": [ + }, + "403": { + "description": "ForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, + "404": { + "description": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } + } + }, + "description": "Establish a WebSocket connection streaming PTY output and accepting terminal input.", + "summary": "Connect to PTY session", + "x-websocket": true + } + }, + "/api/shell": { + "get": { + "tags": [ + "shell" + ], + "operationId": "v2.shell.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ { - "pattern": "^ses" + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" } ] }, - "required": true + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], @@ -9406,14 +9676,18 @@ "schema": { "type": "object", "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, "data": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Request" + "$ref": "#/components/schemas/Shell.Info1" } } }, "required": [ + "location", "data" ], "additionalProperties": false @@ -9440,116 +9714,135 @@ } } } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } } }, - "description": "Retrieve pending question requests owned by a session.", - "summary": "List session question requests" - } - }, - "/api/session/{sessionID}/question/{requestID}/reply": { + "description": "List currently running shell commands for a location. Exited commands are not included.", + "summary": "List running shell commands" + }, "post": { "tags": [ - "question" + "shell" ], - "operationId": "v2.session.question.reply", + "operationId": "v2.shell.create", "parameters": [ { - "name": "sessionID", - "in": "path", + "name": "location", + "in": "query", "schema": { - "type": "string", - "allOf": [ + "anyOf": [ { - "pattern": "^ses" - } - ] - }, - "required": true - }, - { - "name": "requestID", - "in": "path", - "schema": { - "type": "string", - "allOf": [ + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, { - "pattern": "^que" + "type": "null" } ] }, - "required": true + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", + "200": { + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidRequestError" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Shell.Info1" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false } } } }, - "401": { - "description": "UnauthorizedError", + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnauthorizedError" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "404": { - "description": "SessionNotFoundError | QuestionNotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/QuestionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Answer a pending question request owned by a session.", - "summary": "Reply to pending question request", + "description": "Spawn one non-interactive shell command for a location. Combined stdout/stderr is captured to a file pageable via output.", + "summary": "Run shell command", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QuestionV2.Reply" + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "timeout": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "command", + "timeout" + ], + "additionalProperties": false } } }, @@ -9557,102 +9850,31 @@ } } }, - "/api/session/{sessionID}/question/{requestID}/reject": { - "post": { + "/api/shell/{id}": { + "get": { "tags": [ - "question" + "shell" ], - "operationId": "v2.session.question.reject", + "operationId": "v2.shell.get", "parameters": [ { - "name": "sessionID", + "name": "id", "in": "path", "schema": { "type": "string", "allOf": [ { - "pattern": "^ses" + "pattern": "^sh_" } ] }, "required": true }, { - "name": "requestID", - "in": "path", + "name": "location", + "in": "query", "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^que" - } - ] - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | QuestionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/QuestionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Reject a pending question request owned by a session.", - "summary": "Reject pending question request" - } - }, - "/api/reference": { - "get": { - "tags": [ - "reference" - ], - "operationId": "v2.reference.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "anyOf": [ + "anyOf": [ { "type": "object", "properties": { @@ -9702,10 +9924,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Reference.Info" - } + "$ref": "#/components/schemas/Shell.Info1" } }, "required": [ @@ -9736,24 +9955,37 @@ } } } + }, + "404": { + "description": "ShellNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShellNotFoundError" + } + } + } } }, - "description": "List references available in the requested location.", - "summary": "List references" - } - }, - "/experimental/project/{projectID}/copy": { - "post": { + "description": "Get one shell command, including its status and exit code once exited.", + "summary": "Get shell command" + }, + "delete": { "tags": [ - "projectCopy" + "shell" ], - "operationId": "v2.projectCopy.create", + "operationId": "v2.shell.remove", "parameters": [ { - "name": "projectID", + "name": "id", "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] }, "required": true }, @@ -9800,82 +10032,61 @@ ], "security": [], "responses": { - "200": { - "description": "ProjectCopy.Copy", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProjectCopy.Copy" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "ProjectCopyError | InvalidRequestError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProjectCopyError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/UnauthorizedError" } } } }, - "401": { - "description": "UnauthorizedError", + "404": { + "description": "ShellNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnauthorizedError" + "$ref": "#/components/schemas/ShellNotFoundError" } } } } }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "strategy": { - "type": "string" - }, - "directory": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": [ - "strategy", - "directory" - ], - "additionalProperties": false - } - } - }, - "required": true - } - }, - "delete": { + "description": "Terminate and remove one shell command and its retained output.", + "summary": "Remove shell command" + } + }, + "/api/shell/{id}/timeout": { + "patch": { "tags": [ - "projectCopy" + "shell" ], - "operationId": "v2.projectCopy.remove", + "operationId": "v2.shell.timeout", "parameters": [ { - "name": "projectID", + "name": "id", "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] }, "required": true }, @@ -9922,22 +10133,35 @@ ], "security": [], "responses": { - "204": { - "description": "" - }, - "400": { - "description": "ProjectCopyError | InvalidRequestError", + "200": { + "description": "Success", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProjectCopyError" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" }, - { - "$ref": "#/components/schemas/InvalidRequestError" + "data": { + "$ref": "#/components/schemas/Shell.Info1" } - ] + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" } } } @@ -9951,24 +10175,37 @@ } } } + }, + "404": { + "description": "ShellNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShellNotFoundError" + } + } + } } }, + "description": "Replace a running shell command's timeout from now, or clear it with zero.", + "summary": "Update shell timeout", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "directory": { - "type": "string" - }, - "force": { - "type": "boolean" + "timeout": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] } }, "required": [ - "directory", - "force" + "timeout" ], "additionalProperties": false } @@ -9978,18 +10215,23 @@ } } }, - "/experimental/project/{projectID}/copy/refresh": { - "post": { + "/api/shell/{id}/output": { + "get": { "tags": [ - "projectCopy" + "shell" ], - "operationId": "v2.projectCopy.refresh", + "operationId": "v2.shell.output", "parameters": [ { - "name": "projectID", + "name": "id", "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] }, "required": true }, @@ -10032,89 +10274,32 @@ "required": false, "style": "deepObject", "explode": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" }, - "400": { - "description": "ProjectCopyError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProjectCopyError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + { + "name": "cursor", + "in": "query", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" } - } - } + ] + }, + "required": false }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - } - } - }, - "/api/vcs/status": { - "get": { - "tags": [ - "vcs" - ], - "operationId": "v2.vcs.status", - "parameters": [ { - "name": "location", + "name": "limit", "in": "query", "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "workspace": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, + "type": "string", + "allOf": [ { - "type": "null" + "pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" } ] }, - "required": false, - "style": "deepObject", - "explode": true + "required": false } ], "security": [], @@ -10130,10 +10315,38 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Vcs.FileStatus" - } + "type": "object", + "properties": { + "output": { + "type": "string" + }, + "cursor": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "size": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "truncated": { + "type": "boolean" + } + }, + "required": [ + "output", + "cursor", + "size", + "truncated" + ], + "additionalProperties": false } }, "required": [ @@ -10164,18 +10377,28 @@ } } } + }, + "404": { + "description": "ShellNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShellNotFoundError" + } + } + } } }, - "description": "List uncommitted working-copy changes relative to the requested location.", - "summary": "VCS status" + "description": "Page through captured combined output by absolute byte cursor.", + "summary": "Read shell output" } }, - "/api/vcs/diff": { + "/api/question/request": { "get": { "tags": [ - "vcs" + "question" ], - "operationId": "v2.vcs.diff", + "operationId": "v2.question.request.list", "parameters": [ { "name": "location", @@ -10216,29 +10439,6 @@ "required": false, "style": "deepObject", "explode": true - }, - { - "name": "mode", - "in": "query", - "schema": { - "$ref": "#/components/schemas/Vcs.Mode" - }, - "required": true - }, - { - "name": "context", - "in": "query", - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "required": false } ], "security": [], @@ -10256,7 +10456,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/FileDiff.Info" + "$ref": "#/components/schemas/QuestionV2.Request" } } }, @@ -10290,17 +10490,31 @@ } } }, - "description": "Diff the working copy against HEAD (mode git) or the default-branch merge base (mode branch) for the requested location.", - "summary": "VCS diff" + "description": "Retrieve pending question requests for a location.", + "summary": "List pending question requests" } }, - "/api/debug/location": { + "/api/session/{sessionID}/question": { "get": { "tags": [ - "debug" + "question" + ], + "operationId": "v2.session.question.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } ], - "operationId": "v2.debug.location.list", - "parameters": [], "security": [], "responses": { "200": { @@ -10308,10 +10522,19 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Location.Ref" - } + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Request" + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false } } } @@ -10335,31 +10558,228 @@ } } } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } } }, - "description": "List locations currently loaded by the server.", - "summary": "List loaded locations" - }, - "delete": { + "description": "Retrieve pending question requests owned by a session.", + "summary": "List session question requests" + } + }, + "/api/session/{sessionID}/question/{requestID}/reply": { + "post": { "tags": [ - "debug" + "question" ], - "operationId": "v2.debug.location.evict", + "operationId": "v2.session.question.reply", "parameters": [ { - "name": "location", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "anyOf": [ + "type": "string", + "allOf": [ { - "type": "object", - "properties": { - "directory": { - "anyOf": [ - { - "type": "string" - }, - { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | QuestionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Answer a pending question request owned by a session.", + "summary": "Reply to pending question request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QuestionV2.Reply" + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/question/{requestID}/reject": { + "post": { + "tags": [ + "question" + ], + "operationId": "v2.session.question.reject", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | QuestionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Reject a pending question request owned by a session.", + "summary": "Reject pending question request" + } + }, + "/api/reference": { + "get": { + "tags": [ + "reference" + ], + "operationId": "v2.reference.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { "type": "null" } ] @@ -10389,8 +10809,31 @@ ], "security": [], "responses": { - "204": { - "description": "" + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Reference.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } }, "400": { "description": "InvalidRequestError", @@ -10413,914 +10856,1006 @@ } } }, - "description": "Dispose the requested location's cached services so its next use boots them fresh.", - "summary": "Evict a loaded location" + "description": "List references available in the requested location.", + "summary": "List references" } - } - }, - "components": { - "schemas": { - "UnauthorizedError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "UnauthorizedError" - ] - }, - "message": { - "type": "string" - } - }, - "required": [ - "_tag", - "message" + }, + "/experimental/project/{projectID}/copy": { + "post": { + "tags": [ + "projectCopy" ], - "additionalProperties": false - }, - "InvalidRequestError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "InvalidRequestError" - ] - }, - "message": { - "type": "string" - }, - "kind": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "operationId": "v2.projectCopy.create", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true }, - "field": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true } - }, - "required": [ - "_tag", - "message" ], - "additionalProperties": false - }, - "Location.Info": { - "type": "object", - "properties": { - "directory": { - "type": "string" + "security": [], + "responses": { + "200": { + "description": "ProjectCopy.Copy", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectCopy.Copy" + } + } + } }, - "workspaceID": { - "type": "string", - "allOf": [ - { - "pattern": "^wrk" + "400": { + "description": "ProjectCopyError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } } - ] + } }, - "project": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "directory": { - "type": "string" + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } } - }, - "required": [ - "id", - "directory" - ], - "additionalProperties": false + } } }, - "required": [ - "directory", - "project" - ], - "additionalProperties": false - }, - "Model.Ref": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "providerID": { - "type": "string" + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "strategy": { + "type": "string" + }, + "directory": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "strategy", + "directory" + ], + "additionalProperties": false + } + } }, - "variant": { - "type": "string" - } - }, - "required": [ - "id", - "providerID" - ], - "additionalProperties": false - }, - "Provider.Settings": { - "type": "object" + "required": true + } }, - "Provider.Request": { - "type": "object", - "properties": { - "settings": { - "$ref": "#/components/schemas/Provider.Settings" - }, - "headers": { - "type": "object", - "additionalProperties": { + "delete": { + "tags": [ + "projectCopy" + ], + "operationId": "v2.projectCopy.remove", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { "type": "string" - } + }, + "required": true }, - "body": { - "type": "object" - } - }, - "required": [ - "settings", - "headers", - "body" - ], - "additionalProperties": false - }, - "Agent.Color": { - "type": "string", - "allOf": [ { - "pattern": "^#[0-9a-fA-F]{6}$" + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true } - ] - }, - "PermissionV2.Effect": { - "type": "string", - "enum": [ - "allow", - "deny", - "ask" - ] - }, - "PermissionV2.Rule": { - "type": "object", - "properties": { - "action": { - "type": "string" - }, - "resource": { - "type": "string" - }, - "effect": { - "$ref": "#/components/schemas/PermissionV2.Effect" - } - }, - "required": [ - "action", - "resource", - "effect" ], - "additionalProperties": false - }, - "PermissionV2.Ruleset": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionV2.Rule" - } - }, - "Agent.Info": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/Model.Ref" - }, - "request": { - "$ref": "#/components/schemas/Provider.Request" - }, - "system": { - "type": "string" - }, - "description": { - "type": "string" - }, - "mode": { - "type": "string", - "enum": [ - "subagent", - "primary", - "all" - ] - }, - "hidden": { - "type": "boolean" - }, - "color": { - "$ref": "#/components/schemas/Agent.Color" + "security": [], + "responses": { + "204": { + "description": "" }, - "steps": { - "type": "integer", - "allOf": [ - { - "exclusiveMinimum": 0 + "400": { + "description": "ProjectCopyError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } } - ] + } }, - "permissions": { - "$ref": "#/components/schemas/PermissionV2.Ruleset" + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } } }, - "required": [ - "id", - "name", - "request", - "mode", - "hidden", - "permissions" + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "force": { + "type": "boolean" + } + }, + "required": [ + "directory", + "force" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/experimental/project/{projectID}/copy/refresh": { + "post": { + "tags": [ + "projectCopy" ], - "additionalProperties": false - }, - "Plugin.Info": { - "type": "object", - "properties": { - "id": { - "type": "string" + "operationId": "v2.projectCopy.refresh", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true } - }, - "required": [ - "id" ], - "additionalProperties": false - }, - "Money.USD": { - "type": "number" - }, - "TokenUsage.Info": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" + "security": [], + "responses": { + "204": { + "description": "" }, - "reasoning": { - "type": "number" + "400": { + "description": "ProjectCopyError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } } - }, - "required": [ - "read", - "write" - ], - "additionalProperties": false + } } - }, - "required": [ - "input", - "output", - "reasoning", - "cache" + } + } + }, + "/api/vcs/status": { + "get": { + "tags": [ + "vcs" ], - "additionalProperties": false - }, - "Location.Ref": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspaceID": { - "type": "string", - "allOf": [ - { - "pattern": "^wrk" - } - ] + "operationId": "v2.vcs.status", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true } - }, - "required": [ - "directory" ], - "additionalProperties": false - }, - "FileDiff.Info": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "patch": { - "type": "string" - }, - "additions": { - "type": "integer", - "allOf": [ - { - "minimum": 0 + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Vcs.FileStatus" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } } - ] + } }, - "deletions": { - "type": "integer", - "allOf": [ - { - "minimum": 0 + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } } - ] + } }, - "status": { - "type": "string", - "enum": [ - "added", - "deleted", - "modified" - ] - } - }, - "required": [ - "file", - "patch", - "additions", - "deletions", - "status" - ], - "additionalProperties": false - }, - "Session.Revert": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } } - ] - }, - "partID": { - "type": "string" - }, - "snapshot": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileDiff.Info" } } }, - "required": [ - "messageID" + "description": "List uncommitted working-copy changes relative to the requested location.", + "summary": "VCS status" + } + }, + "/api/vcs/diff": { + "get": { + "tags": [ + "vcs" ], - "additionalProperties": false - }, - "Session.Info": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] + "operationId": "v2.vcs.diff", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true }, - "parentID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] + { + "name": "mode", + "in": "query", + "schema": { + "$ref": "#/components/schemas/Vcs.Mode" + }, + "required": true }, - "fork": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "messageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - } - }, - "required": [ - "sessionID" - ], - "additionalProperties": false - }, - "projectID": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/Model.Ref" - }, - "cost": { - "$ref": "#/components/schemas/Money.USD" - }, - "tokens": { - "$ref": "#/components/schemas/TokenUsage.Info" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - }, - "updated": { - "type": "number" - }, - "archived": { - "type": "number" - } + { + "name": "context", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, - "required": [ - "created", - "updated" - ], - "additionalProperties": false - }, - "title": { - "type": "string" - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "subpath": { - "type": "string" - }, - "revert": { - "$ref": "#/components/schemas/Session.Revert" + "required": false } - }, - "required": [ - "id", - "projectID", - "cost", - "tokens", - "time", - "title", - "location" ], - "additionalProperties": false - }, - "SessionsResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Session.Info" + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileDiff.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } } }, - "cursor": { - "type": "object", - "properties": { - "previous": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "next": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } } - }, - "additionalProperties": false - } - }, - "required": [ - "data", - "cursor" - ], - "additionalProperties": false - }, - "InvalidCursorError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "InvalidCursorError" - ] + } }, - "message": { - "type": "string" + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } } }, - "required": [ - "_tag", - "message" + "description": "Diff the working copy against HEAD (mode git) or the default-branch merge base (mode branch) for the requested location.", + "summary": "VCS diff" + } + }, + "/api/debug/location": { + "get": { + "tags": [ + "debug" ], - "additionalProperties": false - }, - "InvalidRequestError1": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "InvalidRequestError" - ] - }, - "message": { - "type": "string" + "operationId": "v2.debug.location.list", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Location.Ref" + } + } + } + } }, - "kind": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } } - ] + } }, - "field": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } } - ] + } } }, - "required": [ - "_tag", - "message" - ], - "additionalProperties": false + "description": "List locations currently loaded by the server.", + "summary": "List loaded locations" }, - "SessionActive": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "running" - ] - } - }, - "required": [ - "type" + "delete": { + "tags": [ + "debug" ], - "additionalProperties": false - }, - "SessionNotFoundError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "SessionNotFoundError" - ] - }, - "sessionID": { - "type": "string" - }, - "message": { - "type": "string" + "operationId": "v2.debug.location.evict", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true } - }, - "required": [ - "_tag", - "sessionID", - "message" ], - "additionalProperties": false - }, - "MessageNotFoundError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "MessageNotFoundError" - ] - }, - "sessionID": { - "type": "string" + "security": [], + "responses": { + "204": { + "description": "" }, - "messageID": { - "type": "string" + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } }, - "message": { - "type": "string" + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } } }, - "required": [ - "_tag", - "sessionID", - "messageID", - "message" + "description": "Dispose the requested location's cached services so its next use boots them fresh.", + "summary": "Evict a loaded location" + } + }, + "/api/websearch/provider": { + "get": { + "tags": [ + "websearch" ], - "additionalProperties": false - }, - "Prompt.Mention": { - "type": "object", - "properties": { - "start": { - "type": "number" - }, - "end": { - "type": "number" - }, - "text": { - "type": "string" + "operationId": "v2.websearch.providers", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true } - }, - "required": [ - "start", - "end", - "text" ], - "additionalProperties": false - }, - "PromptInput.FileAttachment": { - "type": "object", - "properties": { - "uri": { - "type": "string" + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebSearch.Provider" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } }, - "name": { - "type": "string" + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } }, - "description": { - "type": "string" + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } }, - "mention": { - "$ref": "#/components/schemas/Prompt.Mention" + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } } }, - "required": [ - "uri" + "description": "Return the registered web search providers.", + "summary": "List web search providers" + } + }, + "/api/websearch": { + "post": { + "tags": [ + "websearch" ], - "additionalProperties": false - }, - "Prompt.AgentAttachment": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "mention": { - "$ref": "#/components/schemas/Prompt.Mention" + "operationId": "v2.websearch.query", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true } - }, - "required": [ - "name" ], - "additionalProperties": false - }, - "PromptInput": { - "type": "object", - "properties": { - "text": { - "type": "string" + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/WebSearch.Response" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } }, - "files": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PromptInput.FileAttachment" + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } } }, - "agents": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Prompt.AgentAttachment" + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } } } }, - "required": [ - "text" - ], - "additionalProperties": false - }, - "Prompt.Base64": { - "type": "string", - "allOf": [ - { - "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$" - } - ] - }, - "Prompt.FileSource": { - "anyOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "inline" - ] + "description": "Run one web search through the selected provider. Specify a provider to override the configured default.", + "summary": "Search the web", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "query": { + "type": "string" + }, + "providerID": { + "type": "string" + } + }, + "required": [ + "query" + ], + "additionalProperties": false } - }, - "required": [ - "type" - ], - "additionalProperties": false + } }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "uri" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "type", - "uri" - ], - "additionalProperties": false - } - ] - }, - "Prompt.FileAttachment": { + "required": true + } + } + } + }, + "components": { + "schemas": { + "ServiceHealth": { "type": "object", "properties": { - "data": { - "$ref": "#/components/schemas/Prompt.Base64" - }, - "mime": { - "type": "string" - }, - "source": { - "$ref": "#/components/schemas/Prompt.FileSource" - }, - "name": { - "type": "string" + "healthy": { + "type": "boolean", + "enum": [ + true + ] }, - "description": { + "version": { "type": "string" }, - "mention": { - "$ref": "#/components/schemas/Prompt.Mention" + "pid": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] } }, "required": [ - "data", - "mime", - "source" + "healthy", + "version", + "pid" ], "additionalProperties": false }, - "Prompt": { + "UnauthorizedError": { "type": "object", "properties": { - "text": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Prompt.FileAttachment" - } + "_tag": { + "type": "string", + "enum": [ + "UnauthorizedError" + ] }, - "agents": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Prompt.AgentAttachment" - } + "message": { + "type": "string" } }, "required": [ - "text" + "_tag", + "message" ], "additionalProperties": false }, - "SessionInput.Admitted": { + "InvalidRequestError": { "type": "object", "properties": { - "admittedSeq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "prompt": { - "$ref": "#/components/schemas/Prompt" - }, - "delivery": { + "_tag": { "type": "string", "enum": [ - "steer", - "queue" + "InvalidRequestError" ] }, - "timeCreated": { - "type": "number" + "message": { + "type": "string" }, - "promotedSeq": { - "type": "integer", - "allOf": [ + "kind": { + "anyOf": [ { - "minimum": 0 + "type": "string" + }, + { + "type": "null" } ] - } - }, - "required": [ - "admittedSeq", - "id", - "sessionID", - "prompt", - "delivery", - "timeCreated" - ], - "additionalProperties": false - }, - "ConflictError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "ConflictError" - ] - }, - "message": { - "type": "string" }, - "resource": { + "field": { "anyOf": [ { "type": "string" @@ -11337,305 +11872,325 @@ ], "additionalProperties": false }, - "CommandNotFoundError": { + "ServiceStopRequest": { "type": "object", "properties": { - "_tag": { - "type": "string", - "enum": [ - "CommandNotFoundError" - ] - }, - "command": { - "type": "string" - }, - "message": { + "instanceID": { "type": "string" } }, "required": [ - "_tag", - "command", - "message" + "instanceID" ], "additionalProperties": false }, - "CommandEvaluationError": { + "ServiceStopResponse": { "type": "object", "properties": { - "_tag": { - "type": "string", - "enum": [ - "CommandEvaluationError" - ] - }, - "command": { - "type": "string" - }, - "message": { - "type": "string" + "accepted": { + "type": "boolean" } }, "required": [ - "_tag", - "command", - "message" + "accepted" ], "additionalProperties": false }, - "SkillNotFoundError": { + "Location.Info": { "type": "object", "properties": { - "_tag": { + "directory": { + "type": "string" + }, + "workspaceID": { "type": "string", - "enum": [ - "SkillNotFoundError" + "allOf": [ + { + "pattern": "^wrk" + } ] }, - "skill": { - "type": "string" - }, - "message": { - "type": "string" + "project": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "directory": { + "type": "string" + } + }, + "required": [ + "id", + "directory" + ], + "additionalProperties": false } }, "required": [ - "_tag", - "skill", - "message" + "directory", + "project" ], "additionalProperties": false }, - "SessionInput.Compaction": { + "Model.Ref": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": [ - "compaction" - ] - }, - "admittedSeq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, "id": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] + "type": "string" }, - "timeCreated": { - "type": "number" + "providerID": { + "type": "string" }, - "handledSeq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] + "variant": { + "type": "string" } }, "required": [ - "type", - "admittedSeq", "id", - "sessionID", - "timeCreated" + "providerID" ], "additionalProperties": false }, - "ServiceUnavailableError": { + "Provider.Settings": { + "type": "object" + }, + "Provider.Request": { "type": "object", "properties": { - "_tag": { - "type": "string", - "enum": [ - "ServiceUnavailableError" - ] + "settings": { + "$ref": "#/components/schemas/Provider.Settings" }, - "message": { - "type": "string" + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } }, - "service": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "body": { + "type": "object" } }, "required": [ - "_tag", - "message" + "settings", + "headers", + "body" ], "additionalProperties": false }, - "SessionBusyError": { + "Agent.Color": { + "type": "string" + }, + "PermissionV2.Effect": { + "type": "string", + "enum": [ + "allow", + "deny", + "ask" + ] + }, + "PermissionV2.Rule": { "type": "object", "properties": { - "_tag": { - "type": "string", - "enum": [ - "SessionBusyError" - ] - }, - "sessionID": { + "action": { "type": "string" }, - "message": { + "resource": { "type": "string" + }, + "effect": { + "$ref": "#/components/schemas/PermissionV2.Effect" } }, "required": [ - "_tag", - "sessionID", - "message" + "action", + "resource", + "effect" ], "additionalProperties": false }, - "UnknownError": { + "PermissionV2.Ruleset": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionV2.Rule" + } + }, + "Agent.Info": { "type": "object", "properties": { - "_tag": { - "type": "string", - "enum": [ - "UnknownError" - ] + "id": { + "type": "string" }, - "message": { + "name": { "type": "string" }, - "ref": { - "anyOf": [ - { - "type": "string" - }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "request": { + "$ref": "#/components/schemas/Provider.Request" + }, + "system": { + "type": "string" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "hidden": { + "type": "boolean" + }, + "color": { + "$ref": "#/components/schemas/Agent.Color" + }, + "steps": { + "type": "integer", + "allOf": [ { - "type": "null" + "exclusiveMinimum": 0 } ] + }, + "permissions": { + "$ref": "#/components/schemas/PermissionV2.Ruleset" } }, "required": [ - "_tag", - "message" + "id", + "name", + "request", + "mode", + "hidden", + "permissions" ], "additionalProperties": false }, - "Session.Message.AgentSelected": { + "Plugin.Info": { "type": "object", "properties": { "id": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "Money.USD": { + "type": "number" + }, + "TokenUsage.Info": { + "type": "object", + "properties": { + "input": { + "type": "number" }, - "metadata": { - "type": "object" + "output": { + "type": "number" }, - "time": { + "reasoning": { + "type": "number" + }, + "cache": { "type": "object", "properties": { - "created": { + "read": { + "type": "number" + }, + "write": { "type": "number" } }, "required": [ - "created" + "read", + "write" ], "additionalProperties": false + } + }, + "required": [ + "input", + "output", + "reasoning", + "cache" + ], + "additionalProperties": false + }, + "Location.Ref": { + "type": "object", + "properties": { + "directory": { + "type": "string" }, - "type": { + "workspaceID": { "type": "string", - "enum": [ - "agent-switched" + "allOf": [ + { + "pattern": "^wrk" + } ] - }, - "agent": { - "type": "string" } }, "required": [ - "id", - "time", - "type", - "agent" + "directory" ], "additionalProperties": false }, - "Session.Message.ModelSelected": { + "FileDiff.Info": { "type": "object", "properties": { - "id": { - "type": "string", + "file": { + "type": "string" + }, + "patch": { + "type": "string" + }, + "additions": { + "type": "integer", "allOf": [ { - "pattern": "^msg_" + "minimum": 0 } ] }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" + "deletions": { + "type": "integer", + "allOf": [ + { + "minimum": 0 } - }, - "required": [ - "created" - ], - "additionalProperties": false + ] }, - "type": { + "status": { "type": "string", "enum": [ - "model-switched" + "added", + "deleted", + "modified" ] - }, - "model": { - "$ref": "#/components/schemas/Model.Ref" - }, - "previous": { - "$ref": "#/components/schemas/Model.Ref" } }, "required": [ - "id", - "time", - "type", - "model" + "file", + "patch", + "additions", + "deletions", + "status" ], "additionalProperties": false }, - "Session.Message.User": { + "Session.Revert": { "type": "object", "properties": { - "id": { + "messageID": { "type": "string", "allOf": [ { @@ -11643,672 +12198,460 @@ } ] }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - } - }, - "required": [ - "created" - ], - "additionalProperties": false + "partID": { + "type": "string" }, - "text": { + "snapshot": { "type": "string" }, "files": { "type": "array", "items": { - "$ref": "#/components/schemas/Prompt.FileAttachment" - } - }, - "agents": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Prompt.AgentAttachment" + "$ref": "#/components/schemas/FileDiff.Info" } - }, - "type": { - "type": "string", - "enum": [ - "user" - ] } }, "required": [ - "id", - "time", - "text", - "type" + "messageID" ], "additionalProperties": false }, - "Session.Message.Synthetic": { + "Session.Info": { "type": "object", "properties": { "id": { "type": "string", "allOf": [ { - "pattern": "^msg_" + "pattern": "^ses" } ] }, - "metadata": { - "type": "object" + "parentID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "time": { + "fork": { "type": "object", "properties": { - "created": { - "type": "number" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] } }, "required": [ - "created" + "sessionID" ], "additionalProperties": false }, - "text": { + "projectID": { "type": "string" }, - "description": { + "agent": { "type": "string" }, - "type": { - "type": "string", - "enum": [ - "synthetic" - ] - } - }, - "required": [ - "id", - "time", - "text", - "type" - ], - "additionalProperties": false - }, - "Session.Message.System": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - } - }, - "required": [ - "created" - ], - "additionalProperties": false - }, - "type": { - "type": "string", - "enum": [ - "system" - ] + "model": { + "$ref": "#/components/schemas/Model.Ref" }, - "text": { - "type": "string" - } - }, - "required": [ - "id", - "time", - "type", - "text" - ], - "additionalProperties": false - }, - "Session.Message.Skill": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] + "cost": { + "$ref": "#/components/schemas/Money.USD" }, - "metadata": { - "type": "object" + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" }, "time": { "type": "object", "properties": { "created": { "type": "number" + }, + "updated": { + "type": "number" + }, + "archived": { + "type": "number" } }, "required": [ - "created" + "created", + "updated" ], "additionalProperties": false }, - "type": { - "type": "string", - "enum": [ - "skill" - ] - }, - "skill": { + "title": { "type": "string" }, - "name": { - "type": "string" + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "text": { + "subpath": { "type": "string" + }, + "revert": { + "$ref": "#/components/schemas/Session.Revert" } }, "required": [ "id", + "projectID", + "cost", + "tokens", "time", - "type", - "skill", - "name", - "text" + "title", + "location" ], "additionalProperties": false }, - "Session.Message.Shell": { + "SessionsResponse": { "type": "object", "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "metadata": { - "type": "object" + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Session.Info" + } }, - "time": { + "cursor": { "type": "object", "properties": { - "created": { - "type": "number" - }, - "completed": { - "type": "number" - } - }, - "required": [ - "created" - ], - "additionalProperties": false - }, - "type": { - "type": "string", - "enum": [ - "shell" - ] - }, - "shellID": { - "type": "string", - "allOf": [ - { - "pattern": "^sh_" - } - ] - }, - "command": { - "type": "string" - }, - "status": { - "type": "string", - "enum": [ - "running", - "exited", - "timeout", - "killed" - ] - }, - "exit": { - "anyOf": [ - { + "previous": { "anyOf": [ { - "type": "number" - }, - { - "type": "string", - "enum": [ - "NaN" - ] - }, - { - "type": "string", - "enum": [ - "Infinity" - ] + "type": "string" }, { - "type": "string", - "enum": [ - "-Infinity" - ] + "type": "null" } ] }, - { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] - } - ] - }, - "output": { - "type": "object", - "properties": { - "output": { - "type": "string" - }, - "cursor": { - "type": "integer", - "allOf": [ + "next": { + "anyOf": [ { - "minimum": 0 - } - ] - }, - "size": { - "type": "integer", - "allOf": [ + "type": "string" + }, { - "minimum": 0 + "type": "null" } ] - }, - "truncated": { - "type": "boolean" } }, - "required": [ - "output", - "cursor", - "size", - "truncated" - ], "additionalProperties": false } }, "required": [ - "id", - "time", - "type", - "shellID", - "command", - "status" + "data", + "cursor" ], "additionalProperties": false }, - "Session.Message.Assistant.Text": { + "InvalidCursorError": { "type": "object", "properties": { - "type": { + "_tag": { "type": "string", "enum": [ - "text" + "InvalidCursorError" ] }, - "text": { + "message": { "type": "string" } }, "required": [ - "type", - "text" + "_tag", + "message" ], "additionalProperties": false }, - "Session.Message.ProviderState": { - "type": "object" - }, - "Session.Message.Assistant.Reasoning": { + "InvalidRequestError1": { "type": "object", "properties": { - "type": { + "_tag": { "type": "string", "enum": [ - "reasoning" + "InvalidRequestError" ] }, - "text": { + "message": { "type": "string" }, - "state": { - "$ref": "#/components/schemas/Session.Message.ProviderState" + "kind": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" + "field": { + "anyOf": [ + { + "type": "string" }, - "completed": { - "type": "number" + { + "type": "null" } - }, - "required": [ - "created" - ], - "additionalProperties": false + ] } }, "required": [ - "type", - "text" + "_tag", + "message" ], "additionalProperties": false }, - "Session.Message.ToolState.Streaming": { + "SessionActive": { "type": "object", "properties": { - "status": { + "type": { "type": "string", "enum": [ - "streaming" + "running" ] - }, - "input": { - "type": "string" } }, "required": [ - "status", - "input" + "type" ], "additionalProperties": false }, - "Tool.TextContent": { + "SessionNotFoundError": { "type": "object", "properties": { - "type": { + "_tag": { "type": "string", "enum": [ - "text" + "SessionNotFoundError" ] }, - "text": { + "sessionID": { + "type": "string" + }, + "message": { "type": "string" } }, "required": [ - "type", - "text" + "_tag", + "sessionID", + "message" ], "additionalProperties": false }, - "Tool.FileContent": { + "MessageNotFoundError": { "type": "object", "properties": { - "type": { + "_tag": { "type": "string", "enum": [ - "file" + "MessageNotFoundError" ] }, - "uri": { + "sessionID": { "type": "string" }, - "mime": { + "messageID": { "type": "string" }, - "name": { + "message": { "type": "string" } }, "required": [ - "type", - "uri", - "mime" + "_tag", + "sessionID", + "messageID", + "message" ], "additionalProperties": false }, - "LLM.ToolContent": { - "anyOf": [ - { - "$ref": "#/components/schemas/Tool.TextContent" - }, - { - "$ref": "#/components/schemas/Tool.FileContent" - } - ] - }, - "Session.Message.ToolState.Running": { + "Prompt.Mention": { "type": "object", "properties": { - "status": { - "type": "string", - "enum": [ - "running" - ] - }, - "input": { - "type": "object" + "start": { + "type": "number" }, - "structured": { - "type": "object" + "end": { + "type": "number" }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLM.ToolContent" - } + "text": { + "type": "string" } }, "required": [ - "status", - "input", - "structured", - "content" + "start", + "end", + "text" ], "additionalProperties": false }, - "Session.Message.ToolState.Completed": { + "PromptInput.FileAttachment": { "type": "object", "properties": { - "status": { - "type": "string", - "enum": [ - "completed" - ] - }, - "input": { - "type": "object" + "uri": { + "type": "string" }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLM.ToolContent" - } + "name": { + "type": "string" }, - "structured": { - "type": "object" + "description": { + "type": "string" }, - "result": {} + "mention": { + "$ref": "#/components/schemas/Prompt.Mention" + } }, "required": [ - "status", - "input", - "content", - "structured" + "uri" ], "additionalProperties": false }, - "Session.StructuredError": { + "Prompt.AgentAttachment": { "type": "object", "properties": { - "type": { + "name": { "type": "string" }, - "message": { - "type": "string" + "mention": { + "$ref": "#/components/schemas/Prompt.Mention" } }, "required": [ - "type", - "message" + "name" ], "additionalProperties": false }, - "Session.Message.ToolState.Error": { + "Prompt.Base64": { + "type": "string", + "allOf": [ + { + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$" + } + ] + }, + "Prompt.FileSource": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "inline" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "uri" + ] + }, + "uri": { + "type": "string" + } + }, + "required": [ + "type", + "uri" + ], + "additionalProperties": false + } + ] + }, + "Prompt.FileAttachment": { "type": "object", "properties": { - "status": { - "type": "string", - "enum": [ - "error" - ] + "data": { + "$ref": "#/components/schemas/Prompt.Base64" }, - "input": { - "type": "object" + "mime": { + "type": "string" }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLM.ToolContent" - } + "source": { + "$ref": "#/components/schemas/Prompt.FileSource" }, - "structured": { - "type": "object" + "name": { + "type": "string" }, - "error": { - "$ref": "#/components/schemas/Session.StructuredError" + "description": { + "type": "string" }, - "result": {} + "mention": { + "$ref": "#/components/schemas/Prompt.Mention" + } }, "required": [ - "status", - "input", - "content", - "structured", - "error" + "data", + "mime", + "source" ], "additionalProperties": false }, - "Session.Message.Assistant.Tool": { + "SessionPending.UserData": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": [ - "tool" - ] - }, - "id": { - "type": "string" - }, - "name": { + "text": { "type": "string" }, - "executed": { - "type": "boolean" - }, - "providerState": { - "$ref": "#/components/schemas/Session.Message.ProviderState" - }, - "providerResultState": { - "$ref": "#/components/schemas/Session.Message.ProviderState" + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.FileAttachment" + } }, - "state": { - "anyOf": [ - { - "$ref": "#/components/schemas/Session.Message.ToolState.Streaming" - }, - { - "$ref": "#/components/schemas/Session.Message.ToolState.Running" - }, - { - "$ref": "#/components/schemas/Session.Message.ToolState.Completed" - }, - { - "$ref": "#/components/schemas/Session.Message.ToolState.Error" - } - ] + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - }, - "ran": { - "type": "number" - }, - "completed": { - "type": "number" - } - }, - "required": [ - "created" - ], - "additionalProperties": false + "metadata": { + "type": "object" } }, "required": [ - "type", - "id", - "name", - "state", - "time" + "text" ], "additionalProperties": false }, - "Session.Message.Assistant.Retry": { + "SessionPending.User": { "type": "object", "properties": { - "attempt": { + "admittedSeq": { "type": "integer", "allOf": [ { - "exclusiveMinimum": 0 + "minimum": 0 } ] }, - "at": { - "type": "number" - }, - "error": { - "$ref": "#/components/schemas/Session.StructuredError" - } - }, - "required": [ - "attempt", - "at", - "error" - ], - "additionalProperties": false - }, - "Session.Message.Assistant": { - "type": "object", - "properties": { "id": { "type": "string", "allOf": [ @@ -12317,174 +12660,170 @@ } ] }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - }, - "completed": { - "type": "number" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" } - }, - "required": [ - "created" - ], - "additionalProperties": false + ] + }, + "timeCreated": { + "type": "number" }, "type": { "type": "string", "enum": [ - "assistant" + "user" ] }, - "agent": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/Model.Ref" - }, - "content": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/Session.Message.Assistant.Text" - }, - { - "$ref": "#/components/schemas/Session.Message.Assistant.Reasoning" - }, - { - "$ref": "#/components/schemas/Session.Message.Assistant.Tool" - } - ] - } - }, - "snapshot": { - "type": "object", - "properties": { - "start": { - "type": "string" - }, - "end": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false + "data": { + "$ref": "#/components/schemas/SessionPending.UserData" }, - "finish": { + "delivery": { "type": "string", "enum": [ - "stop", - "length", - "tool-calls", - "content-filter", - "error", - "unknown" + "steer", + "queue" ] - }, - "cost": { - "$ref": "#/components/schemas/Money.USD" - }, - "tokens": { - "$ref": "#/components/schemas/TokenUsage.Info" - }, - "error": { - "$ref": "#/components/schemas/Session.StructuredError" - }, - "retry": { - "$ref": "#/components/schemas/Session.Message.Assistant.Retry" } }, "required": [ + "admittedSeq", "id", - "time", + "sessionID", + "timeCreated", "type", - "agent", - "model", - "content" + "data", + "delivery" ], "additionalProperties": false }, - "Session.Message.Compaction.Running": { + "ConflictError": { "type": "object", "properties": { - "type": { + "_tag": { "type": "string", "enum": [ - "compaction" - ] - }, - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } + "ConflictError" ] }, - "metadata": { - "type": "object" + "message": { + "type": "string" }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" + "resource": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" } - }, - "required": [ - "created" - ], - "additionalProperties": false - }, - "status": { - "type": "string", - "enum": [ - "running" ] - }, - "reason": { + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "CommandNotFoundError": { + "type": "object", + "properties": { + "_tag": { "type": "string", "enum": [ - "auto", - "manual" + "CommandNotFoundError" ] }, - "summary": { + "command": { "type": "string" }, - "recent": { + "message": { "type": "string" } }, "required": [ - "type", - "id", - "time", - "status", - "reason", - "summary", - "recent" + "_tag", + "command", + "message" ], "additionalProperties": false }, - "Session.Message.Compaction.Completed": { + "CommandEvaluationError": { "type": "object", "properties": { - "type": { + "_tag": { "type": "string", "enum": [ - "compaction" + "CommandEvaluationError" + ] + }, + "command": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "command", + "message" + ], + "additionalProperties": false + }, + "SkillNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "SkillNotFoundError" + ] + }, + "skill": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "skill", + "message" + ], + "additionalProperties": false + }, + "SessionPending.SyntheticData": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "description": { + "type": "string" + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "text" + ], + "additionalProperties": false + }, + "SessionPending.Synthetic": { + "type": "object", + "properties": { + "admittedSeq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } ] }, "id": { @@ -12495,61 +12834,175 @@ } ] }, - "metadata": { - "type": "object" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" + "timeCreated": { + "type": "number" + }, + "type": { + "type": "string", + "enum": [ + "synthetic" + ] + }, + "data": { + "$ref": "#/components/schemas/SessionPending.SyntheticData" + }, + "delivery": { + "type": "string", + "enum": [ + "steer", + "queue" + ] + } + }, + "required": [ + "admittedSeq", + "id", + "sessionID", + "timeCreated", + "type", + "data", + "delivery" + ], + "additionalProperties": false + }, + "SessionPending.Compaction": { + "type": "object", + "properties": { + "admittedSeq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 } - }, - "required": [ - "created" - ], - "additionalProperties": false + ] }, - "status": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "timeCreated": { + "type": "number" + }, + "type": { "type": "string", "enum": [ - "completed" + "compaction" + ] + } + }, + "required": [ + "admittedSeq", + "id", + "sessionID", + "timeCreated", + "type" + ], + "additionalProperties": false + }, + "ServiceUnavailableError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "ServiceUnavailableError" ] }, - "reason": { + "message": { + "type": "string" + }, + "service": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "SessionBusyError": { + "type": "object", + "properties": { + "_tag": { "type": "string", "enum": [ - "auto", - "manual" + "SessionBusyError" ] }, - "summary": { + "sessionID": { "type": "string" }, - "recent": { + "message": { "type": "string" } }, "required": [ - "type", - "id", - "time", - "status", - "reason", - "summary", - "recent" + "_tag", + "sessionID", + "message" ], "additionalProperties": false }, - "Session.Message.Compaction.Failed": { + "UnknownError": { "type": "object", "properties": { - "type": { + "_tag": { "type": "string", "enum": [ - "compaction" + "UnknownError" ] }, + "message": { + "type": "string" + }, + "ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "Session.Message.AgentSelected": { + "type": "object", + "properties": { "id": { "type": "string", "allOf": [ @@ -12573,1482 +13026,997 @@ ], "additionalProperties": false }, - "status": { - "type": "string", - "enum": [ - "failed" - ] - }, - "reason": { + "type": { "type": "string", "enum": [ - "auto", - "manual" + "agent-switched" ] }, - "error": { - "$ref": "#/components/schemas/Session.StructuredError" + "agent": { + "type": "string" } }, "required": [ - "type", "id", "time", - "status", - "reason", - "error" + "type", + "agent" ], "additionalProperties": false }, - "Session.Message.Compaction": { - "anyOf": [ - { - "$ref": "#/components/schemas/Session.Message.Compaction.Running" - }, - { - "$ref": "#/components/schemas/Session.Message.Compaction.Completed" - }, - { - "$ref": "#/components/schemas/Session.Message.Compaction.Failed" - } - ] - }, - "Session.Message.Info": { - "anyOf": [ - { - "$ref": "#/components/schemas/Session.Message.AgentSelected" - }, - { - "$ref": "#/components/schemas/Session.Message.ModelSelected" - }, - { - "$ref": "#/components/schemas/Session.Message.User" - }, - { - "$ref": "#/components/schemas/Session.Message.Synthetic" - }, - { - "$ref": "#/components/schemas/Session.Message.System" - }, - { - "$ref": "#/components/schemas/Session.Message.Skill" - }, - { - "$ref": "#/components/schemas/Session.Message.Shell" - }, - { - "$ref": "#/components/schemas/Session.Message.Assistant" - }, - { - "$ref": "#/components/schemas/Session.Message.Compaction" - } - ] - }, - "InstructionEntry.Key": { - "type": "string", - "allOf": [ - { - "pattern": "^[a-z0-9][a-z0-9._-]*$", - "description": "Instruction entry key (lowercase alphanumerics plus . _ -)" - } - ] - }, - "InstructionEntry.Info": { - "type": "object", - "properties": { - "key": { - "$ref": "#/components/schemas/InstructionEntry.Key" - }, - "value": {} - }, - "required": [ - "key", - "value" - ], - "additionalProperties": false - }, - "session.agent.selected": { + "Session.Message.ModelSelected": { "type": "object", "properties": { "id": { "type": "string", "allOf": [ { - "pattern": "^evt_" + "pattern": "^msg_" } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, - "type": { - "type": "string", - "enum": [ - "session.agent.selected" - ] - }, - "durable": { + "time": { "type": "object", "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] + "created": { + "type": "number" } }, "required": [ - "aggregateID", - "seq", - "version" + "created" ], "additionalProperties": false }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + "type": { + "type": "string", + "enum": [ + "model-switched" + ] }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "agent": { - "type": "string" - } - }, - "required": [ - "sessionID", - "agent" - ], - "additionalProperties": false + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "previous": { + "$ref": "#/components/schemas/Model.Ref" } }, "required": [ "id", - "created", + "time", "type", - "durable", - "data" + "model" ], "additionalProperties": false }, - "session.model.selected": { + "Session.Message.User": { "type": "object", "properties": { "id": { "type": "string", "allOf": [ { - "pattern": "^evt_" + "pattern": "^msg_" } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, - "type": { - "type": "string", - "enum": [ - "session.model.selected" - ] - }, - "durable": { + "time": { "type": "object", "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] + "created": { + "type": "number" } }, "required": [ - "aggregateID", - "seq", - "version" + "created" ], "additionalProperties": false }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + "text": { + "type": "string" }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "model": { - "$ref": "#/components/schemas/Model.Ref" - } - }, - "required": [ - "sessionID", - "model" - ], - "additionalProperties": false + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + }, + "type": { + "type": "string", + "enum": [ + "user" + ] } }, "required": [ "id", - "created", - "type", - "durable", - "data" + "time", + "text", + "type" ], "additionalProperties": false }, - "session.moved": { + "Session.Message.Synthetic": { "type": "object", "properties": { "id": { "type": "string", "allOf": [ { - "pattern": "^evt_" + "pattern": "^msg_" } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "text": { + "type": "string" + }, + "description": { + "type": "string" + }, "type": { "type": "string", "enum": [ - "session.moved" + "synthetic" ] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "subpath": { - "type": "string" - } - }, - "required": [ - "sessionID", - "location" - ], - "additionalProperties": false } }, "required": [ "id", - "created", - "type", - "durable", - "data" + "time", + "text", + "type" ], "additionalProperties": false }, - "session.renamed": { + "Session.Message.System": { "type": "object", "properties": { "id": { "type": "string", "allOf": [ { - "pattern": "^evt_" + "pattern": "^msg_" } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, - "type": { - "type": "string", - "enum": [ - "session.renamed" - ] - }, - "durable": { + "time": { "type": "object", "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] + "created": { + "type": "number" } }, "required": [ - "aggregateID", - "seq", - "version" + "created" ], "additionalProperties": false }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + "type": { + "type": "string", + "enum": [ + "system" + ] }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "title": { - "type": "string" - } - }, - "required": [ - "sessionID", - "title" - ], - "additionalProperties": false + "text": { + "type": "string" } }, "required": [ "id", - "created", + "time", "type", - "durable", - "data" + "text" ], "additionalProperties": false }, - "session.deleted": { + "Session.Message.Skill": { "type": "object", "properties": { "id": { "type": "string", "allOf": [ { - "pattern": "^evt_" + "pattern": "^msg_" } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, - "type": { - "type": "string", - "enum": [ - "session.deleted" - ] - }, - "durable": { + "time": { "type": "object", "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 2 - ] + "created": { + "type": "number" } }, "required": [ - "aggregateID", - "seq", - "version" + "created" ], "additionalProperties": false }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + "type": { + "type": "string", + "enum": [ + "skill" + ] }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - } - }, - "required": [ - "sessionID" - ], - "additionalProperties": false + "skill": { + "type": "string" + }, + "name": { + "type": "string" + }, + "text": { + "type": "string" } }, "required": [ "id", - "created", + "time", "type", - "durable", - "data" + "skill", + "name", + "text" ], "additionalProperties": false }, - "session.forked": { + "Session.Message.Shell": { "type": "object", "properties": { "id": { "type": "string", "allOf": [ { - "pattern": "^evt_" + "pattern": "^msg_" } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, - "type": { - "type": "string", - "enum": [ - "session.forked" - ] - }, - "durable": { + "time": { "type": "object", "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] + "created": { + "type": "number" }, - "version": { - "type": "number", - "enum": [ - 1 - ] + "completed": { + "type": "number" } }, "required": [ - "aggregateID", - "seq", - "version" + "created" ], "additionalProperties": false }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + "type": { + "type": "string", + "enum": [ + "shell" + ] }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "parentID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "from": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - } - }, - "required": [ - "sessionID", - "parentID" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], - "additionalProperties": false - }, - "session.prompt.promoted": { - "type": "object", - "properties": { - "id": { + "shellID": { "type": "string", "allOf": [ { - "pattern": "^evt_" + "pattern": "^sh_" } ] }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" + "command": { + "type": "string" }, - "type": { + "status": { "type": "string", "enum": [ - "session.prompt.promoted" + "running", + "exited", + "timeout", + "killed" ] }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ + "exit": { + "anyOf": [ + { + "anyOf": [ { - "minimum": 0 + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] } ] }, - "version": { - "type": "number", + { + "type": "string", "enum": [ - 1 + "Infinity", + "-Infinity", + "NaN" ] } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + ] }, - "data": { + "output": { "type": "object", "properties": { - "sessionID": { - "type": "string", + "output": { + "type": "string" + }, + "cursor": { + "type": "integer", "allOf": [ { - "pattern": "^ses" + "minimum": 0 } ] }, - "inputID": { - "type": "string", + "size": { + "type": "integer", "allOf": [ { - "pattern": "^msg_" + "minimum": 0 } ] + }, + "truncated": { + "type": "boolean" } }, "required": [ - "sessionID", - "inputID" + "output", + "cursor", + "size", + "truncated" ], "additionalProperties": false } }, "required": [ "id", - "created", + "time", "type", - "durable", - "data" + "shellID", + "command", + "status" ], "additionalProperties": false }, - "session.prompt.admitted": { + "Session.Message.ProviderState": { + "type": "object" + }, + "Session.Message.Assistant.Text": { "type": "object", "properties": { - "id": { + "type": { "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } + "enum": [ + "text" ] }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" + "text": { + "type": "string" }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState" + } + }, + "required": [ + "type", + "text" + ], + "additionalProperties": false + }, + "Session.Message.Assistant.Reasoning": { + "type": "object", + "properties": { "type": { "type": "string", "enum": [ - "session.prompt.admitted" + "reasoning" ] }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false + "text": { + "type": "string" }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState" }, - "data": { + "time": { "type": "object", "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "inputID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "prompt": { - "$ref": "#/components/schemas/Prompt" + "created": { + "type": "number" }, - "delivery": { - "type": "string", - "enum": [ - "steer", - "queue" - ] + "completed": { + "type": "number" } }, "required": [ - "sessionID", - "inputID", - "prompt", - "delivery" + "created" ], "additionalProperties": false } }, "required": [ - "id", - "created", "type", - "durable", - "data" + "text" ], "additionalProperties": false }, - "session.execution.started": { + "Session.Message.ToolState.Streaming": { "type": "object", "properties": { - "id": { + "status": { "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } + "enum": [ + "streaming" ] }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "session.execution.started" - ] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - } - }, - "required": [ - "sessionID" - ], - "additionalProperties": false + "input": { + "type": "string" } }, "required": [ - "id", - "created", - "type", - "durable", - "data" + "status", + "input" ], "additionalProperties": false }, - "session.execution.succeeded": { + "Session.Message.ToolState.Running": { "type": "object", "properties": { - "id": { + "status": { "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } + "enum": [ + "running" ] }, - "created": { - "type": "number" + "input": { + "type": "object" }, "metadata": { "type": "object" - }, + } + }, + "required": [ + "status", + "input", + "metadata" + ], + "additionalProperties": false + }, + "Tool.TextContent": { + "type": "object", + "properties": { "type": { "type": "string", "enum": [ - "session.execution.succeeded" + "text" ] }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - } - }, - "required": [ - "sessionID" - ], - "additionalProperties": false + "text": { + "type": "string" } }, "required": [ - "id", - "created", "type", - "durable", - "data" + "text" ], "additionalProperties": false }, - "session.execution.failed": { + "Tool.FileContent": { "type": "object", "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, "type": { "type": "string", "enum": [ - "session.execution.failed" + "file" ] }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false + "uri": { + "type": "string" }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + "mime": { + "type": "string" }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "error": { - "$ref": "#/components/schemas/Session.StructuredError" - } - }, - "required": [ - "sessionID", - "error" - ], - "additionalProperties": false + "name": { + "type": "string" } }, "required": [ - "id", - "created", "type", - "durable", - "data" + "uri", + "mime" ], "additionalProperties": false }, - "session.execution.interrupted": { + "LLM.ToolContent": { + "anyOf": [ + { + "$ref": "#/components/schemas/Tool.TextContent" + }, + { + "$ref": "#/components/schemas/Tool.FileContent" + } + ] + }, + "Session.Message.ToolState.Completed": { "type": "object", "properties": { - "id": { + "status": { "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } + "enum": [ + "completed" ] }, - "created": { - "type": "number" + "input": { + "type": "object" + }, + "content": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/LLM.ToolContent" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/LLM.ToolContent" + } }, "metadata": { "type": "object" - }, + } + }, + "required": [ + "status", + "input", + "content" + ], + "additionalProperties": false + }, + "Session.StructuredError": { + "type": "object", + "properties": { "type": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "type", + "message" + ], + "additionalProperties": false + }, + "Session.Message.ToolState.Error": { + "type": "object", + "properties": { + "status": { "type": "string", "enum": [ - "session.execution.interrupted" + "error" ] }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false + "input": { + "type": "object" }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + "error": { + "$ref": "#/components/schemas/Session.StructuredError" }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "reason": { - "type": "string", - "enum": [ - "user", - "shutdown", - "superseded" - ] + "content": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/LLM.ToolContent" } - }, - "required": [ - "sessionID", - "reason" ], - "additionalProperties": false + "minItems": 1, + "items": { + "$ref": "#/components/schemas/LLM.ToolContent" + } + }, + "metadata": { + "type": "object" } }, "required": [ - "id", - "created", - "type", - "durable", - "data" + "status", + "input", + "error" ], "additionalProperties": false }, - "session.instructions.updated": { + "Session.Message.Assistant.Tool": { "type": "object", "properties": { - "id": { + "type": { "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } + "enum": [ + "tool" ] }, - "created": { - "type": "number" + "id": { + "type": "string" }, - "metadata": { - "type": "object" + "name": { + "type": "string" }, - "type": { - "type": "string", - "enum": [ - "session.instructions.updated" - ] + "executed": { + "type": "boolean" }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" + "providerState": { + "$ref": "#/components/schemas/Session.Message.ProviderState" + }, + "providerResultState": { + "$ref": "#/components/schemas/Session.Message.ProviderState" + }, + "state": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.ToolState.Streaming" }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] + { + "$ref": "#/components/schemas/Session.Message.ToolState.Running" }, - "version": { - "type": "number", - "enum": [ - 1 - ] + { + "$ref": "#/components/schemas/Session.Message.ToolState.Completed" + }, + { + "$ref": "#/components/schemas/Session.Message.ToolState.Error" } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + ] }, - "data": { + "time": { "type": "object", "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] + "created": { + "type": "number" }, - "text": { - "type": "string" + "ran": { + "type": "number" + }, + "completed": { + "type": "number" } }, "required": [ - "sessionID", - "text" + "created" ], "additionalProperties": false } }, "required": [ - "id", - "created", "type", - "durable", - "data" + "id", + "name", + "state", + "time" ], "additionalProperties": false }, - "session.synthetic": { + "Session.Message.Assistant.Retry": { "type": "object", "properties": { - "id": { - "type": "string", + "attempt": { + "type": "integer", "allOf": [ { - "pattern": "^evt_" + "exclusiveMinimum": 0 } ] }, - "created": { + "at": { "type": "number" }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + } + }, + "required": [ + "attempt", + "at", + "error" + ], + "additionalProperties": false + }, + "Session.Message.Assistant": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, "metadata": { "type": "object" }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "completed": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, "type": { "type": "string", "enum": [ - "session.synthetic" + "assistant" ] }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false + "agent": { + "type": "string" }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + "model": { + "$ref": "#/components/schemas/Model.Ref" }, - "data": { + "content": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.Assistant.Text" + }, + { + "$ref": "#/components/schemas/Session.Message.Assistant.Reasoning" + }, + { + "$ref": "#/components/schemas/Session.Message.Assistant.Tool" + } + ] + } + }, + "snapshot": { "type": "object", "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "text": { + "start": { "type": "string" }, - "description": { + "end": { "type": "string" }, - "metadata": { - "type": "object" + "files": { + "type": "array", + "items": { + "type": "string" + } } }, - "required": [ - "sessionID", - "text" - ], "additionalProperties": false + }, + "finish": { + "type": "string", + "enum": [ + "stop", + "length", + "tool-calls", + "content-filter", + "error", + "unknown" + ] + }, + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + }, + "retry": { + "$ref": "#/components/schemas/Session.Message.Assistant.Retry" } }, "required": [ "id", - "created", + "time", "type", - "durable", - "data" + "agent", + "model", + "content" ], "additionalProperties": false }, - "session.skill.activated": { + "Session.Message.Compaction.Running": { "type": "object", "properties": { + "type": { + "type": "string", + "enum": [ + "compaction" + ] + }, "id": { "type": "string", "allOf": [ { - "pattern": "^evt_" + "pattern": "^msg_" } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, - "type": { - "type": "string", - "enum": [ - "session.skill.activated" - ] - }, - "durable": { + "time": { "type": "object", "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] + "created": { + "type": "number" } }, "required": [ - "aggregateID", - "seq", - "version" + "created" ], "additionalProperties": false }, - "location": { - "$ref": "#/components/schemas/Location.Ref" + "status": { + "type": "string", + "enum": [ + "running" + ] }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "text": { - "type": "string" - } - }, - "required": [ - "sessionID", - "id", - "name", - "text" - ], - "additionalProperties": false + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "summary": { + "type": "string" + }, + "recent": { + "type": "string" } }, "required": [ - "id", - "created", "type", - "durable", - "data" + "id", + "time", + "status", + "reason", + "summary", + "recent" ], "additionalProperties": false }, - "Shell": { + "Session.Message.Compaction.Completed": { "type": "object", "properties": { + "type": { + "type": "string", + "enum": [ + "compaction" + ] + }, "id": { "type": "string", "allOf": [ { - "pattern": "^sh_" + "pattern": "^msg_" } ] }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, "status": { "type": "string", "enum": [ - "running", - "exited", - "timeout", - "killed" + "completed" ] }, - "command": { - "type": "string" - }, - "cwd": { - "type": "string" + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] }, - "shell": { + "summary": { "type": "string" }, - "file": { + "recent": { "type": "string" - }, - "pid": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "exit": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "NaN" - ] - }, - { - "type": "string", - "enum": [ - "Infinity" - ] - }, + } + }, + "required": [ + "type", + "id", + "time", + "status", + "reason", + "summary", + "recent" + ], + "additionalProperties": false + }, + "Session.Message.Compaction.Failed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "compaction" + ] + }, + "id": { + "type": "string", + "allOf": [ { - "type": "string", - "enum": [ - "-Infinity" - ] + "pattern": "^msg_" } ] }, @@ -14058,76 +14026,171 @@ "time": { "type": "object", "properties": { - "started": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "NaN" - ] - }, - { - "type": "string", - "enum": [ - "Infinity" - ] - }, - { - "type": "string", - "enum": [ - "-Infinity" - ] - } - ] - }, - "completed": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "NaN" - ] - }, - { - "type": "string", - "enum": [ - "Infinity" - ] - }, - { - "type": "string", - "enum": [ - "-Infinity" - ] - } - ] + "created": { + "type": "number" } }, "required": [ - "started" + "created" ], "additionalProperties": false + }, + "status": { + "type": "string", + "enum": [ + "failed" + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" } }, "required": [ + "type", "id", + "time", "status", - "command", - "cwd", - "shell", - "file", - "metadata", - "time" + "reason", + "error" ], "additionalProperties": false }, - "session.shell.started": { + "Session.Message.Compaction": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.Compaction.Running" + }, + { + "$ref": "#/components/schemas/Session.Message.Compaction.Completed" + }, + { + "$ref": "#/components/schemas/Session.Message.Compaction.Failed" + } + ] + }, + "Session.Message.Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.AgentSelected" + }, + { + "$ref": "#/components/schemas/Session.Message.ModelSelected" + }, + { + "$ref": "#/components/schemas/Session.Message.User" + }, + { + "$ref": "#/components/schemas/Session.Message.Synthetic" + }, + { + "$ref": "#/components/schemas/Session.Message.System" + }, + { + "$ref": "#/components/schemas/Session.Message.Skill" + }, + { + "$ref": "#/components/schemas/Session.Message.Shell" + }, + { + "$ref": "#/components/schemas/Session.Message.Assistant" + }, + { + "$ref": "#/components/schemas/Session.Message.Compaction" + } + ] + }, + "SessionPending.Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionPending.User" + }, + { + "$ref": "#/components/schemas/SessionPending.Synthetic" + }, + { + "$ref": "#/components/schemas/SessionPending.Compaction" + } + ] + }, + "InstructionEntry.Key": { + "type": "string", + "allOf": [ + { + "pattern": "^[a-z0-9][a-z0-9._-]*$", + "description": "Instruction entry key (lowercase alphanumerics plus . _ -)" + } + ] + }, + "InstructionEntry.Info": { + "type": "object", + "properties": { + "key": { + "$ref": "#/components/schemas/InstructionEntry.Key" + }, + "value": {} + }, + "required": [ + "key", + "value" + ], + "additionalProperties": false + }, + "InstructionEntryValueTooLargeError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "InstructionEntryValueTooLargeError" + ] + }, + "actualBytes": { + "type": "integer" + }, + "maxBytes": { + "type": "integer" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "actualBytes", + "maxBytes", + "message" + ], + "additionalProperties": false + }, + "SessionGenerateResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": [ + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "data" + ], + "additionalProperties": false + }, + "session.agent.selected": { "type": "object", "properties": { "id": { @@ -14147,7 +14210,7 @@ "type": { "type": "string", "enum": [ - "session.shell.started" + "session.agent.selected" ] }, "durable": { @@ -14192,13 +14255,13 @@ } ] }, - "shell": { - "$ref": "#/components/schemas/Shell" + "agent": { + "type": "string" } }, "required": [ "sessionID", - "shell" + "agent" ], "additionalProperties": false } @@ -14212,7 +14275,7 @@ ], "additionalProperties": false }, - "session.shell.ended": { + "session.model.selected": { "type": "object", "properties": { "id": { @@ -14232,7 +14295,7 @@ "type": { "type": "string", "enum": [ - "session.shell.ended" + "session.model.selected" ] }, "durable": { @@ -14277,48 +14340,13 @@ } ] }, - "shell": { - "$ref": "#/components/schemas/Shell" - }, - "output": { - "type": "object", - "properties": { - "output": { - "type": "string" - }, - "cursor": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "size": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "truncated": { - "type": "boolean" - } - }, - "required": [ - "output", - "cursor", - "size", - "truncated" - ], - "additionalProperties": false + "model": { + "$ref": "#/components/schemas/Model.Ref" } }, "required": [ "sessionID", - "shell", - "output" + "model" ], "additionalProperties": false } @@ -14332,7 +14360,7 @@ ], "additionalProperties": false }, - "session.step.started": { + "session.moved": { "type": "object", "properties": { "id": { @@ -14352,7 +14380,7 @@ "type": { "type": "string", "enum": [ - "session.step.started" + "session.moved" ] }, "durable": { @@ -14397,29 +14425,19 @@ } ] }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "agent": { + "projectID": { "type": "string" }, - "model": { - "$ref": "#/components/schemas/Model.Ref" - }, - "snapshot": { + "subpath": { "type": "string" } }, "required": [ "sessionID", - "assistantMessageID", - "agent", - "model" + "location" ], "additionalProperties": false } @@ -14433,7 +14451,7 @@ ], "additionalProperties": false }, - "session.step.ended": { + "session.renamed": { "type": "object", "properties": { "id": { @@ -14453,7 +14471,7 @@ "type": { "type": "string", "enum": [ - "session.step.ended" + "session.renamed" ] }, "durable": { @@ -14498,47 +14516,13 @@ } ] }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "finish": { - "type": "string", - "enum": [ - "stop", - "length", - "tool-calls", - "content-filter", - "error", - "unknown" - ] - }, - "cost": { - "$ref": "#/components/schemas/Money.USD" - }, - "tokens": { - "$ref": "#/components/schemas/TokenUsage.Info" - }, - "snapshot": { + "title": { "type": "string" - }, - "files": { - "type": "array", - "items": { - "type": "string" - } } }, "required": [ "sessionID", - "assistantMessageID", - "finish", - "cost", - "tokens" + "title" ], "additionalProperties": false } @@ -14552,7 +14536,7 @@ ], "additionalProperties": false }, - "session.step.failed": { + "session.deleted": { "type": "object", "properties": { "id": { @@ -14572,7 +14556,7 @@ "type": { "type": "string", "enum": [ - "session.step.failed" + "session.deleted" ] }, "durable": { @@ -14592,7 +14576,7 @@ "version": { "type": "number", "enum": [ - 1 + 2 ] } }, @@ -14616,29 +14600,10 @@ "pattern": "^ses" } ] - }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "error": { - "$ref": "#/components/schemas/Session.StructuredError" - }, - "cost": { - "$ref": "#/components/schemas/Money.USD" - }, - "tokens": { - "$ref": "#/components/schemas/TokenUsage.Info" } }, "required": [ - "sessionID", - "assistantMessageID", - "error" + "sessionID" ], "additionalProperties": false } @@ -14652,7 +14617,7 @@ ], "additionalProperties": false }, - "session.text.started": { + "session.forked": { "type": "object", "properties": { "id": { @@ -14672,7 +14637,7 @@ "type": { "type": "string", "enum": [ - "session.text.started" + "session.forked" ] }, "durable": { @@ -14692,7 +14657,7 @@ "version": { "type": "number", "enum": [ - 1 + 2 ] } }, @@ -14717,27 +14682,35 @@ } ] }, - "assistantMessageID": { + "parentID": { "type": "string", "allOf": [ { - "pattern": "^msg_" + "pattern": "^ses" } ] }, - "ordinal": { + "parentSeq": { "type": "integer", "allOf": [ { - "minimum": 0 + "minimum": -1 + } + ] + }, + "from": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" } ] } }, "required": [ "sessionID", - "assistantMessageID", - "ordinal" + "parentID", + "parentSeq" ], "additionalProperties": false } @@ -14751,7 +14724,7 @@ ], "additionalProperties": false }, - "session.text.ended": { + "session.input.promoted": { "type": "object", "properties": { "id": { @@ -14771,7 +14744,7 @@ "type": { "type": "string", "enum": [ - "session.text.ended" + "session.input.promoted" ] }, "durable": { @@ -14816,31 +14789,18 @@ } ] }, - "assistantMessageID": { + "inputID": { "type": "string", "allOf": [ { "pattern": "^msg_" } ] - }, - "ordinal": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "text": { - "type": "string" } }, "required": [ "sessionID", - "assistantMessageID", - "ordinal", - "text" + "inputID" ], "additionalProperties": false } @@ -14854,47 +14814,153 @@ ], "additionalProperties": false }, - "Session.Message.ProviderState3": { - "type": "object" + "SessionPending.UserData1": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "text" + ], + "additionalProperties": false }, - "session.reasoning.started": { + "SessionPending.UserMessage": { "type": "object", "properties": { - "id": { + "type": { "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } + "enum": [ + "user" ] }, - "created": { - "type": "number" + "data": { + "$ref": "#/components/schemas/SessionPending.UserData1" + }, + "delivery": { + "type": "string", + "enum": [ + "steer", + "queue" + ] + } + }, + "required": [ + "type", + "data", + "delivery" + ], + "additionalProperties": false + }, + "SessionPending.SyntheticData1": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "description": { + "type": "string" }, "metadata": { "type": "object" - }, + } + }, + "required": [ + "text" + ], + "additionalProperties": false + }, + "SessionPending.SyntheticMessage": { + "type": "object", + "properties": { "type": { "type": "string", "enum": [ - "session.reasoning.started" + "synthetic" ] }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { + "data": { + "$ref": "#/components/schemas/SessionPending.SyntheticData1" + }, + "delivery": { + "type": "string", + "enum": [ + "steer", + "queue" + ] + } + }, + "required": [ + "type", + "data", + "delivery" + ], + "additionalProperties": false + }, + "SessionPending.Message": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionPending.UserMessage" + }, + { + "$ref": "#/components/schemas/SessionPending.SyntheticMessage" + } + ] + }, + "session.input.admitted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.input.admitted" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { "type": "number", "enum": [ 1 @@ -14922,7 +14988,7 @@ } ] }, - "assistantMessageID": { + "inputID": { "type": "string", "allOf": [ { @@ -14930,22 +14996,14 @@ } ] }, - "ordinal": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "state": { - "$ref": "#/components/schemas/Session.Message.ProviderState3" + "input": { + "$ref": "#/components/schemas/SessionPending.Message" } }, "required": [ "sessionID", - "assistantMessageID", - "ordinal" + "inputID", + "input" ], "additionalProperties": false } @@ -14959,10 +15017,7 @@ ], "additionalProperties": false }, - "Session.Message.ProviderState4": { - "type": "object" - }, - "session.reasoning.ended": { + "session.execution.started": { "type": "object", "properties": { "id": { @@ -14982,7 +15037,7 @@ "type": { "type": "string", "enum": [ - "session.reasoning.ended" + "session.execution.started" ] }, "durable": { @@ -15026,35 +15081,10 @@ "pattern": "^ses" } ] - }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "ordinal": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "text": { - "type": "string" - }, - "state": { - "$ref": "#/components/schemas/Session.Message.ProviderState4" } }, "required": [ - "sessionID", - "assistantMessageID", - "ordinal", - "text" + "sessionID" ], "additionalProperties": false } @@ -15068,7 +15098,7 @@ ], "additionalProperties": false }, - "session.tool.input.started": { + "session.execution.succeeded": { "type": "object", "properties": { "id": { @@ -15088,7 +15118,7 @@ "type": { "type": "string", "enum": [ - "session.tool.input.started" + "session.execution.succeeded" ] }, "durable": { @@ -15132,27 +15162,10 @@ "pattern": "^ses" } ] - }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "callID": { - "type": "string" - }, - "name": { - "type": "string" } }, "required": [ - "sessionID", - "assistantMessageID", - "callID", - "name" + "sessionID" ], "additionalProperties": false } @@ -15166,7 +15179,7 @@ ], "additionalProperties": false }, - "session.tool.input.ended": { + "session.execution.failed": { "type": "object", "properties": { "id": { @@ -15186,7 +15199,7 @@ "type": { "type": "string", "enum": [ - "session.tool.input.ended" + "session.execution.failed" ] }, "durable": { @@ -15231,26 +15244,13 @@ } ] }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "callID": { - "type": "string" - }, - "text": { - "type": "string" + "error": { + "$ref": "#/components/schemas/Session.StructuredError" } }, "required": [ "sessionID", - "assistantMessageID", - "callID", - "text" + "error" ], "additionalProperties": false } @@ -15264,10 +15264,7 @@ ], "additionalProperties": false }, - "Session.Message.ProviderState5": { - "type": "object" - }, - "session.tool.called": { + "session.execution.interrupted": { "type": "object", "properties": { "id": { @@ -15287,7 +15284,7 @@ "type": { "type": "string", "enum": [ - "session.tool.called" + "session.execution.interrupted" ] }, "durable": { @@ -15332,33 +15329,18 @@ } ] }, - "assistantMessageID": { + "reason": { "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } + "enum": [ + "user", + "shutdown", + "superseded" ] - }, - "callID": { - "type": "string" - }, - "input": { - "type": "object" - }, - "executed": { - "type": "boolean" - }, - "state": { - "$ref": "#/components/schemas/Session.Message.ProviderState5" } }, "required": [ "sessionID", - "assistantMessageID", - "callID", - "input", - "executed" + "reason" ], "additionalProperties": false } @@ -15372,7 +15354,7 @@ ], "additionalProperties": false }, - "session.tool.progress": { + "session.instructions.updated": { "type": "object", "properties": { "id": { @@ -15392,7 +15374,7 @@ "type": { "type": "string", "enum": [ - "session.tool.progress" + "session.instructions.updated" ] }, "durable": { @@ -15412,7 +15394,7 @@ "version": { "type": "number", "enum": [ - 1 + 2 ] } }, @@ -15437,33 +15419,31 @@ } ] }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "callID": { - "type": "string" - }, - "structured": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLM.ToolContent" + "delta": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^[a-f0-9]{64}$" + } + ] + }, + { + "type": "string", + "enum": [ + "removed" + ] + } + ] } } }, "required": [ "sessionID", - "assistantMessageID", - "callID", - "structured", - "content" + "delta" ], "additionalProperties": false } @@ -15477,10 +15457,7 @@ ], "additionalProperties": false }, - "Session.Message.ProviderState6": { - "type": "object" - }, - "session.tool.success": { + "session.synthetic": { "type": "object", "properties": { "id": { @@ -15500,7 +15477,7 @@ "type": { "type": "string", "enum": [ - "session.tool.success" + "session.synthetic" ] }, "durable": { @@ -15545,41 +15522,19 @@ } ] }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] + "text": { + "type": "string" }, - "callID": { + "description": { "type": "string" }, - "structured": { + "metadata": { "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLM.ToolContent" - } - }, - "result": {}, - "executed": { - "type": "boolean" - }, - "resultState": { - "$ref": "#/components/schemas/Session.Message.ProviderState6" } }, "required": [ "sessionID", - "assistantMessageID", - "callID", - "structured", - "content", - "executed" + "text" ], "additionalProperties": false } @@ -15593,10 +15548,7 @@ ], "additionalProperties": false }, - "Session.Message.ProviderState7": { - "type": "object" - }, - "session.tool.failed": { + "session.skill.activated": { "type": "object", "properties": { "id": { @@ -15616,7 +15568,7 @@ "type": { "type": "string", "enum": [ - "session.tool.failed" + "session.skill.activated" ] }, "durable": { @@ -15661,34 +15613,21 @@ } ] }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "callID": { + "id": { "type": "string" }, - "error": { - "$ref": "#/components/schemas/Session.StructuredError" - }, - "result": {}, - "executed": { - "type": "boolean" + "name": { + "type": "string" }, - "resultState": { - "$ref": "#/components/schemas/Session.Message.ProviderState7" + "text": { + "type": "string" } }, "required": [ "sessionID", - "assistantMessageID", - "callID", - "error", - "executed" + "id", + "name", + "text" ], "additionalProperties": false } @@ -15702,7 +15641,81 @@ ], "additionalProperties": false }, - "session.retry.scheduled": { + "Shell.Info": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "status": { + "type": "string", + "enum": [ + "running", + "exited", + "timeout", + "killed" + ] + }, + "command": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "shell": { + "type": "string" + }, + "file": { + "type": "string" + }, + "pid": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "exit": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "started": { + "type": "number" + }, + "completed": { + "type": "number" + } + }, + "required": [ + "started" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "status", + "command", + "cwd", + "shell", + "file", + "metadata", + "time" + ], + "additionalProperties": false + }, + "session.shell.started": { "type": "object", "properties": { "id": { @@ -15722,7 +15735,7 @@ "type": { "type": "string", "enum": [ - "session.retry.scheduled" + "session.shell.started" ] }, "durable": { @@ -15767,40 +15780,13 @@ } ] }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "attempt": { - "type": "integer", - "allOf": [ - { - "exclusiveMinimum": 0 - } - ] - }, - "at": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "error": { - "$ref": "#/components/schemas/Session.StructuredError" + "shell": { + "$ref": "#/components/schemas/Shell.Info" } }, "required": [ "sessionID", - "assistantMessageID", - "attempt", - "at", - "error" + "shell" ], "additionalProperties": false } @@ -15814,7 +15800,7 @@ ], "additionalProperties": false }, - "session.compaction.admitted": { + "session.shell.ended": { "type": "object", "properties": { "id": { @@ -15834,7 +15820,7 @@ "type": { "type": "string", "enum": [ - "session.compaction.admitted" + "session.shell.ended" ] }, "durable": { @@ -15879,18 +15865,48 @@ } ] }, - "inputID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" + "shell": { + "$ref": "#/components/schemas/Shell.Info" + }, + "output": { + "type": "object", + "properties": { + "output": { + "type": "string" + }, + "cursor": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "size": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "truncated": { + "type": "boolean" } - ] + }, + "required": [ + "output", + "cursor", + "size", + "truncated" + ], + "additionalProperties": false } }, "required": [ "sessionID", - "inputID" + "shell", + "output" ], "additionalProperties": false } @@ -15904,7 +15920,7 @@ ], "additionalProperties": false }, - "session.compaction.started": { + "session.step.started": { "type": "object", "properties": { "id": { @@ -15924,7 +15940,7 @@ "type": { "type": "string", "enum": [ - "session.compaction.started" + "session.step.started" ] }, "durable": { @@ -15969,29 +15985,29 @@ } ] }, - "reason": { - "type": "string", - "enum": [ - "auto", - "manual" - ] - }, - "recent": { - "type": "string" - }, - "inputID": { + "assistantMessageID": { "type": "string", "allOf": [ { "pattern": "^msg_" } ] + }, + "agent": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "snapshot": { + "type": "string" } }, "required": [ "sessionID", - "reason", - "recent" + "assistantMessageID", + "agent", + "model" ], "additionalProperties": false } @@ -16005,7 +16021,7 @@ ], "additionalProperties": false }, - "session.compaction.ended": { + "session.step.ended": { "type": "object", "properties": { "id": { @@ -16025,7 +16041,7 @@ "type": { "type": "string", "enum": [ - "session.compaction.ended" + "session.step.ended" ] }, "durable": { @@ -16070,25 +16086,47 @@ } ] }, - "reason": { + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "finish": { "type": "string", "enum": [ - "auto", - "manual" + "stop", + "length", + "tool-calls", + "content-filter", + "error", + "unknown" ] }, - "text": { - "type": "string" + "cost": { + "$ref": "#/components/schemas/Money.USD" }, - "recent": { + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" + }, + "snapshot": { "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ "sessionID", - "reason", - "text", - "recent" + "assistantMessageID", + "finish", + "cost", + "tokens" ], "additionalProperties": false } @@ -16102,7 +16140,7 @@ ], "additionalProperties": false }, - "session.compaction.failed": { + "session.step.failed": { "type": "object", "properties": { "id": { @@ -16122,7 +16160,7 @@ "type": { "type": "string", "enum": [ - "session.compaction.failed" + "session.step.failed" ] }, "durable": { @@ -16167,29 +16205,136 @@ } ] }, - "reason": { + "assistantMessageID": { "type": "string", - "enum": [ - "auto", - "manual" + "allOf": [ + { + "pattern": "^msg_" + } ] }, "error": { "$ref": "#/components/schemas/Session.StructuredError" }, - "inputID": { + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" + }, + "snapshot": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "error" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.text.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.text.started" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { "type": "string", "allOf": [ { "pattern": "^msg_" } ] + }, + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] } }, "required": [ "sessionID", - "reason", - "error" + "assistantMessageID", + "ordinal" ], "additionalProperties": false } @@ -16203,7 +16348,10 @@ ], "additionalProperties": false }, - "session.revert.staged": { + "Session.Message.ProviderState4": { + "type": "object" + }, + "session.text.ended": { "type": "object", "properties": { "id": { @@ -16223,7 +16371,7 @@ "type": { "type": "string", "enum": [ - "session.revert.staged" + "session.text.ended" ] }, "durable": { @@ -16268,13 +16416,34 @@ } ] }, - "revert": { - "$ref": "#/components/schemas/Session.Revert" + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "text": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState4" } }, "required": [ "sessionID", - "revert" + "assistantMessageID", + "ordinal", + "text" ], "additionalProperties": false } @@ -16288,7 +16457,10 @@ ], "additionalProperties": false }, - "session.revert.cleared": { + "Session.Message.ProviderState5": { + "type": "object" + }, + "session.reasoning.started": { "type": "object", "properties": { "id": { @@ -16308,7 +16480,7 @@ "type": { "type": "string", "enum": [ - "session.revert.cleared" + "session.reasoning.started" ] }, "durable": { @@ -16352,10 +16524,31 @@ "pattern": "^ses" } ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState5" } }, "required": [ - "sessionID" + "sessionID", + "assistantMessageID", + "ordinal" ], "additionalProperties": false } @@ -16369,7 +16562,10 @@ ], "additionalProperties": false }, - "session.revert.committed": { + "Session.Message.ProviderState6": { + "type": "object" + }, + "session.reasoning.ended": { "type": "object", "properties": { "id": { @@ -16389,7 +16585,7 @@ "type": { "type": "string", "enum": [ - "session.revert.committed" + "session.reasoning.ended" ] }, "durable": { @@ -16434,18 +16630,34 @@ } ] }, - "to": { + "assistantMessageID": { "type": "string", "allOf": [ { "pattern": "^msg_" } ] + }, + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "text": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState6" } }, "required": [ "sessionID", - "to" + "assistantMessageID", + "ordinal", + "text" ], "additionalProperties": false } @@ -16459,797 +16671,2766 @@ ], "additionalProperties": false }, - "Session.Event.Durable": { - "oneOf": [ - { - "$ref": "#/components/schemas/session.agent.selected" + "session.tool.input.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] }, - { - "$ref": "#/components/schemas/session.model.selected" + "created": { + "type": "number" }, - { - "$ref": "#/components/schemas/session.moved" - }, - { - "$ref": "#/components/schemas/session.renamed" - }, - { - "$ref": "#/components/schemas/session.deleted" - }, - { - "$ref": "#/components/schemas/session.forked" - }, - { - "$ref": "#/components/schemas/session.prompt.promoted" - }, - { - "$ref": "#/components/schemas/session.prompt.admitted" - }, - { - "$ref": "#/components/schemas/session.execution.started" - }, - { - "$ref": "#/components/schemas/session.execution.succeeded" - }, - { - "$ref": "#/components/schemas/session.execution.failed" - }, - { - "$ref": "#/components/schemas/session.execution.interrupted" - }, - { - "$ref": "#/components/schemas/session.instructions.updated" - }, - { - "$ref": "#/components/schemas/session.synthetic" - }, - { - "$ref": "#/components/schemas/session.skill.activated" - }, - { - "$ref": "#/components/schemas/session.shell.started" - }, - { - "$ref": "#/components/schemas/session.shell.ended" - }, - { - "$ref": "#/components/schemas/session.step.started" - }, - { - "$ref": "#/components/schemas/session.step.ended" - }, - { - "$ref": "#/components/schemas/session.step.failed" - }, - { - "$ref": "#/components/schemas/session.text.started" - }, - { - "$ref": "#/components/schemas/session.text.ended" - }, - { - "$ref": "#/components/schemas/session.reasoning.started" - }, - { - "$ref": "#/components/schemas/session.reasoning.ended" - }, - { - "$ref": "#/components/schemas/session.tool.input.started" - }, - { - "$ref": "#/components/schemas/session.tool.input.ended" - }, - { - "$ref": "#/components/schemas/session.tool.called" - }, - { - "$ref": "#/components/schemas/session.tool.progress" - }, - { - "$ref": "#/components/schemas/session.tool.success" - }, - { - "$ref": "#/components/schemas/session.tool.failed" - }, - { - "$ref": "#/components/schemas/session.retry.scheduled" - }, - { - "$ref": "#/components/schemas/session.compaction.admitted" - }, - { - "$ref": "#/components/schemas/session.compaction.started" - }, - { - "$ref": "#/components/schemas/session.compaction.ended" - }, - { - "$ref": "#/components/schemas/session.compaction.failed" - }, - { - "$ref": "#/components/schemas/session.revert.staged" - }, - { - "$ref": "#/components/schemas/session.revert.cleared" + "metadata": { + "type": "object" }, - { - "$ref": "#/components/schemas/session.revert.committed" - } - ] - }, - "EventLog.Synced": { - "type": "object", - "properties": { "type": { "type": "string", "enum": [ - "log.synced" + "session.tool.input.started" ] }, - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] } - ] - } - }, - "required": [ - "type", - "aggregateID" - ], - "additionalProperties": false, - "description": "Marker emitted once when a log read reaches its captured watermark. The reader holds every event committed at or below seq." - }, - "SessionLogItem": { - "anyOf": [ - { - "$ref": "#/components/schemas/Session.Event.Durable" + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false }, - { - "$ref": "#/components/schemas/EventLog.Synced" - } - ] - }, - "SessionLogItemStream": { - "type": "string", - "contentSchema": { - "$ref": "#/components/schemas/SessionLogItem" - }, - "contentMediaType": "application/json" - }, - "SessionMessagesResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Session.Message.Info" - } + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "cursor": { + "data": { "type": "object", "properties": { - "previous": { - "anyOf": [ - { - "type": "string" - }, + "sessionID": { + "type": "string", + "allOf": [ { - "type": "null" + "pattern": "^ses" } ] }, - "next": { - "anyOf": [ - { - "type": "string" - }, + "assistantMessageID": { + "type": "string", + "allOf": [ { - "type": "null" + "pattern": "^msg_" } ] + }, + "callID": { + "type": "string" + }, + "name": { + "type": "string" } }, + "required": [ + "sessionID", + "assistantMessageID", + "callID", + "name" + ], "additionalProperties": false } }, "required": [ - "data", - "cursor" + "id", + "created", + "type", + "durable", + "data" ], "additionalProperties": false }, - "Model.Capabilities": { + "session.tool.input.ended": { "type": "object", "properties": { - "tools": { - "type": "boolean" + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] }, - "input": { - "type": "array", - "items": { - "type": "string" - } + "created": { + "type": "number" }, - "output": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "tools", - "input", - "output" - ], - "additionalProperties": false - }, - "Model.Variant": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "settings": { + "metadata": { "type": "object" }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } + "type": { + "type": "string", + "enum": [ + "session.tool.input.ended" + ] }, - "body": { - "type": "object" - } - }, - "required": [ - "id" - ], - "additionalProperties": false - }, - "Money.USDPerMillionTokens": { - "type": "number" - }, - "Model.Cost": { - "type": "object", - "properties": { - "tier": { + "durable": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": [ - "context" + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } ] }, - "size": { - "type": "integer" + "version": { + "type": "number", + "enum": [ + 1 + ] } }, "required": [ - "type", - "size" + "aggregateID", + "seq", + "version" ], "additionalProperties": false }, - "input": { - "$ref": "#/components/schemas/Money.USDPerMillionTokens" - }, - "output": { - "$ref": "#/components/schemas/Money.USDPerMillionTokens" + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "cache": { + "data": { "type": "object", "properties": { - "read": { - "$ref": "#/components/schemas/Money.USDPerMillionTokens" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "write": { - "$ref": "#/components/schemas/Money.USDPerMillionTokens" + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "text": { + "type": "string" } }, "required": [ - "read", - "write" + "sessionID", + "assistantMessageID", + "callID", + "text" ], "additionalProperties": false } }, "required": [ - "input", - "output", - "cache" + "id", + "created", + "type", + "durable", + "data" ], "additionalProperties": false }, - "Model.Info": { + "Session.Message.ProviderState7": { + "type": "object" + }, + "session.tool.called": { "type": "object", "properties": { "id": { - "type": "string" - }, - "modelID": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "family": { - "type": "string" - }, - "name": { - "type": "string" - }, - "package": { - "type": "string" - }, - "settings": { - "type": "object" + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } + "created": { + "type": "number" }, - "body": { + "metadata": { "type": "object" }, - "capabilities": { - "$ref": "#/components/schemas/Model.Capabilities" - }, - "variants": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Model.Variant" - } + "type": { + "type": "string", + "enum": [ + "session.tool.called" + ] }, - "time": { + "durable": { "type": "object", "properties": { - "released": { - "type": "number" + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] } }, "required": [ - "released" + "aggregateID", + "seq", + "version" ], "additionalProperties": false }, - "cost": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Model.Cost" - } - }, - "status": { - "type": "string", - "enum": [ - "alpha", - "beta", - "deprecated", - "active" - ] - }, - "enabled": { - "type": "boolean" + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "limit": { + "data": { "type": "object", "properties": { - "context": { - "type": "integer" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" }, "input": { - "type": "integer" + "type": "object" }, - "output": { - "type": "integer" + "executed": { + "type": "boolean" + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState7" } }, "required": [ - "context", - "output" + "sessionID", + "assistantMessageID", + "callID", + "input", + "executed" ], "additionalProperties": false } }, "required": [ "id", - "modelID", - "providerID", - "name", - "capabilities", - "variants", - "time", - "cost", - "status", - "enabled", - "limit" + "created", + "type", + "durable", + "data" ], "additionalProperties": false }, - "GenerateTextResponse": { + "Session.Message.ProviderState8": { + "type": "object" + }, + "session.tool.success": { "type": "object", "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.tool.success" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 2 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, "data": { "type": "object", "properties": { - "text": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { "type": "string" + }, + "content": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/LLM.ToolContent" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/LLM.ToolContent" + } + }, + "metadata": { + "type": "object" + }, + "executed": { + "type": "boolean" + }, + "resultState": { + "$ref": "#/components/schemas/Session.Message.ProviderState8" } }, "required": [ - "text" + "sessionID", + "assistantMessageID", + "callID", + "content", + "executed" ], "additionalProperties": false } }, "required": [ + "id", + "created", + "type", + "durable", "data" ], "additionalProperties": false }, - "ProviderV2.Info": { + "Session.Message.ProviderState9": { + "type": "object" + }, + "session.tool.failed": { "type": "object", "properties": { "id": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] }, - "integrationID": { - "type": "string" + "created": { + "type": "number" }, - "name": { - "type": "string" + "metadata": { + "type": "object" }, - "disabled": { - "type": "boolean" + "type": { + "type": "string", + "enum": [ + "session.tool.failed" + ] }, - "package": { - "type": "string" + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 2 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false }, - "settings": { - "type": "object" + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "headers": { + "data": { "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "body": { - "type": "object" + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + }, + "content": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/LLM.ToolContent" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/LLM.ToolContent" + } + }, + "metadata": { + "type": "object" + }, + "executed": { + "type": "boolean" + }, + "resultState": { + "$ref": "#/components/schemas/Session.Message.ProviderState9" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "callID", + "error", + "executed" + ], + "additionalProperties": false } }, "required": [ "id", - "name", - "package" + "created", + "type", + "durable", + "data" ], "additionalProperties": false }, - "ProviderNotFoundError": { + "session.retry.scheduled": { "type": "object", "properties": { - "_tag": { + "id": { "type": "string", - "enum": [ - "ProviderNotFoundError" + "allOf": [ + { + "pattern": "^evt_" + } ] }, - "providerID": { - "type": "string" + "created": { + "type": "number" }, - "message": { - "type": "string" - } - }, - "required": [ - "_tag", - "providerID", - "message" - ], - "additionalProperties": false - }, - "Integration.When": { - "type": "object", - "properties": { - "key": { - "type": "string" + "metadata": { + "type": "object" }, - "op": { + "type": { "type": "string", "enum": [ - "eq", - "neq" + "session.retry.scheduled" ] }, - "value": { - "type": "string" + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "attempt": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "at": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "attempt", + "at", + "error" + ], + "additionalProperties": false } }, "required": [ - "key", - "op", - "value" + "id", + "created", + "type", + "durable", + "data" ], "additionalProperties": false }, - "Integration.TextPrompt": { + "session.compaction.admitted": { "type": "object", "properties": { - "type": { + "id": { "type": "string", - "enum": [ - "text" + "allOf": [ + { + "pattern": "^evt_" + } ] }, - "key": { - "type": "string" - }, - "message": { - "type": "string" + "created": { + "type": "number" }, - "placeholder": { - "type": "string" + "metadata": { + "type": "object" }, - "when": { - "$ref": "#/components/schemas/Integration.When" - } - }, - "required": [ - "type", - "key", - "message" - ], - "additionalProperties": false - }, - "Integration.SelectPrompt": { - "type": "object", - "properties": { "type": { "type": "string", "enum": [ - "select" + "session.compaction.admitted" ] }, - "key": { - "type": "string" + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false }, - "message": { - "type": "string" + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "options": { - "type": "array", - "items": { - "type": "object", - "properties": { - "label": { - "type": "string" - }, - "value": { - "type": "string" - }, - "hint": { - "type": "string" - } + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "required": [ - "label", - "value" - ], - "additionalProperties": false - } - }, - "when": { - "$ref": "#/components/schemas/Integration.When" + "inputID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": [ + "sessionID", + "inputID" + ], + "additionalProperties": false } }, "required": [ + "id", + "created", "type", - "key", - "message", - "options" + "durable", + "data" ], "additionalProperties": false }, - "Integration.OAuthMethod": { + "session.compaction.started": { "type": "object", "properties": { "id": { - "type": "string" - }, - "type": { "type": "string", - "enum": [ - "oauth" + "allOf": [ + { + "pattern": "^evt_" + } ] }, - "label": { - "type": "string" + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, - "prompts": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/Integration.TextPrompt" - }, - { - "$ref": "#/components/schemas/Integration.SelectPrompt" - } - ] - } - } - }, - "required": [ - "id", - "type", - "label" - ], - "additionalProperties": false - }, - "Integration.KeyMethod": { - "type": "object", - "properties": { "type": { "type": "string", "enum": [ - "key" + "session.compaction.started" ] }, - "label": { - "type": "string" + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "recent": { + "type": "string" + }, + "inputID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": [ + "sessionID", + "reason", + "recent" + ], + "additionalProperties": false } }, "required": [ - "type" + "id", + "created", + "type", + "durable", + "data" ], "additionalProperties": false }, - "Integration.EnvMethod": { + "session.compaction.ended": { "type": "object", "properties": { - "type": { + "id": { "type": "string", - "enum": [ - "env" + "allOf": [ + { + "pattern": "^evt_" + } ] }, - "names": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "type", - "names" - ], - "additionalProperties": false - }, - "Integration.Method": { - "anyOf": [ - { - "$ref": "#/components/schemas/Integration.OAuthMethod" + "created": { + "type": "number" }, - { - "$ref": "#/components/schemas/Integration.KeyMethod" + "metadata": { + "type": "object" }, - { - "$ref": "#/components/schemas/Integration.EnvMethod" - } - ] - }, - "Connection.CredentialInfo": { - "type": "object", - "properties": { "type": { "type": "string", "enum": [ - "credential" + "session.compaction.ended" ] }, - "id": { - "type": "string" + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false }, - "label": { - "type": "string" + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "text": { + "type": "string" + }, + "recent": { + "type": "string" + } + }, + "required": [ + "sessionID", + "reason", + "text", + "recent" + ], + "additionalProperties": false } }, "required": [ - "type", "id", - "label" + "created", + "type", + "durable", + "data" ], "additionalProperties": false }, - "Connection.EnvInfo": { + "session.compaction.failed": { "type": "object", "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, "type": { "type": "string", "enum": [ - "env" + "session.compaction.failed" ] }, - "name": { - "type": "string" - } - }, - "required": [ - "type", - "name" - ], - "additionalProperties": false - }, - "Connection.Info": { - "anyOf": [ - { - "$ref": "#/components/schemas/Connection.CredentialInfo" + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false }, - { - "$ref": "#/components/schemas/Connection.EnvInfo" + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + }, + "inputID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": [ + "sessionID", + "reason", + "error" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.revert.staged": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.revert.staged" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "revert": { + "$ref": "#/components/schemas/Session.Revert" + } + }, + "required": [ + "sessionID", + "revert" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.revert.cleared": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.revert.cleared" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + } + }, + "required": [ + "sessionID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.revert.committed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.revert.committed" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "to": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": [ + "sessionID", + "to" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.usage.recorded": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.usage.recorded" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "source": { + "type": "string", + "enum": [ + "title", + "compaction" + ] + }, + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" + } + }, + "required": [ + "sessionID", + "source", + "cost", + "tokens" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "Session.Event.Durable": { + "oneOf": [ + { + "$ref": "#/components/schemas/session.agent.selected" + }, + { + "$ref": "#/components/schemas/session.model.selected" + }, + { + "$ref": "#/components/schemas/session.moved" + }, + { + "$ref": "#/components/schemas/session.renamed" + }, + { + "$ref": "#/components/schemas/session.deleted" + }, + { + "$ref": "#/components/schemas/session.forked" + }, + { + "$ref": "#/components/schemas/session.input.promoted" + }, + { + "$ref": "#/components/schemas/session.input.admitted" + }, + { + "$ref": "#/components/schemas/session.execution.started" + }, + { + "$ref": "#/components/schemas/session.execution.succeeded" + }, + { + "$ref": "#/components/schemas/session.execution.failed" + }, + { + "$ref": "#/components/schemas/session.execution.interrupted" + }, + { + "$ref": "#/components/schemas/session.instructions.updated" + }, + { + "$ref": "#/components/schemas/session.synthetic" + }, + { + "$ref": "#/components/schemas/session.skill.activated" + }, + { + "$ref": "#/components/schemas/session.shell.started" + }, + { + "$ref": "#/components/schemas/session.shell.ended" + }, + { + "$ref": "#/components/schemas/session.step.started" + }, + { + "$ref": "#/components/schemas/session.step.ended" + }, + { + "$ref": "#/components/schemas/session.step.failed" + }, + { + "$ref": "#/components/schemas/session.text.started" + }, + { + "$ref": "#/components/schemas/session.text.ended" + }, + { + "$ref": "#/components/schemas/session.reasoning.started" + }, + { + "$ref": "#/components/schemas/session.reasoning.ended" + }, + { + "$ref": "#/components/schemas/session.tool.input.started" + }, + { + "$ref": "#/components/schemas/session.tool.input.ended" + }, + { + "$ref": "#/components/schemas/session.tool.called" + }, + { + "$ref": "#/components/schemas/session.tool.success" + }, + { + "$ref": "#/components/schemas/session.tool.failed" + }, + { + "$ref": "#/components/schemas/session.retry.scheduled" + }, + { + "$ref": "#/components/schemas/session.compaction.admitted" + }, + { + "$ref": "#/components/schemas/session.compaction.started" + }, + { + "$ref": "#/components/schemas/session.compaction.ended" + }, + { + "$ref": "#/components/schemas/session.compaction.failed" + }, + { + "$ref": "#/components/schemas/session.revert.staged" + }, + { + "$ref": "#/components/schemas/session.revert.cleared" + }, + { + "$ref": "#/components/schemas/session.revert.committed" + }, + { + "$ref": "#/components/schemas/session.usage.recorded" + } + ] + }, + "EventLog.Synced": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "log.synced" + ] + }, + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "type", + "aggregateID" + ], + "additionalProperties": false, + "description": "Marker emitted once when a log read reaches its captured watermark. The reader holds every event committed at or below seq." + }, + "SessionLogItem": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Event.Durable" + }, + { + "$ref": "#/components/schemas/EventLog.Synced" + } + ] + }, + "SessionLogItemJsonString": { + "type": "string", + "contentSchema": { + "$ref": "#/components/schemas/SessionLogItem" + }, + "contentMediaType": "application/json" + }, + "SessionMessagesResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Session.Message.Info" + } + }, + "cursor": { + "type": "object", + "properties": { + "previous": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "next": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "data", + "cursor" + ], + "additionalProperties": false + }, + "Model.ReasoningField": { + "anyOf": [ + { + "type": "string", + "enum": [ + "reasoning", + "reasoning_content", + "reasoning_text" + ] + }, + { + "type": "string" + } + ] + }, + "Model.Compatibility": { + "type": "object", + "properties": { + "reasoningField": { + "$ref": "#/components/schemas/Model.ReasoningField" + } + }, + "additionalProperties": false + }, + "Model.Capabilities": { + "type": "object", + "properties": { + "tools": { + "type": "boolean" + }, + "input": { + "type": "array", + "items": { + "type": "string" + } + }, + "output": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "tools", + "input", + "output" + ], + "additionalProperties": false + }, + "Model.Variant": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "settings": { + "type": "object" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "Money.USDPerMillionTokens": { + "type": "number" + }, + "Model.Cost": { + "type": "object", + "properties": { + "tier": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "context" + ] + }, + "size": { + "type": "integer" + } + }, + "required": [ + "type", + "size" + ], + "additionalProperties": false + }, + "input": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + "output": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + "write": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + } + }, + "required": [ + "read", + "write" + ], + "additionalProperties": false + } + }, + "required": [ + "input", + "output", + "cache" + ], + "additionalProperties": false + }, + "Model.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "modelID": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "family": { + "type": "string" + }, + "name": { + "type": "string" + }, + "compatibility": { + "$ref": "#/components/schemas/Model.Compatibility" + }, + "package": { + "type": "string" + }, + "settings": { + "type": "object" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + }, + "capabilities": { + "$ref": "#/components/schemas/Model.Capabilities" + }, + "variants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Model.Variant" + } + }, + "time": { + "type": "object", + "properties": { + "released": { + "type": "number" + } + }, + "required": [ + "released" + ], + "additionalProperties": false + }, + "cost": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Model.Cost" + } + }, + "status": { + "type": "string", + "enum": [ + "alpha", + "beta", + "deprecated", + "active" + ] + }, + "enabled": { + "type": "boolean" + }, + "limit": { + "type": "object", + "properties": { + "context": { + "type": "integer" + }, + "input": { + "type": "integer" + }, + "output": { + "type": "integer" + } + }, + "required": [ + "context", + "output" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "modelID", + "providerID", + "name", + "capabilities", + "variants", + "time", + "cost", + "status", + "enabled", + "limit" + ], + "additionalProperties": false + }, + "GenerateTextResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": [ + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "data" + ], + "additionalProperties": false + }, + "ProviderV2.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "integrationID": { + "type": "string" + }, + "name": { + "type": "string" + }, + "disabled": { + "type": "boolean" + }, + "package": { + "type": "string" + }, + "settings": { + "type": "object" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + } + }, + "required": [ + "id", + "name", + "package" + ], + "additionalProperties": false + }, + "ProviderNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "ProviderNotFoundError" + ] + }, + "providerID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "providerID", + "message" + ], + "additionalProperties": false + }, + "Integration.When": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "op": { + "type": "string", + "enum": [ + "eq", + "neq" + ] + }, + "value": { + "type": "string" + } + }, + "required": [ + "key", + "op", + "value" + ], + "additionalProperties": false + }, + "Integration.TextPrompt": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "text" + ] + }, + "key": { + "type": "string" + }, + "message": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "when": { + "$ref": "#/components/schemas/Integration.When" + } + }, + "required": [ + "type", + "key", + "message" + ], + "additionalProperties": false + }, + "Integration.SelectPrompt": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "select" + ] + }, + "key": { + "type": "string" + }, + "message": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + }, + "hint": { + "type": "string" + } + }, + "required": [ + "label", + "value" + ], + "additionalProperties": false + } + }, + "when": { + "$ref": "#/components/schemas/Integration.When" + } + }, + "required": [ + "type", + "key", + "message", + "options" + ], + "additionalProperties": false + }, + "Integration.OAuthMethod": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "oauth" + ] + }, + "label": { + "type": "string" + }, + "prompts": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Integration.TextPrompt" + }, + { + "$ref": "#/components/schemas/Integration.SelectPrompt" + } + ] + } + } + }, + "required": [ + "id", + "type", + "label" + ], + "additionalProperties": false + }, + "Integration.CommandMethod": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "command" + ] + }, + "label": { + "type": "string" + }, + "command": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "type", + "label", + "command" + ], + "additionalProperties": false + }, + "Integration.KeyMethod": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "key" + ] + }, + "label": { + "type": "string" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "Integration.EnvMethod": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "env" + ] + }, + "names": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "type", + "names" + ], + "additionalProperties": false + }, + "Integration.Method": { + "anyOf": [ + { + "$ref": "#/components/schemas/Integration.OAuthMethod" + }, + { + "$ref": "#/components/schemas/Integration.CommandMethod" + }, + { + "$ref": "#/components/schemas/Integration.KeyMethod" + }, + { + "$ref": "#/components/schemas/Integration.EnvMethod" + } + ] + }, + "Connection.CredentialInfo": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "credential" + ] + }, + "id": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": [ + "type", + "id", + "label" + ], + "additionalProperties": false + }, + "Connection.EnvInfo": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "env" + ] + }, + "name": { + "type": "string" + } + }, + "required": [ + "type", + "name" + ], + "additionalProperties": false + }, + "Connection.Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/Connection.CredentialInfo" + }, + { + "$ref": "#/components/schemas/Connection.EnvInfo" + } + ] + }, + "Integration.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "methods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Integration.Method" + } + }, + "connections": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Connection.Info" + } + } + }, + "required": [ + "id", + "name", + "methods", + "connections" + ], + "additionalProperties": false + }, + "Integration.Attempt": { + "type": "object", + "properties": { + "attemptID": { + "type": "string" + }, + "url": { + "type": "string" + }, + "instructions": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "auto", + "code" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "attemptID", + "url", + "instructions", + "mode", + "time" + ], + "additionalProperties": false + }, + "Integration.AttemptStatus": { + "anyOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "pending" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "time" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "complete" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "time" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "failed" + ] + }, + "message": { + "type": "string" + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "message", + "time" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "expired" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "time" + ], + "additionalProperties": false } ] }, - "Integration.Info": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "methods": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Integration.Method" - } - }, - "connections": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Connection.Info" - } - } - }, - "required": [ - "id", - "name", - "methods", - "connections" - ], - "additionalProperties": false - }, - "Integration.Attempt": { + "Integration.CommandAttempt": { "type": "object", "properties": { "attemptID": { "type": "string" }, - "url": { - "type": "string" - }, - "instructions": { - "type": "string" - }, - "mode": { - "type": "string", - "enum": [ - "auto", - "code" - ] - }, "time": { "type": "object", "properties": { @@ -17337,14 +19518,11 @@ }, "required": [ "attemptID", - "url", - "instructions", - "mode", "time" ], "additionalProperties": false }, - "Integration.AttemptStatus": { + "Integration.CommandAttemptStatus": { "anyOf": [ { "type": "object", @@ -17355,6 +19533,9 @@ "pending" ] }, + "message": { + "type": "string" + }, "time": { "type": "object", "properties": { @@ -17777,114 +19958,413 @@ ] } }, - "required": [ - "status" - ], + "required": [ + "status" + ], + "additionalProperties": false + }, + "Mcp.Status.Disabled": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "disabled" + ] + } + }, + "required": [ + "status" + ], + "additionalProperties": false + }, + "Mcp.Status.Failed": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "failed" + ] + }, + "error": { + "type": "string" + } + }, + "required": [ + "status", + "error" + ], + "additionalProperties": false + }, + "Mcp.Status.NeedsAuth": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "needs_auth" + ] + } + }, + "required": [ + "status" + ], + "additionalProperties": false + }, + "Mcp.Status.NeedsClientRegistration": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "needs_client_registration" + ] + }, + "error": { + "type": "string" + } + }, + "required": [ + "status", + "error" + ], + "additionalProperties": false + }, + "Mcp.Server": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "status": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.Status.Connected" + }, + { + "$ref": "#/components/schemas/Mcp.Status.Pending" + }, + { + "$ref": "#/components/schemas/Mcp.Status.Disabled" + }, + { + "$ref": "#/components/schemas/Mcp.Status.Failed" + }, + { + "$ref": "#/components/schemas/Mcp.Status.NeedsAuth" + }, + { + "$ref": "#/components/schemas/Mcp.Status.NeedsClientRegistration" + } + ] + }, + "integrationID": { + "type": "string" + } + }, + "required": [ + "name", + "status" + ], + "additionalProperties": false + }, + "Mcp.TimeoutConfig": { + "type": "object", + "properties": { + "startup": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ], + "description": "Maximum time in milliseconds to establish and initialize the MCP server." + }, + "catalog": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ], + "description": "Maximum time in milliseconds to wait for MCP discovery requests such as tools/list and prompts/list." + }, + "execution": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ], + "description": "Maximum time in milliseconds to wait for MCP tool and prompt execution." + } + }, "additionalProperties": false }, - "Mcp.Status.Disabled": { + "Mcp.LocalConfig": { "type": "object", "properties": { - "status": { + "type": { "type": "string", "enum": [ - "disabled" + "local" ] - } - }, - "required": [ - "status" - ], - "additionalProperties": false - }, - "Mcp.Status.Failed": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "failed" + }, + "command": { + "type": "array", + "items": { + "type": "string" + } + }, + "cwd": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Working directory for the MCP server process. Relative paths resolve from the workspace directory." + }, + "environment": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } ] }, - "error": { - "type": "string" + "disabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "codemode": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Expose this server's tools through Code Mode. Defaults to true." + }, + "timeout": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.TimeoutConfig" + }, + { + "type": "null" + } + ] } }, "required": [ - "status", - "error" + "type", + "command" ], "additionalProperties": false }, - "Mcp.Status.NeedsAuth": { + "Mcp.OAuthConfig": { "type": "object", "properties": { - "status": { - "type": "string", - "enum": [ - "needs_auth" + "client_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "client_secret": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "scope": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "callback_port": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 1, + "maximum": 65535 + } + ] + }, + { + "type": "null" + } + ] + }, + "redirect_uri": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } ] } }, - "required": [ - "status" - ], "additionalProperties": false }, - "Mcp.Status.NeedsClientRegistration": { + "Mcp.RemoteConfig": { "type": "object", "properties": { - "status": { + "type": { "type": "string", "enum": [ - "needs_client_registration" + "remote" ] }, - "error": { - "type": "string" - } - }, - "required": [ - "status", - "error" - ], - "additionalProperties": false - }, - "Mcp.Server": { - "type": "object", - "properties": { - "name": { + "url": { "type": "string" }, - "status": { + "headers": { "anyOf": [ { - "$ref": "#/components/schemas/Mcp.Status.Connected" + "type": "object", + "additionalProperties": { + "type": "string" + } }, { - "$ref": "#/components/schemas/Mcp.Status.Pending" + "type": "null" + } + ] + }, + "oauth": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.OAuthConfig" + }, + { + "type": "boolean", + "enum": [ + false + ] + } + ] }, { - "$ref": "#/components/schemas/Mcp.Status.Disabled" + "type": "null" + } + ] + }, + "disabled": { + "anyOf": [ + { + "type": "boolean" }, { - "$ref": "#/components/schemas/Mcp.Status.Failed" + "type": "null" + } + ] + }, + "codemode": { + "anyOf": [ + { + "type": "boolean" }, { - "$ref": "#/components/schemas/Mcp.Status.NeedsAuth" + "type": "null" + } + ], + "description": "Expose this server's tools through Code Mode. Defaults to true." + }, + "timeout": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.TimeoutConfig" }, { - "$ref": "#/components/schemas/Mcp.Status.NeedsClientRegistration" + "type": "null" } ] + } + }, + "required": [ + "type", + "url" + ], + "additionalProperties": false + }, + "McpServerNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "McpServerNotFoundError" + ] }, - "integrationID": { + "server": { + "type": "string" + }, + "message": { "type": "string" } }, "required": [ - "name", - "status" + "_tag", + "server", + "message" ], "additionalProperties": false }, @@ -18664,65 +21144,70 @@ ], "additionalProperties": false }, - "Form.FormInfo": { + "Form.ExternalField": { "type": "object", "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^frm_" - } - ] - }, - "sessionID": { - "type": "string" - }, - "title": { + "key": { "type": "string" }, - "metadata": { - "$ref": "#/components/schemas/Form.Metadata" - }, - "mode": { + "type": { "type": "string", "enum": [ - "form" + "external" ] }, - "fields": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/Form.StringField" - }, - { - "$ref": "#/components/schemas/Form.NumberField" - }, - { - "$ref": "#/components/schemas/Form.IntegerField" - }, - { - "$ref": "#/components/schemas/Form.BooleanField" - }, - { - "$ref": "#/components/schemas/Form.MultiselectField" - } - ] - } + "url": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" } }, "required": [ - "id", - "sessionID", - "title", - "mode", - "fields" + "key", + "type", + "url" ], "additionalProperties": false }, - "Form.UrlInfo": { + "Form.Field": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.StringField" + }, + { + "$ref": "#/components/schemas/Form.NumberField" + }, + { + "$ref": "#/components/schemas/Form.IntegerField" + }, + { + "$ref": "#/components/schemas/Form.BooleanField" + }, + { + "$ref": "#/components/schemas/Form.MultiselectField" + }, + { + "$ref": "#/components/schemas/Form.ExternalField" + } + ] + }, + "Form.Fields": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/Form.Field" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/Form.Field" + } + }, + "Form.Info": { "type": "object", "properties": { "id": { @@ -18742,22 +21227,15 @@ "metadata": { "$ref": "#/components/schemas/Form.Metadata" }, - "mode": { - "type": "string", - "enum": [ - "url" - ] - }, - "url": { - "type": "string" + "fields": { + "$ref": "#/components/schemas/Form.Fields" } }, "required": [ "id", "sessionID", "title", - "mode", - "url" + "fields" ], "additionalProperties": false }, @@ -18785,56 +21263,13 @@ "metadata": { "$ref": "#/components/schemas/Form.Metadata" }, - "mode": { - "type": "string", - "enum": [ - "form", - "url" - ] - }, "fields": { - "anyOf": [ - { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/Form.StringField" - }, - { - "$ref": "#/components/schemas/Form.NumberField" - }, - { - "$ref": "#/components/schemas/Form.IntegerField" - }, - { - "$ref": "#/components/schemas/Form.BooleanField" - }, - { - "$ref": "#/components/schemas/Form.MultiselectField" - } - ] - } - }, - { - "type": "null" - } - ] - }, - "url": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "$ref": "#/components/schemas/Form.Fields" } }, "required": [ "title", - "mode" + "fields" ], "additionalProperties": false }, @@ -22811,6 +25246,75 @@ ], "additionalProperties": false }, + "session.tool.progress": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.tool.progress" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "callID", + "metadata" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, "session.compaction.delta": { "type": "object", "properties": { @@ -23689,7 +26193,7 @@ "type": "object", "properties": { "info": { - "$ref": "#/components/schemas/Shell" + "$ref": "#/components/schemas/Shell.Info" } }, "required": [ @@ -23744,29 +26248,7 @@ ] }, "exit": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "NaN" - ] - }, - { - "type": "string", - "enum": [ - "Infinity" - ] - }, - { - "type": "string", - "enum": [ - "-Infinity" - ] - } - ] + "type": "number" }, "status": { "type": "string", @@ -24567,73 +27049,49 @@ "type": "string" } } - }, - "required": [ - "key", - "type", - "options" - ], - "additionalProperties": false - }, - "Form.FormInfo1": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^frm_" - } - ] - }, - "sessionID": { - "type": "string" - }, - "title": { - "type": "string" - }, - "metadata": { - "$ref": "#/components/schemas/Form.Metadata1" - }, - "mode": { - "type": "string", - "enum": [ - "form" - ] - }, - "fields": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/Form.StringField1" - }, - { - "$ref": "#/components/schemas/Form.NumberField1" - }, - { - "$ref": "#/components/schemas/Form.IntegerField1" - }, - { - "$ref": "#/components/schemas/Form.BooleanField1" - }, - { - "$ref": "#/components/schemas/Form.MultiselectField1" - } - ] - } - } - }, - "required": [ - "id", - "sessionID", - "title", - "mode", - "fields" + }, + "required": [ + "key", + "type", + "options" ], "additionalProperties": false }, - "Form.UrlInfo1": { + "Form.Field1": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.StringField1" + }, + { + "$ref": "#/components/schemas/Form.NumberField1" + }, + { + "$ref": "#/components/schemas/Form.IntegerField1" + }, + { + "$ref": "#/components/schemas/Form.BooleanField1" + }, + { + "$ref": "#/components/schemas/Form.MultiselectField1" + }, + { + "$ref": "#/components/schemas/Form.ExternalField" + } + ] + }, + "Form.Fields1": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/Form.Field1" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/Form.Field1" + } + }, + "Form.Info1": { "type": "object", "properties": { "id": { @@ -24653,22 +27111,15 @@ "metadata": { "$ref": "#/components/schemas/Form.Metadata1" }, - "mode": { - "type": "string", - "enum": [ - "url" - ] - }, - "url": { - "type": "string" + "fields": { + "$ref": "#/components/schemas/Form.Fields1" } }, "required": [ "id", "sessionID", "title", - "mode", - "url" + "fields" ], "additionalProperties": false }, @@ -24702,14 +27153,7 @@ "type": "object", "properties": { "form": { - "anyOf": [ - { - "$ref": "#/components/schemas/Form.FormInfo1" - }, - { - "$ref": "#/components/schemas/Form.UrlInfo1" - } - ] + "$ref": "#/components/schemas/Form.Info1" } }, "required": [ @@ -24889,6 +27333,51 @@ ], "additionalProperties": false }, + "websearch.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "websearch.updated" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, "SessionStatus": { "anyOf": [ { @@ -26262,10 +28751,10 @@ "$ref": "#/components/schemas/session.forked" }, { - "$ref": "#/components/schemas/session.prompt.promoted" + "$ref": "#/components/schemas/session.input.promoted" }, { - "$ref": "#/components/schemas/session.prompt.admitted" + "$ref": "#/components/schemas/session.input.admitted" }, { "$ref": "#/components/schemas/session.execution.started" @@ -26438,6 +28927,9 @@ { "$ref": "#/components/schemas/form.cancelled" }, + { + "$ref": "#/components/schemas/websearch.updated" + }, { "$ref": "#/components/schemas/session.status" }, @@ -26494,7 +28986,7 @@ } ] }, - "V2EventStream": { + "V2EventJsonString": { "type": "string", "contentSchema": { "$ref": "#/components/schemas/V2Event" @@ -26564,7 +29056,7 @@ ], "additionalProperties": false }, - "Shell1": { + "Shell.Info1": { "type": "object", "properties": { "id": { @@ -26605,41 +29097,7 @@ ] }, "exit": { - "anyOf": [ - { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "NaN" - ] - }, - { - "type": "string", - "enum": [ - "Infinity" - ] - }, - { - "type": "string", - "enum": [ - "-Infinity" - ] - } - ] - }, - { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] - } - ] + "type": "number" }, "metadata": { "type": "object" @@ -26648,78 +29106,10 @@ "type": "object", "properties": { "started": { - "anyOf": [ - { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "NaN" - ] - }, - { - "type": "string", - "enum": [ - "Infinity" - ] - }, - { - "type": "string", - "enum": [ - "-Infinity" - ] - } - ] - }, - { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] - } - ] + "type": "number" }, "completed": { - "anyOf": [ - { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "NaN" - ] - }, - { - "type": "string", - "enum": [ - "Infinity" - ] - }, - { - "type": "string", - "enum": [ - "-Infinity" - ] - } - ] - }, - { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] - } - ] + "type": "number" } }, "required": [ @@ -27023,6 +29413,69 @@ "working", "branch" ] + }, + "WebSearch.Provider": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "additionalProperties": false + }, + "WebSearch.Result": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "title": { + "type": "string" + }, + "content": { + "type": "string" + }, + "time": { + "type": "object", + "properties": { + "published": { + "type": "number" + } + }, + "additionalProperties": false + } + }, + "required": [ + "url", + "time" + ], + "additionalProperties": false + }, + "WebSearch.Response": { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebSearch.Result" + } + } + }, + "required": [ + "providerID", + "results" + ], + "additionalProperties": false } }, "securitySchemes": {} @@ -27130,6 +29583,10 @@ }, { "name": "debug" + }, + { + "name": "websearch", + "description": "Location-scoped web search routes." } ] } diff --git a/packages/www/script/generate-openapi.ts b/packages/www/script/generate-openapi.ts new file mode 100644 index 000000000000..80b9c72ca39c --- /dev/null +++ b/packages/www/script/generate-openapi.ts @@ -0,0 +1,18 @@ +import { fileURLToPath } from "url" + +const source = fileURLToPath(new URL("../../protocol/openapi.json", import.meta.url)) +const targets = ["../openapi.json", "../public/openapi.json"].map((path) => + fileURLToPath(new URL(path, import.meta.url)), +) +const document = await Bun.file(source).text() + +if (process.argv.includes("--check")) { + const stale = (await Promise.all(targets.map((path) => Bun.file(path).text()))).some((value) => value !== document) + if (stale) { + console.error("Generated OpenAPI documents are stale. Run `bun run generate` from packages/www.") + process.exit(1) + } + process.exit(0) +} + +await Promise.all(targets.map((path) => Bun.write(path, document))) diff --git a/script/generate.ts b/script/generate.ts index e51808342f82..ff4f29e6d52a 100755 --- a/script/generate.ts +++ b/script/generate.ts @@ -2,9 +2,7 @@ import { $ } from "bun" -await $`bun ./packages/sdk/js/script/build.ts` - -await $`bun dev generate > ../sdk/openapi.json`.cwd("packages/opencode") +await $`bun run generate`.cwd("packages/protocol") await $`bun run generate`.cwd("packages/www") diff --git a/script/publish.ts b/script/publish.ts index 3d74fd68121f..3623eeb77765 100755 --- a/script/publish.ts +++ b/script/publish.ts @@ -26,7 +26,6 @@ async function prepareReleaseFiles() { } await $`bun install` - await $`./packages/sdk/js/script/build.ts` } if (Script.release && !Script.preview) { @@ -54,9 +53,6 @@ await $`bun ./packages/client/script/publish.ts` console.log("\n=== cli ===\n") await $`bun ./packages/cli/script/publish.ts` -console.log("\n=== sdk ===\n") -await $`bun ./packages/sdk/js/script/publish.ts` - console.log("\n=== plugin ===\n") await $`bun ./packages/plugin/script/publish.ts` diff --git a/script/raw-changelog.ts b/script/raw-changelog.ts index 2ea76247d58b..effa0f1d4727 100644 --- a/script/raw-changelog.ts +++ b/script/raw-changelog.ts @@ -120,7 +120,7 @@ async function commits(from: string, to: string) { } const log = - await $`git log ${base}..${head} --format=%H -- packages/opencode packages/sdk packages/plugin packages/desktop packages/app sdks/vscode packages/extensions github`.text() + await $`git log ${base}..${head} --format=%H -- packages/opencode packages/plugin packages/desktop packages/app sdks/vscode packages/extensions github`.text() const list: Commit[] = [] for (const hash of log.split("\n").filter(Boolean)) { @@ -136,7 +136,7 @@ async function commits(from: string, to: string) { else if (file.startsWith("packages/opencode/")) areas.add("core") else if (file.startsWith("packages/desktop/src-tauri/")) areas.add("tauri") else if (file.startsWith("packages/desktop/") || file.startsWith("packages/app/")) areas.add("app") - else if (file.startsWith("packages/sdk/") || file.startsWith("packages/plugin/")) areas.add("sdk") + else if (file.startsWith("packages/plugin/")) areas.add("sdk") else if (file.startsWith("sdks/vscode/") || file.startsWith("github/")) areas.add("extensions/vscode") } From 0fd73a29760536bfcf9ead0832a1cf3c29612022 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sun, 26 Jul 2026 17:29:27 -0500 Subject: [PATCH 126/150] fix(core): align grep behavior and guidance (#38999) --- packages/core/src/tool/grep.ts | 56 ++++++++------- packages/core/test/tool-search.test.ts | 97 +++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 25 deletions(-) diff --git a/packages/core/src/tool/grep.ts b/packages/core/src/tool/grep.ts index 0f8a2d60d3b0..93267b34bea1 100644 --- a/packages/core/src/tool/grep.ts +++ b/packages/core/src/tool/grep.ts @@ -7,6 +7,7 @@ import path from "path" import { FileSystem } from "../filesystem" import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "../location" +import { LocationMutation } from "../location-mutation" import { PermissionV2 } from "../permission" import { Ripgrep } from "../ripgrep" import { RelativePath } from "../schema" @@ -18,27 +19,27 @@ export const Input = Schema.Struct({ pattern: FileSystem.GrepInput.fields.pattern.check( Schema.isMinLength(1, { message: "Pattern must not be empty" }), ).annotate({ - description: "Regex pattern to search for in file contents", + description: "Regular expression to search for in file contents (ripgrep syntax)", }), path: RelativePath.pipe(Schema.optional).annotate({ - description: "Relative directory to search. Defaults to the active Location.", + description: "File or directory to search. Defaults to the current working directory.", }), include: FileSystem.GrepInput.fields.include.annotate({ - description: 'File glob to include in the search (for example, "*.js" or "*.{ts,tsx}")', + description: 'Glob pattern to filter files (for example, "*.js" or "*.{ts,tsx}")', }), limit: FileSystem.GrepInput.fields.limit.annotate({ - description: `Maximum matches to return (default: ${FileSystem.DEFAULT_SEARCH_LIMIT})`, + description: `Maximum number of matching lines to return (default: ${FileSystem.DEFAULT_SEARCH_LIMIT})`, }), }) export const Output = Schema.Array(FileSystem.Match) -type ModelOutput = typeof Output.Encoded +type EncodedOutput = typeof Output.Encoded -/** Format raw search matches into the familiar concise model output. */ -export const toModelOutput = (output: ModelOutput, truncated = false) => { - const lines = output.length === 0 ? ["No files found"] : [`Found ${output.length} matches`] +/** Format raw search matches into concise model content. */ +export const toModelContent = (matches: EncodedOutput, truncated = false) => { + const lines = matches.length === 0 ? ["No matches found"] : [`Found ${matches.length} matches`] let current = "" - for (const match of output) { + for (const match of matches) { if (current !== match.entry.path) { if (current) lines.push("") current = match.entry.path @@ -49,7 +50,7 @@ export const toModelOutput = (output: ModelOutput, truncated = false) => { if (truncated) lines.push( "", - `(Results are truncated: showing first ${output.length} results. Consider using a more specific path or pattern.)`, + `(Results are truncated: showing first ${matches.length} results. Consider using a more specific path or pattern.)`, ) return lines.join("\n") } @@ -61,6 +62,7 @@ export const Plugin = { const fs = yield* FSUtil.Service const ripgrep = yield* Ripgrep.Service const location = yield* Location.Service + const mutation = yield* LocationMutation.Service const permission = yield* PermissionV2.Service yield* ctx.tool @@ -69,11 +71,20 @@ export const Plugin = { name, Tool.make({ description: - "Search file contents by regular expression within the active Location or an absolute managed tool-output file. Use a path to narrow the search, include to filter files by glob, and limit to bound the match count. Returns concise file resources, line numbers, and bounded line previews.", + "Search file contents using regular expressions. Use it to locate specific code, symbols, or text patterns, and narrow searches with `path` or `include`. Returns matching file paths, line numbers, and line previews.", input: Input, output: Output, execute: (input, context) => Effect.gen(function* () { + const source = { type: "tool" as const, messageID: context.messageID, callID: context.callID } + const target = yield* mutation.resolve({ path: input.path ?? "." }) + if (target.externalDirectory) + yield* permission.assert({ + ...LocationMutation.externalDirectoryPermission(target.externalDirectory), + sessionID: context.sessionID, + agent: context.agent, + source, + }) yield* permission.assert({ action: name, resources: [input.pattern], @@ -86,22 +97,23 @@ export const Plugin = { }, sessionID: context.sessionID, agent: context.agent, - source: { type: "tool", messageID: context.messageID, callID: context.callID }, + source, }) - const target = path.resolve(location.directory, input.path ?? ".") + const root = path.resolve(location.directory, input.path ?? ".") const info = yield* fs - .stat(target) + .stat(root) .pipe( Effect.catchReason("PlatformError", "NotFound", () => Effect.fail(new ToolFailure({ message: `Search path does not exist: ${input.path ?? "."}` })), ), ) + const cwd = info?.type === "Directory" ? root : path.dirname(root) const limit = input.limit ?? FileSystem.DEFAULT_SEARCH_LIMIT const matches = yield* ripgrep .grep({ - cwd: info?.type === "Directory" ? target : path.dirname(target), + cwd, pattern: input.pattern, - file: info?.type === "File" ? path.basename(target) : undefined, + file: info?.type === "File" ? path.basename(root) : undefined, include: input.include, limit: limit + 1, }) @@ -113,13 +125,7 @@ export const Plugin = { entry: FileSystem.Entry.make({ ...match.entry, path: RelativePath.make( - path.relative( - location.directory, - path.resolve( - info?.type === "Directory" ? target : path.dirname(target), - match.entry.path, - ), - ), + path.relative(location.directory, path.resolve(cwd, match.entry.path)), ), }), }), @@ -130,7 +136,7 @@ export const Plugin = { }).pipe( Effect.map((result) => ({ output: result.matches, - content: toModelOutput( + content: toModelContent( result.matches.map((match) => ({ ...match, entry: { ...match.entry, path: path.resolve(location.directory, match.entry.path) }, @@ -142,6 +148,8 @@ export const Plugin = { Effect.mapError((error) => error instanceof ToolFailure ? error + : error instanceof Ripgrep.InvalidPatternError + ? new ToolFailure({ message: `Invalid regex pattern: ${error.message}` }) : new ToolFailure({ message: `Unable to grep for ${input.pattern}`, error }), ), ), diff --git a/packages/core/test/tool-search.test.ts b/packages/core/test/tool-search.test.ts index 5318e5279801..8b688a23ffd4 100644 --- a/packages/core/test/tool-search.test.ts +++ b/packages/core/test/tool-search.test.ts @@ -37,7 +37,14 @@ const globToolNode = makeLocationNode({ const grepToolNode = makeLocationNode({ name: "test/grep-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(GrepTool.Plugin)), - deps: [ToolRegistry.toolsNode, FSUtil.node, Ripgrep.node, Location.node, PermissionV2.node], + deps: [ + ToolRegistry.toolsNode, + FSUtil.node, + Ripgrep.node, + Location.node, + LocationMutation.node, + PermissionV2.node, + ], }) const sessionID = SessionV2.ID.make("ses_search_tool_test") @@ -183,6 +190,94 @@ describe("search tools", () => { ), ) + it.live("reports no grep matches", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => + Effect.promise(() => fs.writeFile(path.join(tmp.path, "file.txt"), "haystack\n")).pipe( + Effect.andThen( + withTools(tmp.path, (registry) => executeTool(registry, call("grep", { pattern: "needle" }))), + ), + Effect.tap((result) => + Effect.sync(() => { + expect(result).toMatchObject({ + status: "completed", + content: [{ type: "text", text: "No matches found" }], + metadata: { matches: 0, truncated: false }, + }) + }), + ), + ), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("reports invalid grep regex details", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => + withTools(tmp.path, (registry) => + Effect.gen(function* () { + const result = yield* executeTool(registry, call("grep", { pattern: "[" })) + expect(result).toMatchObject({ + status: "error", + error: { type: "tool.execution" }, + }) + if (result.status !== "error") return + expect(result.error.message).toStartWith("Invalid regex pattern:") + expect(result.error.message).toContain("unclosed character class") + }), + ), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("requires external_directory approval for external grep files and directories", () => + Effect.acquireUseRelease( + Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), + ([active, outside]) => { + const assertions: PermissionV2.AssertInput[] = [] + return Effect.promise(() => fs.writeFile(path.join(outside.path, "outside.txt"), "needle\n")).pipe( + Effect.andThen( + withTools( + active.path, + (registry) => + Effect.gen(function* () { + const directory = yield* executeTool( + registry, + call("grep", { path: outside.path, pattern: "needle" }), + ) + const file = yield* executeTool( + registry, + call("grep", { path: path.join(outside.path, "outside.txt"), pattern: "needle" }), + ) + expect(directory.status).toBe("completed") + expect(file.status).toBe("completed") + }), + assertions, + ), + ), + Effect.tap(() => + Effect.sync(() => { + expect(assertions.map((input) => input.action)).toEqual([ + "external_directory", + "grep", + "external_directory", + "grep", + ]) + expect(assertions[0]?.resources).toEqual([path.join(outside.path, "*").replaceAll("\\", "/")]) + expect(assertions[2]?.resources).toEqual([path.join(outside.path, "*").replaceAll("\\", "/")]) + }), + ), + ) + }, + ([active, outside]) => + Effect.promise(() => + Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined), + ), + ), + ) + for (const name of ["glob", "grep"] as const) { it.live(`${name} reports a missing search path`, () => Effect.acquireUseRelease( From 8db7487c894b46d8093135a18f5cf23c8447a4e9 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 26 Jul 2026 20:08:55 -0400 Subject: [PATCH 127/150] refactor(core): consolidate tool architecture --- bun.lock | 34 +- package.json | 14 +- .../ai/src/protocols/anthropic-messages.ts | 6 +- packages/ai/src/protocols/gemini.ts | 4 +- packages/ai/src/protocols/open-responses.ts | 8 +- packages/ai/src/protocols/openai-chat.ts | 4 +- packages/ai/src/protocols/shared.ts | 4 +- packages/ai/src/schema/errors.ts | 7 +- packages/ai/src/schema/messages.ts | 12 +- packages/ai/src/tool-runtime.ts | 2 +- packages/ai/src/tool.ts | 12 +- packages/ai/test/tool-runtime.test.ts | 4 +- packages/app/src/context/server-sdk.test.ts | 2 +- packages/app/src/context/server-sdk.tsx | 10 +- packages/cli/src/acp/event.ts | 2 +- packages/cli/src/acp/permission.ts | 2 +- .../cli/src/node/plugin-runtime.effect.ts | 6 +- .../cli/src/node/plugin-runtime.promise.ts | 4 +- packages/cli/src/run/noninteractive.ts | 2 +- packages/cli/src/services/standalone.ts | 4 +- packages/cli/src/util/process.ts | 8 +- .../cli/test/acp/permission-behavior.test.ts | 2 +- packages/cli/test/fixture/standalone-owner.ts | 6 +- packages/cli/vite.node.config.ts | 31 +- packages/client/package.json | 2 +- packages/client/script/build.ts | 107 +- packages/client/src/effect/api/api.ts | 1522 ++++++++++----- .../client/src/effect/generated/client.ts | 1732 ++++++++--------- .../client/src/promise/generated/client.ts | 14 + .../client/src/promise/generated/types.ts | 320 ++- packages/client/test/api.types.ts | 7 +- .../client/test/contract-identity.test.ts | 8 + packages/core/package.json | 1 + packages/core/src/account.ts | 2 +- packages/core/src/account/sql.ts | 16 +- packages/core/src/agent.ts | 24 +- packages/core/src/aisdk.ts | 40 +- packages/core/src/{event.ts => bus.ts} | 116 +- packages/core/src/catalog.ts | 102 +- packages/core/src/codemode.ts | 77 - .../src/{tool/execute.ts => codemode/tool.ts} | 59 +- packages/core/src/command.ts | 20 +- packages/core/src/config.ts | 14 +- packages/core/src/config/agent.ts | 2 +- packages/core/src/config/attachments.ts | 4 +- packages/core/src/config/command.ts | 2 +- packages/core/src/config/compaction.ts | 4 +- packages/core/src/config/formatter.ts | 2 +- packages/core/src/config/lsp.ts | 2 +- packages/core/src/config/mcp.ts | 2 +- packages/core/src/config/plugin.ts | 2 +- packages/core/src/config/plugin/agent.ts | 16 +- packages/core/src/config/plugin/command.ts | 4 +- packages/core/src/config/plugin/policy.ts | 2 +- packages/core/src/config/plugin/provider.ts | 22 +- packages/core/src/config/plugin/reference.ts | 2 +- packages/core/src/config/plugin/skill.ts | 14 +- packages/core/src/config/plugin/websearch.ts | 2 +- packages/core/src/config/provider.ts | 24 +- packages/core/src/config/reference.ts | 4 +- packages/core/src/config/tool-output.ts | 2 +- packages/core/src/config/warming.ts | 2 +- packages/core/src/config/watcher.ts | 2 +- .../core/src/control-plane/move-session.ts | 24 +- .../core/src/control-plane/workspace.sql.ts | 8 +- packages/core/src/credential.ts | 2 +- packages/core/src/database/database.ts | 2 +- packages/core/src/event-logger.ts | 8 +- packages/core/src/event/sql.ts | 4 +- packages/core/src/file-mutation.ts | 12 +- packages/core/src/filesystem.ts | 2 +- .../core/src/filesystem/location-watcher.ts | 8 +- packages/core/src/filesystem/search.ts | 2 +- packages/core/src/form.ts | 14 +- packages/core/src/generate.ts | 6 +- packages/core/src/git.ts | 2 +- packages/core/src/github-copilot/models.ts | 34 +- packages/core/src/instruction-discovery.ts | 2 +- packages/core/src/instructions/builtins.ts | 2 +- packages/core/src/integration.ts | 32 +- packages/core/src/kv.ts | 2 +- packages/core/src/location-mutation.ts | 2 +- packages/core/src/location-services.ts | 35 +- packages/core/src/mcp/index.ts | 48 +- packages/core/src/mcp/instructions.ts | 12 +- packages/core/src/model-resolver.ts | 96 +- packages/core/src/model.ts | 8 +- packages/core/src/models-dev.ts | 116 +- packages/core/src/permission.ts | 64 +- packages/core/src/permission/saved.ts | 8 +- packages/core/src/permission/sql.ts | 4 +- packages/core/src/plugin.ts | 50 +- packages/core/src/plugin/agent.ts | 54 +- packages/core/src/plugin/command.ts | 2 +- packages/core/src/plugin/hooks.ts | 8 +- packages/core/src/plugin/host.ts | 165 +- packages/core/src/plugin/internal.ts | 62 +- packages/core/src/plugin/models-dev.ts | 11 +- packages/core/src/plugin/promise.ts | 78 +- packages/core/src/plugin/provider/alibaba.ts | 2 +- .../src/plugin/provider/amazon-bedrock.ts | 14 +- .../core/src/plugin/provider/anthropic.ts | 8 +- packages/core/src/plugin/provider/azure.ts | 20 +- packages/core/src/plugin/provider/cerebras.ts | 8 +- .../plugin/provider/cloudflare-ai-gateway.ts | 2 +- .../plugin/provider/cloudflare-workers-ai.ts | 10 +- packages/core/src/plugin/provider/cohere.ts | 2 +- .../core/src/plugin/provider/deepinfra.ts | 2 +- packages/core/src/plugin/provider/dynamic.ts | 2 +- packages/core/src/plugin/provider/gateway.ts | 2 +- .../src/plugin/provider/github-copilot.ts | 34 +- packages/core/src/plugin/provider/gitlab.ts | 6 +- .../core/src/plugin/provider/google-vertex.ts | 24 +- packages/core/src/plugin/provider/google.ts | 2 +- packages/core/src/plugin/provider/groq.ts | 2 +- packages/core/src/plugin/provider/kilo.ts | 8 +- .../core/src/plugin/provider/llmgateway.ts | 8 +- packages/core/src/plugin/provider/mistral.ts | 2 +- packages/core/src/plugin/provider/nvidia.ts | 8 +- .../src/plugin/provider/openai-compatible.ts | 2 +- packages/core/src/plugin/provider/openai.ts | 26 +- packages/core/src/plugin/provider/opencode.ts | 30 +- .../core/src/plugin/provider/openrouter.ts | 12 +- .../core/src/plugin/provider/perplexity.ts | 2 +- .../core/src/plugin/provider/sap-ai-core.ts | 8 +- .../src/plugin/provider/snowflake-cortex.ts | 6 +- .../core/src/plugin/provider/togetherai.ts | 2 +- packages/core/src/plugin/provider/venice.ts | 2 +- packages/core/src/plugin/provider/vercel.ts | 8 +- packages/core/src/plugin/provider/xai.ts | 8 +- packages/core/src/plugin/provider/zenmux.ts | 8 +- packages/core/src/plugin/runtime.ts | 14 +- packages/core/src/plugin/sdk.ts | 18 +- packages/core/src/plugin/skill.ts | 20 +- packages/core/src/plugin/skill/opencode.md | 38 +- packages/core/src/plugin/supervisor.ts | 47 +- packages/core/src/plugin/system-prompt.ts | 10 +- packages/core/src/plugin/variant.ts | 12 +- packages/core/src/plugin/warming.ts | 2 +- packages/core/src/plugin/websearch/exa.ts | 2 +- .../core/src/plugin/websearch/parallel.ts | 2 +- packages/core/src/project.ts | 3 +- packages/core/src/project/copy.ts | 8 +- packages/core/src/provider.ts | 8 +- packages/core/src/pty.ts | 18 +- packages/core/src/pty/ticket.ts | 4 +- packages/core/src/question.ts | 30 +- packages/core/src/reference.ts | 12 +- packages/core/src/reference/instructions.ts | 2 +- packages/core/src/ripgrep.ts | 2 +- packages/core/src/session.ts | 185 +- packages/core/src/session/compaction.ts | 28 +- packages/core/src/session/context.ts | 30 +- packages/core/src/session/execution.ts | 16 +- .../core/src/session/execution/restart.ts | 2 +- packages/core/src/session/generate-node.ts | 4 +- packages/core/src/session/generate.ts | 2 +- packages/core/src/session/info.ts | 22 +- .../core/src/session/instruction-entry.ts | 2 +- .../core/src/session/instruction-state.ts | 21 +- packages/core/src/session/instructions.ts | 14 +- packages/core/src/session/model-request.ts | 50 +- packages/core/src/session/pending.ts | 24 +- packages/core/src/session/projector.ts | 112 +- packages/core/src/session/revert.ts | 14 +- packages/core/src/session/runner/index.ts | 4 +- packages/core/src/session/runner/llm.ts | 45 +- packages/core/src/session/runner/model.ts | 20 +- .../src/session/runner/publish-llm-event.ts | 118 +- packages/core/src/session/runner/retry.ts | 6 +- .../core/src/session/runner/to-llm-message.ts | 10 +- packages/core/src/session/sql.ts | 8 +- packages/core/src/session/store.ts | 2 +- packages/core/src/session/title.ts | 24 +- packages/core/src/session/to-session-error.ts | 14 +- packages/core/src/session/usage.ts | 6 +- packages/core/src/shell.ts | 14 +- packages/core/src/skill.ts | 34 +- packages/core/src/skill/discovery.ts | 2 +- packages/core/src/skill/instructions.ts | 18 +- packages/core/src/snapshot.ts | 2 +- packages/core/src/tool-output-store.ts | 201 -- packages/core/src/tool.ts | 285 +++ packages/core/src/tool/AGENTS.md | 29 +- packages/core/src/tool/hooks.ts | 79 - packages/core/src/tool/mcp.ts | 92 +- packages/core/src/tool/{ => plugin}/edit.ts | 27 +- packages/core/src/tool/{ => plugin}/glob.ts | 23 +- packages/core/src/tool/{ => plugin}/grep.ts | 23 +- packages/core/src/tool/{ => plugin}/patch.ts | 17 +- .../core/src/tool/{ => plugin}/question.ts | 31 +- packages/core/src/tool/{ => plugin}/read.ts | 32 +- packages/core/src/tool/{ => plugin}/shell.ts | 30 +- packages/core/src/tool/{ => plugin}/skill.ts | 23 +- .../core/src/tool/{ => plugin}/subagent.ts | 27 +- .../core/src/tool/{ => plugin}/webfetch.ts | 15 +- .../core/src/tool/{ => plugin}/websearch.ts | 16 +- packages/core/src/tool/{ => plugin}/write.ts | 27 +- packages/core/src/tool/registry.ts | 377 ---- packages/core/src/tool/runtime.ts | 125 ++ packages/core/src/tool/tool.ts | 90 - packages/core/src/tool/tools.ts | 23 - packages/core/src/v1/config/migrate.ts | 10 +- packages/core/src/v2-schema.ts | 3 - packages/core/src/vcs.ts | 2 +- packages/core/src/websearch.ts | 12 +- packages/core/src/wellknown.ts | 16 +- packages/core/src/wellknown/plugin.ts | 8 +- packages/core/src/workspace.ts | 2 +- packages/core/test/agent.test.ts | 56 +- packages/core/test/aisdk.test.ts | 18 +- .../core/test/{event.test.ts => bus.test.ts} | 590 +++--- packages/core/test/catalog.test.ts | 88 +- packages/core/test/codemode.test.ts | 20 +- .../core/test/codemode/instructions.test.ts | 32 +- packages/core/test/command.test.ts | 36 +- packages/core/test/config/agent.test.ts | 62 +- packages/core/test/config/command.test.ts | 32 +- packages/core/test/config/config.test.ts | 24 +- .../fixtures/plugin/directory-plugin.ts | 2 +- packages/core/test/config/plugin.test.ts | 67 +- packages/core/test/config/policy.test.ts | 34 +- packages/core/test/config/provider.test.ts | 38 +- packages/core/test/config/reload.test.ts | 36 +- packages/core/test/config/skill.test.ts | 22 +- packages/core/test/database-migration.test.ts | 14 +- packages/core/test/event-logger.test.ts | 20 +- packages/core/test/filesystem/watcher.test.ts | 14 +- packages/core/test/form.test.ts | 16 +- packages/core/test/generate.test.ts | 16 +- .../core/test/github-copilot/models.test.ts | 28 +- packages/core/test/instruction-state.test.ts | 11 +- packages/core/test/integration.test.ts | 10 +- packages/core/test/lib/image.ts | 2 +- packages/core/test/lib/tool.ts | 68 +- packages/core/test/location-layer.test.ts | 103 +- packages/core/test/location.test.ts | 4 +- packages/core/test/mcp-instructions.test.ts | 10 +- packages/core/test/mcp.test.ts | 67 +- packages/core/test/model-resolver.test.ts | 88 +- packages/core/test/model.test.ts | 18 +- packages/core/test/models.test.ts | 32 +- packages/core/test/move-session.test.ts | 20 +- packages/core/test/permission.test.ts | 130 +- packages/core/test/plugin.test.ts | 115 +- packages/core/test/plugin/command.test.ts | 6 +- packages/core/test/plugin/fixture.ts | 27 +- .../plugin/fixtures/config-effect-plugin.ts | 2 +- .../plugin/fixtures/config-promise-plugin.ts | 2 +- .../test/plugin/fixtures/failing-plugin.ts | 2 +- .../plugin/fixtures/variant-source-plugin.ts | 13 +- packages/core/test/plugin/host.ts | 82 +- packages/core/test/plugin/models-dev.test.ts | 196 +- packages/core/test/plugin/promise.test.ts | 108 +- .../plugin/provider-amazon-bedrock.test.ts | 218 +-- .../test/plugin/provider-anthropic.test.ts | 42 +- .../provider-azure-cognitive-services.test.ts | 68 +- .../core/test/plugin/provider-azure.test.ts | 126 +- .../test/plugin/provider-cerebras.test.ts | 54 +- .../provider-cloudflare-ai-gateway.test.ts | 100 +- .../provider-cloudflare-workers-ai.test.ts | 74 +- .../core/test/plugin/provider-dynamic.test.ts | 68 +- .../core/test/plugin/provider-factory.test.ts | 16 +- .../plugin/provider-github-copilot.test.ts | 124 +- .../core/test/plugin/provider-gitlab.test.ts | 72 +- .../provider-google-vertex-anthropic.test.ts | 96 +- .../plugin/provider-google-vertex.test.ts | 76 +- .../core/test/plugin/provider-google.test.ts | 38 +- .../core/test/plugin/provider-kilo.test.ts | 40 +- .../test/plugin/provider-llmgateway.test.ts | 24 +- .../core/test/plugin/provider-nvidia.test.ts | 28 +- .../plugin/provider-openai-compatible.test.ts | 44 +- .../core/test/plugin/provider-openai.test.ts | 118 +- .../test/plugin/provider-opencode.test.ts | 172 +- .../test/plugin/provider-openrouter.test.ts | 60 +- .../test/plugin/provider-sap-ai-core.test.ts | 26 +- .../plugin/provider-snowflake-cortex.test.ts | 56 +- .../core/test/plugin/provider-vercel.test.ts | 34 +- .../core/test/plugin/provider-xai.test.ts | 38 +- .../core/test/plugin/provider-zenmux.test.ts | 28 +- packages/core/test/plugin/skill.test.ts | 6 +- .../core/test/plugin/system-prompt.test.ts | 37 +- packages/core/test/plugin/variant.test.ts | 26 +- .../core/test/plugin/websearch-fixture.ts | 4 +- packages/core/test/project-copy.test.ts | 16 +- packages/core/test/project.test.ts | 64 +- packages/core/test/pty/pty-session.test.ts | 8 +- packages/core/test/pty/ticket.test.ts | 6 +- packages/core/test/question.test.ts | 71 +- packages/core/test/session-compact.test.ts | 28 +- packages/core/test/session-compaction.test.ts | 20 +- packages/core/test/session-create.test.ts | 196 +- packages/core/test/session-error.test.ts | 8 +- packages/core/test/session-execution.test.ts | 34 +- packages/core/test/session-generate.test.ts | 67 +- .../core/test/session-instructions.test.ts | 83 +- packages/core/test/session-log.test.ts | 61 +- packages/core/test/session-projector.test.ts | 107 +- packages/core/test/session-prompt.test.ts | 170 +- packages/core/test/session-remove.test.ts | 24 +- .../core/test/session-runner-message.test.ts | 34 +- .../core/test/session-runner-recorded.test.ts | 41 +- .../test/session-runner-tool-events.test.ts | 33 +- .../test/session-runner-tool-registry.test.ts | 261 ++- packages/core/test/session-runner.test.ts | 534 +++-- packages/core/test/session-skill.test.ts | 30 +- packages/core/test/session-title.test.ts | 42 +- .../core/test/session-tool-progress.test.ts | 28 +- packages/core/test/session-wait.test.ts | 20 +- packages/core/test/shared-schema.test.ts | 62 +- packages/core/test/skill.test.ts | 52 +- packages/core/test/skill/instructions.test.ts | 66 +- packages/core/test/tool-edit.test.ts | 32 +- packages/core/test/tool-execute.test.ts | 133 +- packages/core/test/tool-output-store.test.ts | 213 -- packages/core/test/tool-patch.test.ts | 34 +- packages/core/test/tool-question.test.ts | 34 +- packages/core/test/tool-read.test.ts | 69 +- .../test/tool-schema.test.ts} | 64 +- packages/core/test/tool-search.test.ts | 47 +- packages/core/test/tool-shell.test.ts | 66 +- packages/core/test/tool-skill.test.ts | 48 +- packages/core/test/tool-subagent.test.ts | 84 +- packages/core/test/tool-webfetch.test.ts | 44 +- packages/core/test/tool-websearch.test.ts | 30 +- packages/core/test/tool-write.test.ts | 32 +- packages/core/test/websearch.test.ts | 4 +- packages/core/test/wellknown.test.ts | 12 +- packages/http-recorder/README.md | 6 +- packages/http-recorder/package.json | 2 +- packages/httpapi-codegen/src/index.ts | 223 ++- .../httpapi-codegen/test/generate.test.ts | 67 +- packages/plugin/AGENTS.md | 6 + packages/plugin/package.json | 16 +- .../plugin/src/{v2/promise => }/README.md | 9 +- packages/plugin/src/{v2 => }/app.ts | 0 packages/plugin/src/{v2 => }/effect/PLAN.md | 10 +- packages/plugin/src/{v2 => }/effect/README.md | 2 +- packages/plugin/src/effect/agent.ts | 17 + packages/plugin/src/{v2 => }/effect/aisdk.ts | 0 .../plugin/src/{v2 => }/effect/catalog.ts | 23 +- .../plugin/src/{v2 => }/effect/command.ts | 2 +- packages/plugin/src/{v2 => }/effect/event.ts | 0 packages/plugin/src/{v2 => }/effect/index.ts | 0 .../plugin/src/{v2 => }/effect/integration.ts | 20 +- packages/plugin/src/{v2 => }/effect/plugin.ts | 0 .../plugin/src/{v2 => }/effect/reference.ts | 2 +- .../src/{v2 => }/effect/registration.ts | 0 .../plugin/src/{v2 => }/effect/session.ts | 0 packages/plugin/src/{v2 => }/effect/skill.ts | 6 +- packages/plugin/src/effect/tool.ts | 45 + .../plugin/src/{v2 => }/effect/websearch.ts | 0 packages/plugin/src/example-workspace.ts | 34 - packages/plugin/src/example.ts | 18 - packages/plugin/src/{v2 => }/options.ts | 0 packages/plugin/src/promise/agent.ts | 17 + packages/plugin/src/{v2 => }/promise/aisdk.ts | 0 packages/plugin/src/promise/catalog.ts | 33 + packages/plugin/src/promise/command.ts | 15 + packages/plugin/src/{v2 => }/promise/event.ts | 0 packages/plugin/src/{v2 => }/promise/index.ts | 0 packages/plugin/src/promise/integration.ts | 64 + .../plugin/src/{v2 => }/promise/plugin.ts | 0 packages/plugin/src/promise/reference.ts | 14 + .../src/{v2 => }/promise/registration.ts | 0 .../plugin/src/{v2 => }/promise/session.ts | 0 packages/plugin/src/{v2 => }/promise/skill.ts | 7 +- packages/plugin/src/promise/tool.ts | 62 + packages/plugin/src/promise/types.ts | 9 + .../plugin/src/{v2 => }/promise/websearch.ts | 0 packages/plugin/src/{v2 => }/tui/context.ts | 8 +- packages/plugin/src/{v2 => }/tui/index.ts | 0 packages/plugin/src/{v2 => }/tui/plugin.ts | 0 packages/plugin/src/{ => v1}/index.ts | 0 packages/plugin/src/{ => v1}/shell.ts | 0 packages/plugin/src/{ => v1}/tool.ts | 0 packages/plugin/src/{ => v1}/tui.ts | 0 packages/plugin/src/v2/effect/agent.ts | 20 - packages/plugin/src/v2/effect/filesystem.ts | 17 - .../plugin/src/v2/effect/internal/tool.ts | 315 --- packages/plugin/src/v2/effect/location.ts | 6 - packages/plugin/src/v2/effect/npm.ts | 11 - packages/plugin/src/v2/effect/path.ts | 8 - packages/plugin/src/v2/effect/tool.ts | 2 - packages/plugin/src/v2/promise/agent.ts | 13 - packages/plugin/src/v2/promise/catalog.ts | 15 - packages/plugin/src/v2/promise/command.ts | 10 - packages/plugin/src/v2/promise/integration.ts | 39 - .../plugin/src/v2/promise/internal/tool.ts | 64 - packages/plugin/src/v2/promise/reference.ts | 10 - packages/plugin/src/v2/promise/tool.ts | 2 - .../plugin/test/contract-identity.test.ts | 8 +- packages/protocol/openapi.json | 989 +++------- packages/protocol/src/errors.ts | 9 + packages/protocol/src/groups/agent.ts | 17 + packages/schema/package.json | 1 + packages/schema/src/event-manifest.ts | 14 +- packages/schema/src/llm.ts | 19 - packages/schema/src/permission.ts | 18 +- packages/schema/src/provider.ts | 12 +- packages/schema/src/question.ts | 22 +- packages/schema/src/session-event.ts | 7 +- packages/schema/src/session-message.ts | 10 +- packages/schema/src/tool.ts | 89 + packages/schema/src/v1/legacy-event.ts | 2 +- packages/schema/src/v1/permission.ts | 18 +- packages/schema/src/v1/question.ts | 20 +- packages/schema/src/v1/session.ts | 70 +- packages/schema/src/workspace-id.ts | 2 +- packages/schema/test/contract-hygiene.test.ts | 22 +- packages/schema/test/legacy-event.test.ts | 4 +- packages/schema/test/v1-isolation.test.ts | 18 +- packages/sdk-next/src/tool.ts | 5 +- .../sdk-next/test/contract-identity.test.ts | 20 +- packages/sdk-next/test/embedded.test.ts | 12 +- packages/server/src/event-feed.ts | 13 +- packages/server/src/handlers/agent.ts | 26 +- packages/server/src/handlers/command.ts | 4 +- packages/server/src/handlers/event.ts | 5 +- packages/server/src/handlers/message.ts | 4 +- packages/server/src/handlers/permission.ts | 16 +- packages/server/src/handlers/plugin.ts | 4 +- packages/server/src/handlers/question.ts | 20 +- packages/server/src/handlers/session.ts | 4 +- packages/server/src/handlers/skill.ts | 4 +- packages/server/src/location.ts | 4 +- .../server/src/middleware/form-location.ts | 8 +- .../server/src/middleware/session-location.ts | 8 +- packages/server/src/routes.ts | 14 +- packages/server/test/event-feed.test.ts | 31 +- .../src/backend/simulated-provider.ts | 38 +- .../test/simulated-provider.test.ts | 60 +- packages/tui/src/attention.ts | 2 +- packages/tui/src/context/data.tsx | 14 +- packages/tui/src/context/keymap.tsx | 4 +- packages/tui/src/feature-plugins/builtins.ts | 2 +- .../tui/src/feature-plugins/home/footer.tsx | 2 +- .../src/feature-plugins/sidebar/context.tsx | 2 +- .../src/feature-plugins/sidebar/footer.tsx | 2 +- .../tui/src/feature-plugins/sidebar/lsp.tsx | 2 +- .../tui/src/feature-plugins/sidebar/mcp.tsx | 2 +- .../feature-plugins/system/diff-viewer.tsx | 4 +- .../feature-plugins/system/notifications.ts | 2 +- .../src/feature-plugins/system/plugins.tsx | 2 +- .../tui/src/feature-plugins/system/scrap.tsx | 2 +- .../src/feature-plugins/system/which-key.tsx | 2 +- packages/tui/src/mini/stream-v2.subagent.ts | 12 +- packages/tui/src/mini/stream-v2.transport.ts | 10 +- packages/tui/src/mini/theme.ts | 2 +- packages/tui/src/mini/types.ts | 4 +- packages/tui/src/plugin/api.ts | 2 +- packages/tui/src/plugin/command-shim.ts | 2 +- packages/tui/src/plugin/context.tsx | 4 +- packages/tui/src/plugin/runtime.tsx | 2 +- packages/tui/src/plugin/slots.tsx | 2 +- .../tui/src/routes/session/permission.tsx | 4 +- .../test/cli/cmd/tui/notifications.test.ts | 8 +- packages/tui/test/cli/tui/data.test.tsx | 17 +- .../tui/test/cli/tui/diff-viewer.test.tsx | 2 +- packages/tui/test/fixture/tui-plugin.ts | 2 +- .../tui/test/mini/stream-v2.transport.test.ts | 10 +- packages/util/src/fs-util.ts | 2 +- packages/www/content/docs/build/plugins.mdx | 33 +- packages/www/openapi.json | 730 ++----- packages/www/public/openapi.json | 730 ++----- ...a.98.patch => effect@4.0.0-beta.101.patch} | 0 466 files changed, 9405 insertions(+), 11071 deletions(-) rename packages/core/src/{event.ts => bus.ts} (88%) delete mode 100644 packages/core/src/codemode.ts rename packages/core/src/{tool/execute.ts => codemode/tool.ts} (81%) delete mode 100644 packages/core/src/tool-output-store.ts create mode 100644 packages/core/src/tool.ts delete mode 100644 packages/core/src/tool/hooks.ts rename packages/core/src/tool/{ => plugin}/edit.ts (92%) rename packages/core/src/tool/{ => plugin}/glob.ts (92%) rename packages/core/src/tool/{ => plugin}/grep.ts (93%) rename packages/core/src/tool/{ => plugin}/patch.ts (98%) rename packages/core/src/tool/{ => plugin}/question.ts (86%) rename packages/core/src/tool/{ => plugin}/read.ts (89%) rename packages/core/src/tool/{ => plugin}/shell.ts (94%) rename packages/core/src/tool/{ => plugin}/skill.ts (87%) rename packages/core/src/tool/{ => plugin}/subagent.ts (95%) rename packages/core/src/tool/{ => plugin}/webfetch.ts (96%) rename packages/core/src/tool/{ => plugin}/websearch.ts (94%) rename packages/core/src/tool/{ => plugin}/write.ts (82%) delete mode 100644 packages/core/src/tool/registry.ts create mode 100644 packages/core/src/tool/runtime.ts delete mode 100644 packages/core/src/tool/tool.ts delete mode 100644 packages/core/src/tool/tools.ts delete mode 100644 packages/core/src/v2-schema.ts rename packages/core/test/{event.test.ts => bus.test.ts} (64%) delete mode 100644 packages/core/test/tool-output-store.test.ts rename packages/{plugin/test/tool.test.ts => core/test/tool-schema.test.ts} (68%) create mode 100644 packages/plugin/AGENTS.md rename packages/plugin/src/{v2/promise => }/README.md (91%) rename packages/plugin/src/{v2 => }/app.ts (100%) rename packages/plugin/src/{v2 => }/effect/PLAN.md (98%) rename packages/plugin/src/{v2 => }/effect/README.md (98%) create mode 100644 packages/plugin/src/effect/agent.ts rename packages/plugin/src/{v2 => }/effect/aisdk.ts (100%) rename packages/plugin/src/{v2 => }/effect/catalog.ts (51%) rename packages/plugin/src/{v2 => }/effect/command.ts (89%) rename packages/plugin/src/{v2 => }/effect/event.ts (100%) rename packages/plugin/src/{v2 => }/effect/index.ts (100%) rename packages/plugin/src/{v2 => }/effect/integration.ts (80%) rename packages/plugin/src/{v2 => }/effect/plugin.ts (100%) rename packages/plugin/src/{v2 => }/effect/reference.ts (95%) rename packages/plugin/src/{v2 => }/effect/registration.ts (100%) rename packages/plugin/src/{v2 => }/effect/session.ts (100%) rename packages/plugin/src/{v2 => }/effect/skill.ts (71%) create mode 100644 packages/plugin/src/effect/tool.ts rename packages/plugin/src/{v2 => }/effect/websearch.ts (100%) delete mode 100644 packages/plugin/src/example-workspace.ts delete mode 100644 packages/plugin/src/example.ts rename packages/plugin/src/{v2 => }/options.ts (100%) create mode 100644 packages/plugin/src/promise/agent.ts rename packages/plugin/src/{v2 => }/promise/aisdk.ts (100%) create mode 100644 packages/plugin/src/promise/catalog.ts create mode 100644 packages/plugin/src/promise/command.ts rename packages/plugin/src/{v2 => }/promise/event.ts (100%) rename packages/plugin/src/{v2 => }/promise/index.ts (100%) create mode 100644 packages/plugin/src/promise/integration.ts rename packages/plugin/src/{v2 => }/promise/plugin.ts (100%) create mode 100644 packages/plugin/src/promise/reference.ts rename packages/plugin/src/{v2 => }/promise/registration.ts (100%) rename packages/plugin/src/{v2 => }/promise/session.ts (100%) rename packages/plugin/src/{v2 => }/promise/skill.ts (61%) create mode 100644 packages/plugin/src/promise/tool.ts create mode 100644 packages/plugin/src/promise/types.ts rename packages/plugin/src/{v2 => }/promise/websearch.ts (100%) rename packages/plugin/src/{v2 => }/tui/context.ts (97%) rename packages/plugin/src/{v2 => }/tui/index.ts (100%) rename packages/plugin/src/{v2 => }/tui/plugin.ts (100%) rename packages/plugin/src/{ => v1}/index.ts (100%) rename packages/plugin/src/{ => v1}/shell.ts (100%) rename packages/plugin/src/{ => v1}/tool.ts (100%) rename packages/plugin/src/{ => v1}/tui.ts (100%) delete mode 100644 packages/plugin/src/v2/effect/agent.ts delete mode 100644 packages/plugin/src/v2/effect/filesystem.ts delete mode 100644 packages/plugin/src/v2/effect/internal/tool.ts delete mode 100644 packages/plugin/src/v2/effect/location.ts delete mode 100644 packages/plugin/src/v2/effect/npm.ts delete mode 100644 packages/plugin/src/v2/effect/path.ts delete mode 100644 packages/plugin/src/v2/effect/tool.ts delete mode 100644 packages/plugin/src/v2/promise/agent.ts delete mode 100644 packages/plugin/src/v2/promise/catalog.ts delete mode 100644 packages/plugin/src/v2/promise/command.ts delete mode 100644 packages/plugin/src/v2/promise/integration.ts delete mode 100644 packages/plugin/src/v2/promise/internal/tool.ts delete mode 100644 packages/plugin/src/v2/promise/reference.ts delete mode 100644 packages/plugin/src/v2/promise/tool.ts create mode 100644 packages/schema/src/tool.ts rename patches/{effect@4.0.0-beta.98.patch => effect@4.0.0-beta.101.patch} (100%) diff --git a/bun.lock b/bun.lock index 6b08db9e5cf9..a5b8672ee44a 100644 --- a/bun.lock +++ b/bun.lock @@ -184,7 +184,7 @@ "effect": "catalog:", }, "peerDependencies": { - "effect": "4.0.0-beta.98", + "effect": "4.0.0-beta.101", }, "optionalPeers": [ "effect", @@ -376,6 +376,7 @@ "@openrouter/ai-sdk-provider": "2.9.0", "@parcel/watcher": "2.5.1", "@silvia-odwyer/photon-node": "0.3.4", + "@standard-schema/spec": "catalog:", "ai-gateway-provider": "3.1.2", "bun-pty": "0.4.8", "diff": "catalog:", @@ -546,7 +547,7 @@ "name": "@opencode-ai/http-recorder", "version": "1.18.4", "dependencies": { - "@effect/platform-node-shared": "4.0.0-beta.98", + "@effect/platform-node-shared": "4.0.0-beta.101", }, "devDependencies": { "@effect/platform-node": "catalog:", @@ -583,7 +584,7 @@ "@opencode-ai/client": "workspace:*", "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "1.18.5", - "@standard-schema/spec": "^1.1.0", + "@standard-schema/spec": "catalog:", "effect": "catalog:", "zod": "catalog:", }, @@ -626,6 +627,7 @@ "name": "@opencode-ai/schema", "version": "1.17.11", "dependencies": { + "@standard-schema/spec": "catalog:", "effect": "catalog:", }, "devDependencies": { @@ -1042,16 +1044,16 @@ ], "patchedDependencies": { "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", - "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", + "effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch", "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", + "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", - "effect@4.0.0-beta.98": "patches/effect@4.0.0-beta.98.patch", }, "overrides": { "@opentui/core": "catalog:", @@ -1059,13 +1061,14 @@ "@opentui/solid": "catalog:", "@types/bun": "catalog:", "@types/node": "catalog:", + "effect": "catalog:", }, "catalog": { "@cloudflare/workers-types": "4.20251008.0", "@corvu/drawer": "0.2.4", - "@effect/opentelemetry": "4.0.0-beta.98", - "@effect/platform-node": "4.0.0-beta.98", - "@effect/sql-sqlite-bun": "4.0.0-beta.98", + "@effect/opentelemetry": "4.0.0-beta.101", + "@effect/platform-node": "4.0.0-beta.101", + "@effect/sql-sqlite-bun": "4.0.0-beta.101", "@hono/standard-validator": "0.2.0", "@hono/zod-validator": "0.4.2", "@kobalte/core": "0.13.11", @@ -1085,6 +1088,7 @@ "@solidjs/meta": "0.29.4", "@solidjs/router": "0.15.4", "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020", + "@standard-schema/spec": "1.1.0", "@tailwindcss/vite": "4.1.11", "@tanstack/solid-virtual": "3.13.32", "@tsconfig/bun": "1.0.9", @@ -1101,7 +1105,7 @@ "dompurify": "3.3.1", "drizzle-kit": "1.0.0-rc.2", "drizzle-orm": "1.0.0-rc.2", - "effect": "4.0.0-beta.98", + "effect": "4.0.0-beta.101", "fuzzysort": "3.1.0", "get-east-asian-width": "1.6.0", "hono": "4.10.7", @@ -1545,13 +1549,13 @@ "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], - "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.98", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.98" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-ITfK8xhcl+9GXOvPwzADWkOQ+dgUGZrJNefT3r2+uLFmzjyKRLtHzhLOl6lZaLSsf5io13+nmt8adfMRQPq+oA=="], + "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.101", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.101" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-IdejlqRLbjRHJgVnea4s8CxTWfvkSjM0HlnpNfP07IGTbhmAvPs7PMaWt1xzYWbOnI7CriQwxv+54eW1PwIkZg=="], - "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.98", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.98", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98", "ioredis": "^5.7.0" } }, "sha512-IQu1TiLXQEDSGkDBllyYjVadf+UqdjptryqX4mmktVTTbGDq7X4uVxe7cSgXuqZvyfG6kagTzwj2lfynxOaKQg=="], + "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.101", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.101", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-beta.101", "ioredis": "^5.7.0" } }, "sha512-pClk7dmMtHgM6Byu7CzGfrPvZ1/4BwmrRlCg2Op+iJozMkwVUxq8v4beK8b9SvxsliJjHZFznjvkVLX7LQjBqw=="], - "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.98", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98" } }, "sha512-iySXaffnCJX1sNAIp79ghhIeui9E5qwUQyqd1VLPkB9UNO4vdpd9B5fTEXwe7S/GusL4jsk9vSvX38XJgRFG1w=="], + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.101", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.101" } }, "sha512-g4L7XiyJSNJLJVhlslyg2zBCQsoKQf1y1gd+Yfd+3wD9ymC+m7ymbd/5FGqnT1aXV6E2AwRr4D/R1eyRUikvWQ=="], - "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.98", "", { "peerDependencies": { "effect": "^4.0.0-beta.98" } }, "sha512-cc41uLhYBqexdbTNu4dlui+31E8hcVLEapLySa0C8d60FmBY8IEAV/RD3oF+6pqPslKEZ9p1+XVLdDm0iflw5Q=="], + "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.101", "", { "peerDependencies": { "effect": "^4.0.0-beta.101" } }, "sha512-s6AC7LXCEjCN+nKegKFY4MOi6bmT1+SLR9YHEYwhY3P5qyQQB4R5yYLgt+3J4EPp5fa3t4FIDdPEUwz9LdKm6g=="], "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], @@ -2105,8 +2109,6 @@ "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="], - "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw=="], - "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], "@opentui/core": ["@opentui/core@0.4.5", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.5", "@opentui/core-darwin-x64": "0.4.5", "@opentui/core-linux-arm64": "0.4.5", "@opentui/core-linux-arm64-musl": "0.4.5", "@opentui/core-linux-x64": "0.4.5", "@opentui/core-linux-x64-musl": "0.4.5", "@opentui/core-win32-arm64": "0.4.5", "@opentui/core-win32-x64": "0.4.5" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-JsgRTPkA6e+Vxmumxai6SElOSlRQkbzNKHlCfemlArRiLhfC1IZ9RXJo2QH4xSu+uBOWAM90uss73/pPlkdEig=="], @@ -3829,7 +3831,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "effect": ["effect@4.0.0-beta.98", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-oz+bsG5h+6RNrw4t5GMfQrk/xBS8ROoqkYsuvRhBr5O7mCOrpvH/hbw+QrDzvKIpX4HJClwm86F94c87W0sJxg=="], + "effect": ["effect@4.0.0-beta.101", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-HjowumlIo+orthn4jMlEJPuzIYPBV+uq/XiciHWhiedLsXQpWHdNJHO5d59BVDP5s1LPuvERcktwFqRXnJqnhA=="], "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], diff --git a/package.json b/package.json index 3ff72db18e8c..de344eac2781 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,9 @@ "packages/slack" ], "catalog": { - "@effect/opentelemetry": "4.0.0-beta.98", - "@effect/platform-node": "4.0.0-beta.98", - "@effect/sql-sqlite-bun": "4.0.0-beta.98", + "@effect/opentelemetry": "4.0.0-beta.101", + "@effect/platform-node": "4.0.0-beta.101", + "@effect/sql-sqlite-bun": "4.0.0-beta.101", "@npmcli/arborist": "9.4.0", "@types/bun": "1.3.13", "@types/cross-spawn": "6.0.6", @@ -50,6 +50,7 @@ "@opentui/solid": "0.4.5", "@tanstack/solid-virtual": "3.13.32", "@shikijs/stream": "4.2.0", + "@standard-schema/spec": "1.1.0", "ulid": "3.0.1", "@kobalte/core": "0.13.11", "@corvu/drawer": "0.2.4", @@ -68,7 +69,7 @@ "dompurify": "3.3.1", "drizzle-kit": "1.0.0-rc.2", "drizzle-orm": "1.0.0-rc.2", - "effect": "4.0.0-beta.98", + "effect": "4.0.0-beta.101", "ai": "6.0.168", "cross-spawn": "7.0.6", "hono": "4.10.7", @@ -151,7 +152,8 @@ "@opentui/keymap": "catalog:", "@opentui/solid": "catalog:", "@types/bun": "catalog:", - "@types/node": "catalog:" + "@types/node": "catalog:", + "effect": "catalog:" }, "patchedDependencies": { "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", @@ -165,7 +167,7 @@ "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", - "effect@4.0.0-beta.98": "patches/effect@4.0.0-beta.98.patch", + "effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch", "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch" } } diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index 316f193d92e6..6041500be64a 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -1,4 +1,5 @@ import { Effect, Schema } from "effect" +import { Tool } from "@opencode-ai/schema/tool" import { Route } from "../route/client" import { Auth } from "../route/auth" import { Endpoint } from "../route/endpoint" @@ -19,7 +20,6 @@ import { type ProviderMetadata, type ToolCallPart, type ToolDefinition, - type ToolContent, type ToolResultPart, } from "../schema" import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" @@ -425,7 +425,7 @@ const lowerMedia = Effect.fn("AnthropicMessages.lowerMedia")(function* (part: Me // Tool results may carry structured text, images, and documents. Keep media as provider-native // content instead of JSON-stringifying base64 into a prompt string. const lowerToolResultContentItem = Effect.fn("AnthropicMessages.lowerToolResultContentItem")(function* ( - item: ToolContent, + item: Tool.Content, ) { if (item.type === "text") return { type: "text" as const, text: item.text } satisfies AnthropicTextBlock return yield* lowerMedia({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name }) @@ -436,7 +436,7 @@ const lowerToolResultContent = Effect.fn("AnthropicMessages.lowerToolResultConte // with existing cassettes and provider expectations. if (part.result.type !== "content") return ProviderShared.toolResultText(part) // Preserve the narrowed array element type when compiled through a consumer package. - const content: ReadonlyArray = part.result.value + const content: ReadonlyArray = part.result.value return yield* Effect.forEach(content, lowerToolResultContentItem) }) diff --git a/packages/ai/src/protocols/gemini.ts b/packages/ai/src/protocols/gemini.ts index 8a62d50fb893..03e3c00353c2 100644 --- a/packages/ai/src/protocols/gemini.ts +++ b/packages/ai/src/protocols/gemini.ts @@ -1,4 +1,5 @@ import { Effect, Schema } from "effect" +import { Tool } from "@opencode-ai/schema/tool" import { Route } from "../route/client" import { Auth } from "../route/auth" import { Endpoint } from "../route/endpoint" @@ -16,7 +17,6 @@ import { type TextPart, type ToolCallPart, type ToolDefinition, - type ToolContent, } from "../schema" import { JsonObject, optionalArray, ProviderShared } from "./shared" import { GeminiToolSchema } from "./utils/gemini-tool-schema" @@ -289,7 +289,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR }) continue } - const content: ReadonlyArray = part.result.value + const content: ReadonlyArray = part.result.value const text = content.filter((item) => item.type === "text").map((item) => item.text) const media: GeminiInlineDataPart[] = [] for (const item of content) { diff --git a/packages/ai/src/protocols/open-responses.ts b/packages/ai/src/protocols/open-responses.ts index 3372145087c0..97ad08461412 100644 --- a/packages/ai/src/protocols/open-responses.ts +++ b/packages/ai/src/protocols/open-responses.ts @@ -1,4 +1,5 @@ import { Effect, Schema } from "effect" +import type { Content } from "@opencode-ai/schema/tool" import { HttpTransport } from "../route/transport" import { Protocol } from "../route/protocol" import { @@ -14,7 +15,6 @@ import { type TextPart, type ToolCallPart, type ToolDefinition, - type ToolContent, type ToolResultPart, } from "../schema" import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" @@ -371,7 +371,7 @@ const lowerUserContent = Effect.fn("OpenResponses.lowerUserContent")(function* ( // Tool results may carry structured text, images, and files. Keep media as provider-native // content instead of JSON-stringifying base64 into a prompt string. const lowerToolResultContentItem = Effect.fn("OpenResponses.lowerToolResultContentItem")(function* ( - item: ToolContent, + item: Content, request: LLMRequest, extension: Extension, ) { @@ -392,7 +392,7 @@ const lowerToolResultOutput = Effect.fn("OpenResponses.lowerToolResultOutput")(f // compatibility with existing cassettes and provider expectations. if (part.result.type !== "content") return ProviderShared.toolResultText(part) // Preserve the narrowed array element type when compiled through a consumer package. - const content: ReadonlyArray = part.result.value + const content: ReadonlyArray = part.result.value return yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, extension)) }) @@ -496,7 +496,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques if (store !== false && itemID && !hostedToolReferences.has(itemID)) input.push({ type: "item_reference", id: itemID }) if (store === false && part.result.type === "content") { - const content: ReadonlyArray = part.result.value + const content: ReadonlyArray = part.result.value input.push({ role: "user", content: yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, extension)), diff --git a/packages/ai/src/protocols/openai-chat.ts b/packages/ai/src/protocols/openai-chat.ts index e130f0bdfe4d..a2d8a7b54174 100644 --- a/packages/ai/src/protocols/openai-chat.ts +++ b/packages/ai/src/protocols/openai-chat.ts @@ -1,4 +1,5 @@ import { Effect, Schema } from "effect" +import { Tool } from "@opencode-ai/schema/tool" import { Route } from "../route/client" import { Auth } from "../route/auth" import { Endpoint } from "../route/endpoint" @@ -17,7 +18,6 @@ import { type TextPart, type ToolCallPart, type ToolDefinition, - type ToolContent, } from "../schema" import { classifyProviderFailure } from "../provider-error" import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" @@ -335,7 +335,7 @@ const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (m messages.push({ role: "tool", tool_call_id: part.id, content: ProviderShared.toolResultText(part) }) continue } - const content: ReadonlyArray = part.result.value + const content: ReadonlyArray = part.result.value const text = content.filter((item) => item.type === "text").map((item) => item.text) messages.push({ role: "tool", tool_call_id: part.id, content: text.join("\n") }) const files = content.filter((item) => item.type === "file") diff --git a/packages/ai/src/protocols/shared.ts b/packages/ai/src/protocols/shared.ts index 478088b08177..9d8f98a1ad21 100644 --- a/packages/ai/src/protocols/shared.ts +++ b/packages/ai/src/protocols/shared.ts @@ -1,4 +1,5 @@ import { Buffer } from "node:buffer" +import { Tool } from "@opencode-ai/schema/tool" import { Effect, Schema, Stream } from "effect" import * as Sse from "effect/unstable/encoding/Sse" import { Headers, HttpClientRequest } from "effect/unstable/http" @@ -9,7 +10,6 @@ import { type ContentPart, type LLMRequest, type MediaPart, - type ToolFileContent, type TextPart, type ToolResultPart, } from "../schema" @@ -206,7 +206,7 @@ export const validateMedia = Effect.fn("ProviderShared.validateMedia")(function* return { mime, base64, dataUrl: `data:${mime};base64,${base64}`, bytes } satisfies ValidatedMedia }) -export const validateToolFile = (route: string, part: ToolFileContent, supportedMimes: ReadonlySet) => +export const validateToolFile = (route: string, part: Tool.FileContent, supportedMimes: ReadonlySet) => validateMedia(route, { type: "media", mediaType: part.mime, data: part.uri, filename: part.name }, supportedMimes) export const trimBaseUrl = (value: string) => value.replace(/\/+$/, "") diff --git a/packages/ai/src/schema/errors.ts b/packages/ai/src/schema/errors.ts index 82acb7cb788d..7b4cb96425b5 100644 --- a/packages/ai/src/schema/errors.ts +++ b/packages/ai/src/schema/errors.ts @@ -1,4 +1,5 @@ import { Schema } from "effect" +import { Tool } from "@opencode-ai/schema/tool" import { ModelID, ProviderID, ProviderMetadata, RouteID } from "./ids" export const ProviderFailureClassification = Schema.Literal("context-overflow") @@ -152,8 +153,4 @@ export class LLMError extends Schema.TaggedErrorClass()("LLM.Error", { * Anything thrown or yielded by a handler that is not a `ToolFailure` is * treated as a defect and fails the stream. */ -export class ToolFailure extends Schema.TaggedErrorClass()("LLM.ToolFailure", { - message: Schema.String, - error: Schema.optional(Schema.Defect()), - metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), -}) {} +export class ToolFailure extends Tool.Error {} diff --git a/packages/ai/src/schema/messages.ts b/packages/ai/src/schema/messages.ts index e6617ddc9ed8..bf859f946cf1 100644 --- a/packages/ai/src/schema/messages.ts +++ b/packages/ai/src/schema/messages.ts @@ -1,5 +1,5 @@ import { Schema } from "effect" -import { ToolContent, ToolFileContent, ToolTextContent } from "@opencode-ai/schema/llm" +import { Tool } from "@opencode-ai/schema/tool" import { JsonSchema, MessageRole, ProviderMetadata } from "./ids" import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, ModelSchema, ProviderOptions } from "./options" import { isRecord } from "../utils/record" @@ -40,8 +40,6 @@ export const MediaPart = Schema.Struct({ }).annotate({ identifier: "LLM.Content.Media" }) export type MediaPart = Schema.Schema.Type -export { ToolContent, ToolFileContent, ToolTextContent } - const isToolResultValue = (value: unknown): value is ToolResultValue => isRecord(value) && (value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") && @@ -63,7 +61,7 @@ export const ToolResultValue = Object.assign( }), Schema.Struct({ type: Schema.Literal("content"), - value: Schema.Array(ToolContent), + value: Schema.Array(Tool.Content), }), ]).annotate({ identifier: "LLM.ToolResult" }), { @@ -79,16 +77,16 @@ export type ToolResultValue = Schema.Schema.Type export interface ToolOutput { readonly structured: unknown - readonly content: ReadonlyArray + readonly content: ReadonlyArray } export const ToolOutput = Object.assign( Schema.Struct({ structured: Schema.Unknown, - content: Schema.Array(ToolContent), + content: Schema.Array(Tool.Content), }).annotate({ identifier: "LLM.ToolOutput" }), { - make: (structured: unknown, content: ReadonlyArray = []): ToolOutput => ({ structured, content }), + make: (structured: unknown, content: ReadonlyArray = []): ToolOutput => ({ structured, content }), fromResultValue: (result: ToolResultValue): ToolOutput | undefined => { switch (result.type) { case "json": diff --git a/packages/ai/src/tool-runtime.ts b/packages/ai/src/tool-runtime.ts index c483950c1252..71958977cb78 100644 --- a/packages/ai/src/tool-runtime.ts +++ b/packages/ai/src/tool-runtime.ts @@ -28,7 +28,7 @@ export const dispatch = (tools: Tools, call: ToolCallPart): Effect.Effect result(call, value)), - Effect.catchTag("LLM.ToolFailure", (failure) => + Effect.catchTag("Tool.Error", (failure) => Effect.succeed(result(call, { type: "error", value: failure.message }, failure.error)), ), ) diff --git a/packages/ai/src/tool.ts b/packages/ai/src/tool.ts index 62bd0df82f11..25df2be2878e 100644 --- a/packages/ai/src/tool.ts +++ b/packages/ai/src/tool.ts @@ -1,7 +1,7 @@ import { Effect, JsonSchema, Schema } from "effect" +import { Tool } from "@opencode-ai/schema/tool" import type { ToolCallPart, - ToolContent, ToolDefinition as ToolDefinitionClass, ToolOutput as ToolOutputType, } from "./schema" @@ -31,7 +31,7 @@ export interface ToolModelOutputInput { export type ToolToModelOutput, Success extends ToolSchema> = ( input: ToolModelOutputInput, Success["Encoded"]>, -) => ReadonlyArray +) => ReadonlyArray /** * A type-safe LLM tool. Each tool bundles its own description, parameter @@ -95,7 +95,7 @@ type DynamicToolConfig = { readonly jsonSchema: JsonSchema.JsonSchema readonly outputSchema?: JsonSchema.JsonSchema readonly execute?: (params: unknown, context?: ToolExecuteContext) => Effect.Effect - readonly toModelOutput?: (input: ToolModelOutputInput) => ReadonlyArray + readonly toModelOutput?: (input: ToolModelOutputInput) => ReadonlyArray readonly toStructuredOutput?: (output: unknown) => unknown } @@ -151,7 +151,7 @@ export function make(config: { readonly jsonSchema: JsonSchema.JsonSchema readonly outputSchema?: JsonSchema.JsonSchema readonly execute: (params: unknown, context?: ToolExecuteContext) => Effect.Effect - readonly toModelOutput?: (input: ToolModelOutputInput) => ReadonlyArray + readonly toModelOutput?: (input: ToolModelOutputInput) => ReadonlyArray readonly toStructuredOutput?: (output: unknown) => unknown }): AnyExecutableTool export function make(config: { @@ -159,7 +159,7 @@ export function make(config: { readonly jsonSchema: JsonSchema.JsonSchema readonly outputSchema?: JsonSchema.JsonSchema readonly execute?: undefined - readonly toModelOutput?: (input: ToolModelOutputInput) => ReadonlyArray + readonly toModelOutput?: (input: ToolModelOutputInput) => ReadonlyArray readonly toStructuredOutput?: (output: unknown) => unknown }): AnyTool export function make(config: TypedToolConfig | DynamicToolConfig): AnyTool { @@ -236,7 +236,7 @@ const toJsonSchema = (schema: Schema.Top): JsonSchema.JsonSchema => { } const project = ( - toModelOutput: ((input: ToolModelOutputInput) => ReadonlyArray) | undefined, + toModelOutput: ((input: ToolModelOutputInput) => ReadonlyArray) | undefined, toStructuredOutput: ((output: unknown) => unknown) | undefined, parameters: unknown, callID: ToolCallPart["id"], diff --git a/packages/ai/test/tool-runtime.test.ts b/packages/ai/test/tool-runtime.test.ts index 356ee779c8b9..227f07fc245f 100644 --- a/packages/ai/test/tool-runtime.test.ts +++ b/packages/ai/test/tool-runtime.test.ts @@ -1,4 +1,5 @@ import { describe, expect } from "bun:test" +import { Content } from "@opencode-ai/schema/tool" import { Effect, Schema, Stream } from "effect" import { GenerationOptions, @@ -7,7 +8,6 @@ import { LLMRequest, LLMResponse, ToolChoice, - ToolContent, ToolOutput, toDefinitions, } from "../src" @@ -279,7 +279,7 @@ describe("LLMClient tools", () => { it.effect("models canonical tool files with URIs", () => Effect.sync(() => { - const decode = Schema.decodeUnknownSync(ToolContent) + const decode = Schema.decodeUnknownSync(Content) expect(decode({ type: "file", uri: "data:image/png;base64,AAAA", mime: "image/png" })).toEqual({ type: "file", diff --git a/packages/app/src/context/server-sdk.test.ts b/packages/app/src/context/server-sdk.test.ts index 767d16ffa05a..4144df859ee3 100644 --- a/packages/app/src/context/server-sdk.test.ts +++ b/packages/app/src/context/server-sdk.test.ts @@ -25,7 +25,7 @@ describe("adaptServerEvent", () => { } as OpenCodeEvent expect(adaptServerEvent(current)).toMatchObject({ - type: "permission.asked", + type: "permission.v2.asked", properties: { id: "perm_1", sessionID: "ses_1", permission: "read", patterns: ["src/**"] }, current, }) diff --git a/packages/app/src/context/server-sdk.tsx b/packages/app/src/context/server-sdk.tsx index 62c585779487..4606363b7eda 100644 --- a/packages/app/src/context/server-sdk.tsx +++ b/packages/app/src/context/server-sdk.tsx @@ -29,7 +29,7 @@ export function adaptServerEvent(event: OpenCodeEvent): ServerEvent { if (event.type === "permission.v2.asked") { return { id: event.id, - type: "permission.asked", + type: "permission.v2.asked", properties: { id: event.data.id, sessionID: event.data.sessionID, @@ -46,13 +46,13 @@ export function adaptServerEvent(event: OpenCodeEvent): ServerEvent { } as ServerEvent } if (event.type === "permission.v2.replied") - return { id: event.id, type: "permission.replied", properties: event.data, current: event } as ServerEvent + return { id: event.id, type: "permission.v2.replied", properties: event.data, current: event } as ServerEvent if (event.type === "question.v2.asked") - return { id: event.id, type: "question.asked", properties: event.data, current: event } as ServerEvent + return { id: event.id, type: "question.v2.asked", properties: event.data, current: event } as ServerEvent if (event.type === "question.v2.replied") - return { id: event.id, type: "question.replied", properties: event.data, current: event } as ServerEvent + return { id: event.id, type: "question.v2.replied", properties: event.data, current: event } as ServerEvent if (event.type === "question.v2.rejected") - return { id: event.id, type: "question.rejected", properties: event.data, current: event } as ServerEvent + return { id: event.id, type: "question.v2.rejected", properties: event.data, current: event } as ServerEvent return { id: event.id, type: event.type, properties: event.data, current: event } as ServerEvent } diff --git a/packages/cli/src/acp/event.ts b/packages/cli/src/acp/event.ts index 187b660141ca..380da2e22e8d 100644 --- a/packages/cli/src/acp/event.ts +++ b/packages/cli/src/acp/event.ts @@ -71,7 +71,7 @@ export async function streamTurn(input: { const next = await stream.next() if (next.done) throw new Error("event stream disconnected during prompt execution") const event = next.value - if (event.type === "permission.v2.asked" && event.data.sessionID === input.sessionID) { + if (event.type === "permission.asked" && event.data.sessionID === input.sessionID) { const tool = event.data.source?.callID ? tools.get(event.data.source.callID) : undefined await replyPermission({ client: input.client, diff --git a/packages/cli/src/acp/permission.ts b/packages/cli/src/acp/permission.ts index 87f6e68d06b1..948ad0eeace6 100644 --- a/packages/cli/src/acp/permission.ts +++ b/packages/cli/src/acp/permission.ts @@ -5,7 +5,7 @@ import { Result } from "effect" import { isAbsolute, resolve } from "node:path" import { pendingToolCall, stringValue, toLocations, toToolKind, type ToolInput } from "./tool" -type PermissionEvent = Extract +type PermissionEvent = Extract type Connection = Pick & Partial> type Tool = { readonly name: string; readonly input: ToolInput } diff --git a/packages/cli/src/node/plugin-runtime.effect.ts b/packages/cli/src/node/plugin-runtime.effect.ts index 5e38b00d4c37..d17fab342fd1 100644 --- a/packages/cli/src/node/plugin-runtime.effect.ts +++ b/packages/cli/src/node/plugin-runtime.effect.ts @@ -9,8 +9,8 @@ import { Provider, Reference, Skill, -} from "@opencode-ai/plugin/v2/effect" -import { Tool } from "@opencode-ai/plugin/v2/effect/tool" +} from "@opencode-ai/plugin/effect" +import { Tool } from "@opencode-ai/schema/tool" const key = Symbol.for("opencode.plugin.v2.effect") ;(globalThis as typeof globalThis & { [key]?: unknown })[key] = { @@ -24,5 +24,5 @@ const key = Symbol.for("opencode.plugin.v2.effect") Provider, Reference, Skill, - Tool, + Tool: { Error: Tool.Error }, } diff --git a/packages/cli/src/node/plugin-runtime.promise.ts b/packages/cli/src/node/plugin-runtime.promise.ts index 49c46512328d..fa33d6edae70 100644 --- a/packages/cli/src/node/plugin-runtime.promise.ts +++ b/packages/cli/src/node/plugin-runtime.promise.ts @@ -9,8 +9,7 @@ import { Provider, Reference, Skill, -} from "@opencode-ai/plugin/v2" -import { Tool } from "@opencode-ai/plugin/v2/tool" +} from "@opencode-ai/plugin" const key = Symbol.for("opencode.plugin.v2.promise") ;(globalThis as typeof globalThis & { [key]?: unknown })[key] = { @@ -24,5 +23,4 @@ const key = Symbol.for("opencode.plugin.v2.promise") Provider, Reference, Skill, - Tool, } diff --git a/packages/cli/src/run/noninteractive.ts b/packages/cli/src/run/noninteractive.ts index 824d228c6a52..bf568d2df546 100644 --- a/packages/cli/src/run/noninteractive.ts +++ b/packages/cli/src/run/noninteractive.ts @@ -177,7 +177,7 @@ export async function runNonInteractivePrompt(input: Input) { } const event = next.value - if (event.type === "permission.v2.asked" && submitted && event.data.sessionID === input.sessionID) { + if (event.type === "permission.asked" && submitted && event.data.sessionID === input.sessionID) { await replyPermission(event.data) continue } diff --git a/packages/cli/src/services/standalone.ts b/packages/cli/src/services/standalone.ts index 3a98a578e418..40b1e73a0541 100644 --- a/packages/cli/src/services/standalone.ts +++ b/packages/cli/src/services/standalone.ts @@ -13,11 +13,13 @@ type Options = { readonly command?: ReadonlyArray } +const startupDirectory = process.cwd() + function command(password: string, options: Options) { const [executable, ...args] = options.command ?? [...selfCommand(), "serve"] if (!executable) throw new Error("Failed to resolve standalone server command") return ChildProcess.make(executable, [...args, "--stdio", "--port", "0"], { - cwd: process.cwd(), + cwd: startupDirectory, // Explicit entry wins over anything inherited, so a user-exported // OPENCODE_PASSWORD cannot shadow the child's lease credential. env: { OPENCODE_PASSWORD: password }, diff --git a/packages/cli/src/util/process.ts b/packages/cli/src/util/process.ts index a0c567aad530..4721a12ffa4d 100644 --- a/packages/cli/src/util/process.ts +++ b/packages/cli/src/util/process.ts @@ -1,11 +1,13 @@ import path from "node:path" +const entrypoint = process.argv[1] ? path.resolve(process.argv[1]) : undefined + export function selfCommand() { const runtime = path.basename(process.execPath, path.extname(process.execPath)).toLowerCase() if (runtime !== "bun" && runtime !== "node" && runtime !== "nodejs") return [process.execPath] - if (!process.argv[1]) throw new Error("Failed to resolve CLI entrypoint") - if (runtime === "node" || runtime === "nodejs") return [process.execPath, ...nodeFlags(), process.argv[1]] - return [process.execPath, process.argv[1]] + if (!entrypoint) throw new Error("Failed to resolve CLI entrypoint") + if (runtime === "node" || runtime === "nodejs") return [process.execPath, ...nodeFlags(), entrypoint] + return [process.execPath, entrypoint] } function nodeFlags() { diff --git a/packages/cli/test/acp/permission-behavior.test.ts b/packages/cli/test/acp/permission-behavior.test.ts index df7de586fddc..9418daa4b1ad 100644 --- a/packages/cli/test/acp/permission-behavior.test.ts +++ b/packages/cli/test/acp/permission-behavior.test.ts @@ -502,7 +502,7 @@ function permissionAsked( readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } } = {}, ) { - return ephemeralEvent("permission.v2.asked", { + return ephemeralEvent("permission.asked", { id, sessionID, action: input.action ?? "shell", diff --git a/packages/cli/test/fixture/standalone-owner.ts b/packages/cli/test/fixture/standalone-owner.ts index 62e1b7488731..d34385a10e4f 100644 --- a/packages/cli/test/fixture/standalone-owner.ts +++ b/packages/cli/test/fixture/standalone-owner.ts @@ -3,12 +3,14 @@ import { Service } from "@opencode-ai/client/effect/service" import path from "node:path" import { Standalone } from "../../src/services/standalone" -process.argv[1] = path.join(import.meta.dir, "../../src/index.ts") +process.chdir(path.join(import.meta.dir, "../../../..")) await Effect.runPromise( Effect.scoped( Effect.gen(function* () { - const endpoint = yield* Standalone.start() + const endpoint = yield* Standalone.start({ + command: [process.execPath, path.join(import.meta.dir, "../../src/index.ts"), "serve"], + }) const response = yield* Effect.promise(() => fetch(new URL("/api/health", endpoint.url), { headers: Service.headers(endpoint) }), ) diff --git a/packages/cli/vite.node.config.ts b/packages/cli/vite.node.config.ts index 15bcc8490a3f..52f00a2b5723 100644 --- a/packages/cli/vite.node.config.ts +++ b/packages/cli/vite.node.config.ts @@ -75,20 +75,11 @@ export const define = sdk.Plugin.define` const effectPluginModule = promisePluginModule .replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect") .replace("Promise plugin", "Effect plugin") - const promiseToolModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.promise")] -if (!sdk) throw new Error("OpenCode Promise plugin SDK is unavailable") -export const Tool = sdk.Tool -export const make = sdk.Tool.make` + const promiseToolModule = `export {}` const effectToolModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.effect")] if (!sdk) throw new Error("OpenCode Effect plugin SDK is unavailable") -export const Tool = sdk.Tool -export const Failure = sdk.Tool.Failure -export const RegistrationError = sdk.Tool.RegistrationError -export const make = sdk.Tool.make -export const validateName = sdk.Tool.validateName -export const registrationEntries = sdk.Tool.registrationEntries -export const validateNamespace = sdk.Tool.validateNamespace -export const toLLMDefinition = sdk.Tool.toLLMDefinition` +export const Error = sdk.Tool.Error +` return `#!/usr/bin/env -S node ${nodeExecArgv.join(" ")} import __cjs_mod__ from "node:module" import { chmodSync as __ocChmod, existsSync as __ocExists, lstatSync as __ocLstat, mkdirSync as __ocMkdir, renameSync as __ocRename, rmSync as __ocRm, writeFileSync as __ocWrite } from "node:fs" @@ -100,17 +91,17 @@ const __filename = import.meta.filename const __dirname = import.meta.dirname const require = __cjs_mod__.createRequire(import.meta.url) const __ocPluginModules = ${JSON.stringify({ - "@opencode-ai/plugin/v2": "opencode:plugin-v2", - "@opencode-ai/plugin/v2/plugin": "opencode:plugin-v2-plugin", - "@opencode-ai/plugin/v2/tool": "opencode:plugin-v2-tool", - "@opencode-ai/plugin/v2/effect": "opencode:plugin-v2-effect", - "@opencode-ai/plugin/v2/effect/plugin": "opencode:plugin-v2-effect-plugin", - "@opencode-ai/plugin/v2/effect/tool": "opencode:plugin-v2-effect-tool", + "@opencode-ai/plugin": "opencode:plugin-v2", + "@opencode-ai/plugin/promise/plugin": "opencode:plugin-promise-plugin", + "@opencode-ai/plugin/promise/tool": "opencode:plugin-promise-tool", + "@opencode-ai/plugin/effect": "opencode:plugin-v2-effect", + "@opencode-ai/plugin/effect/plugin": "opencode:plugin-v2-effect-plugin", + "@opencode-ai/plugin/effect/tool": "opencode:plugin-v2-effect-tool", })} const __ocPluginSources = ${JSON.stringify({ "opencode:plugin-v2": promiseModule, - "opencode:plugin-v2-plugin": promisePluginModule, - "opencode:plugin-v2-tool": promiseToolModule, + "opencode:plugin-promise-plugin": promisePluginModule, + "opencode:plugin-promise-tool": promiseToolModule, "opencode:plugin-v2-effect": effectModule, "opencode:plugin-v2-effect-plugin": effectPluginModule, "opencode:plugin-v2-effect-tool": effectToolModule, diff --git a/packages/client/package.json b/packages/client/package.json index ffb6458d849c..fe379b54577c 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -36,7 +36,7 @@ "@opencode-ai/protocol": "workspace:*" }, "peerDependencies": { - "effect": "4.0.0-beta.98" + "effect": "4.0.0-beta.101" }, "peerDependenciesMeta": { "effect": { diff --git a/packages/client/script/build.ts b/packages/client/script/build.ts index 8e432b0a17cc..6f0dbaad99ff 100644 --- a/packages/client/script/build.ts +++ b/packages/client/script/build.ts @@ -6,11 +6,86 @@ import { groupNames, promiseOmitEndpoints, } from "@opencode-ai/protocol/client" -import { Effect } from "effect" +import { Agent } from "@opencode-ai/schema/agent" +import { Command } from "@opencode-ai/schema/command" +import { Credential } from "@opencode-ai/schema/credential" +import { Event } from "@opencode-ai/schema/event" +import { EventLog } from "@opencode-ai/schema/event-log" +import { FileDiff } from "@opencode-ai/schema/file-diff" +import { FileSystem } from "@opencode-ai/schema/filesystem" +import { Form } from "@opencode-ai/schema/form" +import { InstructionEntry } from "@opencode-ai/schema/instruction-entry" +import { Integration } from "@opencode-ai/schema/integration" +import { Location } from "@opencode-ai/schema/location" +import { Mcp } from "@opencode-ai/schema/mcp" +import { Model } from "@opencode-ai/schema/model" +import { Permission } from "@opencode-ai/schema/permission" +import { PermissionSaved } from "@opencode-ai/schema/permission-saved" +import { Plugin } from "@opencode-ai/schema/plugin" +import { Project } from "@opencode-ai/schema/project" +import { ProjectCopy } from "@opencode-ai/schema/project-copy" +import { AgentAttachment, FileAttachment, Prompt, PromptMention } from "@opencode-ai/schema/prompt" +import { PromptInput } from "@opencode-ai/schema/prompt-input" +import { Provider } from "@opencode-ai/schema/provider" +import { Pty } from "@opencode-ai/schema/pty" +import { PtyTicket } from "@opencode-ai/schema/pty-ticket" +import { Question } from "@opencode-ai/schema/question" +import { Reference } from "@opencode-ai/schema/reference" +import { AbsolutePath, PositiveInt, RelativePath } from "@opencode-ai/schema/schema" +import { Session } from "@opencode-ai/schema/session" +import { SessionMessage } from "@opencode-ai/schema/session-message" +import { SessionPending } from "@opencode-ai/schema/session-pending" +import { Shell } from "@opencode-ai/schema/shell" +import { Skill } from "@opencode-ai/schema/skill" +import { Vcs } from "@opencode-ai/schema/vcs" +import { WebSearch } from "@opencode-ai/schema/websearch" +import { Workspace } from "@opencode-ai/schema/workspace" +import { Effect, Schema } from "effect" import { fileURLToPath } from "url" const promiseContract = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints }) const effectContract = compile(ClientApi, { groupNames, omitEndpoints: effectOmitEndpoints }) +const effectTypeReferences = [ + ...namespaceTypes("Agent", "@opencode-ai/schema/agent", Agent), + ...namespaceTypes("Command", "@opencode-ai/schema/command", Command), + ...namespaceTypes("Credential", "@opencode-ai/schema/credential", Credential), + ...namespaceTypes("Event", "@opencode-ai/schema/event", Event), + ...namespaceTypes("EventLog", "@opencode-ai/schema/event-log", EventLog), + ...namespaceTypes("FileDiff", "@opencode-ai/schema/file-diff", FileDiff), + ...namespaceTypes("FileSystem", "@opencode-ai/schema/filesystem", FileSystem), + ...namespaceTypes("Form", "@opencode-ai/schema/form", Form), + ...namespaceTypes("InstructionEntry", "@opencode-ai/schema/instruction-entry", InstructionEntry), + ...namespaceTypes("Integration", "@opencode-ai/schema/integration", Integration), + ...namespaceTypes("Location", "@opencode-ai/schema/location", Location), + ...namespaceTypes("Mcp", "@opencode-ai/schema/mcp", Mcp), + ...namespaceTypes("Model", "@opencode-ai/schema/model", Model), + ...namespaceTypes("Permission", "@opencode-ai/schema/permission", Permission), + ...namespaceTypes("PermissionSaved", "@opencode-ai/schema/permission-saved", PermissionSaved), + ...namespaceTypes("Plugin", "@opencode-ai/schema/plugin", Plugin), + ...namespaceTypes("Project", "@opencode-ai/schema/project", Project), + ...namespaceTypes("ProjectCopy", "@opencode-ai/schema/project-copy", ProjectCopy), + ...namespaceTypes("PromptInput", "@opencode-ai/schema/prompt-input", PromptInput), + ...namespaceTypes("Provider", "@opencode-ai/schema/provider", Provider), + ...namespaceTypes("Pty", "@opencode-ai/schema/pty", Pty), + ...namespaceTypes("PtyTicket", "@opencode-ai/schema/pty-ticket", PtyTicket), + ...namespaceTypes("Question", "@opencode-ai/schema/question", Question), + ...namespaceTypes("Reference", "@opencode-ai/schema/reference", Reference), + ...namespaceTypes("Session", "@opencode-ai/schema/session", Session), + ...namespaceTypes("SessionMessage", "@opencode-ai/schema/session-message", SessionMessage), + ...namespaceTypes("SessionPending", "@opencode-ai/schema/session-pending", SessionPending), + ...namespaceTypes("Shell", "@opencode-ai/schema/shell", Shell), + ...namespaceTypes("Skill", "@opencode-ai/schema/skill", Skill), + ...namespaceTypes("Vcs", "@opencode-ai/schema/vcs", Vcs), + ...namespaceTypes("WebSearch", "@opencode-ai/schema/websearch", WebSearch), + ...namespaceTypes("Workspace", "@opencode-ai/schema/workspace", Workspace), + typeReference("Prompt", "@opencode-ai/schema/prompt", Prompt), + typeReference("PromptMention", "@opencode-ai/schema/prompt", PromptMention), + typeReference("FileAttachment", "@opencode-ai/schema/prompt", FileAttachment), + typeReference("AgentAttachment", "@opencode-ai/schema/prompt", AgentAttachment), + typeReference("AbsolutePath", "@opencode-ai/schema/schema", AbsolutePath), + typeReference("PositiveInt", "@opencode-ai/schema/schema", PositiveInt), + typeReference("RelativePath", "@opencode-ai/schema/schema", RelativePath), +] await Effect.runPromise( Effect.all( @@ -22,14 +97,40 @@ await Effect.runPromise( fileURLToPath(new URL("../src/promise/generated", import.meta.url)), ), write( - emitEffectImported(effectContract, { module: "../../contract", api: "ClientApi" }), + emitEffectImported(effectContract, { + module: "../../contract", + api: "ClientApi", + shapeModule: "../api/api.js", + }), fileURLToPath(new URL("../src/effect/generated", import.meta.url)), ), write( - emitEffectShape(effectContract, { module: "../../contract", api: "ClientApi" }), + emitEffectShape(effectContract, { + typeReferences: effectTypeReferences, + outputTypes: { + "event.subscribe": { + name: "OpenCodeEvent", + import: 'import type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event"', + }, + }, + }), fileURLToPath(new URL("../src/effect/api", import.meta.url)), ), ], { concurrency: 3, discard: true }, ).pipe(Effect.provide(NodeFileSystem.layer)), ) + +function namespaceTypes(namespace: string, module: string, values: object) { + return Object.entries(values).flatMap(([name, schema]) => + Schema.isSchema(schema) ? [typeReference(`${namespace}.${name}`, module, schema)] : [], + ) +} + +function typeReference(name: string, module: string, schema: Schema.Top) { + return { + schema, + name, + import: `import type { ${name.split(".")[0]} } from ${JSON.stringify(module)}`, + } +} diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index 3c229bc61519..3bb65cc0073c 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -1,18 +1,49 @@ // Generated by @opencode-ai/httpapi-codegen. Do not edit. import type { Effect, Stream } from "effect" -import type { HttpApiClient } from "effect/unstable/httpapi" -import type { ClientApi } from "../../contract" - -type RawClient = HttpApiClient.ForApi -type EffectValue = A extends Effect.Effect ? Success : never -type StreamValue = A extends Stream.Stream ? Success : never - -export type Endpoint0_0Output = EffectValue> +import type { Location } from "@opencode-ai/schema/location" +import type { Agent } from "@opencode-ai/schema/agent" +import type { Plugin } from "@opencode-ai/schema/plugin" +import type { Workspace } from "@opencode-ai/schema/workspace" +import type { Session } from "@opencode-ai/schema/session" +import type { AbsolutePath } from "@opencode-ai/schema/schema" +import type { Project } from "@opencode-ai/schema/project" +import type { RelativePath } from "@opencode-ai/schema/schema" +import type { Brand } from "effect" +import type { Model } from "@opencode-ai/schema/model" +import type { SessionMessage } from "@opencode-ai/schema/session-message" +import type { PromptInput } from "@opencode-ai/schema/prompt-input" +import type { AgentAttachment } from "@opencode-ai/schema/prompt" +import type { SessionPending } from "@opencode-ai/schema/session-pending" +import type { Skill } from "@opencode-ai/schema/skill" +import type { Event } from "@opencode-ai/schema/event" +import type { InstructionEntry } from "@opencode-ai/schema/instruction-entry" +import type { Schema } from "effect" +import type { EventLog } from "@opencode-ai/schema/event-log" +import type { Shell } from "@opencode-ai/schema/shell" +import type { DateTime } from "effect" +import type { Provider } from "@opencode-ai/schema/provider" +import type { Integration } from "@opencode-ai/schema/integration" +import type { Mcp } from "@opencode-ai/schema/mcp" +import type { Credential } from "@opencode-ai/schema/credential" +import type { Form } from "@opencode-ai/schema/form" +import type { Permission } from "@opencode-ai/schema/permission" +import type { PermissionSaved } from "@opencode-ai/schema/permission-saved" +import type { FileSystem } from "@opencode-ai/schema/filesystem" +import type { Command } from "@opencode-ai/schema/command" +import type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event" +import type { Pty } from "@opencode-ai/schema/pty" +import type { Question } from "@opencode-ai/schema/question" +import type { Reference } from "@opencode-ai/schema/reference" +import type { ProjectCopy } from "@opencode-ai/schema/project-copy" +import type { Vcs } from "@opencode-ai/schema/vcs" +import type { FileDiff } from "@opencode-ai/schema/file-diff" +import type { WebSearch } from "@opencode-ai/schema/websearch" + +export type Endpoint0_0Output = { readonly healthy: true; readonly version: string; readonly pid: number } export type HealthGetOperation = () => Effect.Effect -type Endpoint0_1Request = Parameters[0] -export type Endpoint0_1Input = { readonly instanceID: Endpoint0_1Request["payload"]["instanceID"] } -export type Endpoint0_1Output = EffectValue> +export type Endpoint0_1Input = { readonly instanceID: string } +export type Endpoint0_1Output = { readonly accepted: boolean } export type HealthStopOperation = (input: Endpoint0_1Input) => Effect.Effect export interface HealthApi { @@ -20,288 +51,792 @@ export interface HealthApi { readonly stop: HealthStopOperation } -export type Endpoint1_0Output = EffectValue> +export type Endpoint1_0Output = { readonly urls: ReadonlyArray } export type ServerGetOperation = () => Effect.Effect export interface ServerApi { readonly get: ServerGetOperation } -type Endpoint2_0Request = Parameters[0] -export type Endpoint2_0Input = { readonly location?: Endpoint2_0Request["query"]["location"] } -export type Endpoint2_0Output = EffectValue> +export type Endpoint2_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint2_0Output = Location.Info export type LocationGetOperation = (input?: Endpoint2_0Input) => Effect.Effect export interface LocationApi { readonly get: LocationGetOperation } -type Endpoint3_0Request = Parameters[0] -export type Endpoint3_0Input = { readonly location?: Endpoint3_0Request["query"]["location"] } -export type Endpoint3_0Output = EffectValue> +export type Endpoint3_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint3_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type AgentListOperation = (input?: Endpoint3_0Input) => Effect.Effect +export type Endpoint3_1Input = { + readonly agentID: Agent.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint3_1Output = { readonly location: Location.Info; readonly data: Agent.Info } +export type AgentGetOperation = (input: Endpoint3_1Input) => Effect.Effect + export interface AgentApi { readonly list: AgentListOperation + readonly get: AgentGetOperation } -type Endpoint4_0Request = Parameters[0] -export type Endpoint4_0Input = { readonly location?: Endpoint4_0Request["query"]["location"] } -export type Endpoint4_0Output = EffectValue> +export type Endpoint4_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint4_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type PluginListOperation = (input?: Endpoint4_0Input) => Effect.Effect export interface PluginApi { readonly list: PluginListOperation } -type Endpoint5_0Request = Parameters[0] export type Endpoint5_0Input = { - readonly workspace?: Endpoint5_0Request["query"]["workspace"] - readonly limit?: Endpoint5_0Request["query"]["limit"] - readonly order?: Endpoint5_0Request["query"]["order"] - readonly search?: Endpoint5_0Request["query"]["search"] - readonly parentID?: Endpoint5_0Request["query"]["parentID"] - readonly directory?: Endpoint5_0Request["query"]["directory"] - readonly project?: Endpoint5_0Request["query"]["project"] - readonly subpath?: Endpoint5_0Request["query"]["subpath"] - readonly cursor?: Endpoint5_0Request["query"]["cursor"] -} -export type Endpoint5_0Output = EffectValue> + readonly workspace?: Workspace.ID | undefined + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly search?: string | undefined + readonly parentID?: Session.ID | null | undefined + readonly directory?: AbsolutePath | undefined + readonly project?: Project.ID | undefined + readonly subpath?: RelativePath | undefined + readonly cursor?: (string & Brand.Brand<"SessionsCursor">) | undefined +} +export type Endpoint5_0Output = { + readonly data: ReadonlyArray + readonly cursor: { + readonly previous?: (string & Brand.Brand<"SessionsCursor">) | undefined + readonly next?: (string & Brand.Brand<"SessionsCursor">) | undefined + } +} export type SessionListOperation = (input?: Endpoint5_0Input) => Effect.Effect -type Endpoint5_1Request = Parameters[0] export type Endpoint5_1Input = { - readonly id?: Endpoint5_1Request["payload"]["id"] - readonly agent?: Endpoint5_1Request["payload"]["agent"] - readonly model?: Endpoint5_1Request["payload"]["model"] - readonly location?: Endpoint5_1Request["payload"]["location"] + readonly id?: Session.ID | undefined + readonly agent?: Agent.ID | undefined + readonly model?: Model.Ref | undefined + readonly location?: Location.Ref | undefined } -export type Endpoint5_1Output = EffectValue>["data"] +export type Endpoint5_1Output = Session.Info export type SessionCreateOperation = (input?: Endpoint5_1Input) => Effect.Effect -export type Endpoint5_2Output = EffectValue>["data"] +export type Endpoint5_2Output = { readonly [x: Session.ID]: { readonly type: "running" } } export type SessionActiveOperation = () => Effect.Effect -type Endpoint5_3Request = Parameters[0] -export type Endpoint5_3Input = { readonly sessionID: Endpoint5_3Request["params"]["sessionID"] } -export type Endpoint5_3Output = EffectValue>["data"] +export type Endpoint5_3Input = { readonly sessionID: Session.ID } +export type Endpoint5_3Output = Session.Info export type SessionGetOperation = (input: Endpoint5_3Input) => Effect.Effect -type Endpoint5_4Request = Parameters[0] -export type Endpoint5_4Input = { readonly sessionID: Endpoint5_4Request["params"]["sessionID"] } -export type Endpoint5_4Output = EffectValue> +export type Endpoint5_4Input = { readonly sessionID: Session.ID } +export type Endpoint5_4Output = void export type SessionRemoveOperation = (input: Endpoint5_4Input) => Effect.Effect -type Endpoint5_5Request = Parameters[0] -export type Endpoint5_5Input = { - readonly sessionID: Endpoint5_5Request["params"]["sessionID"] - readonly messageID?: Endpoint5_5Request["payload"]["messageID"] -} -export type Endpoint5_5Output = EffectValue>["data"] +export type Endpoint5_5Input = { readonly sessionID: Session.ID; readonly messageID?: SessionMessage.ID | undefined } +export type Endpoint5_5Output = Session.Info export type SessionForkOperation = (input: Endpoint5_5Input) => Effect.Effect -type Endpoint5_6Request = Parameters[0] -export type Endpoint5_6Input = { - readonly sessionID: Endpoint5_6Request["params"]["sessionID"] - readonly agent: Endpoint5_6Request["payload"]["agent"] -} -export type Endpoint5_6Output = EffectValue> +export type Endpoint5_6Input = { readonly sessionID: Session.ID; readonly agent: Agent.ID } +export type Endpoint5_6Output = void export type SessionSwitchAgentOperation = (input: Endpoint5_6Input) => Effect.Effect -type Endpoint5_7Request = Parameters[0] -export type Endpoint5_7Input = { - readonly sessionID: Endpoint5_7Request["params"]["sessionID"] - readonly model: Endpoint5_7Request["payload"]["model"] -} -export type Endpoint5_7Output = EffectValue> +export type Endpoint5_7Input = { readonly sessionID: Session.ID; readonly model: Model.Ref } +export type Endpoint5_7Output = void export type SessionSwitchModelOperation = (input: Endpoint5_7Input) => Effect.Effect -type Endpoint5_8Request = Parameters[0] -export type Endpoint5_8Input = { - readonly sessionID: Endpoint5_8Request["params"]["sessionID"] - readonly title: Endpoint5_8Request["payload"]["title"] -} -export type Endpoint5_8Output = EffectValue> +export type Endpoint5_8Input = { readonly sessionID: Session.ID; readonly title: string } +export type Endpoint5_8Output = void export type SessionRenameOperation = (input: Endpoint5_8Input) => Effect.Effect -type Endpoint5_9Request = Parameters[0] export type Endpoint5_9Input = { - readonly sessionID: Endpoint5_9Request["params"]["sessionID"] - readonly directory: Endpoint5_9Request["payload"]["directory"] - readonly workspaceID?: Endpoint5_9Request["payload"]["workspaceID"] + readonly sessionID: Session.ID + readonly directory: AbsolutePath + readonly workspaceID?: Workspace.ID | undefined } -export type Endpoint5_9Output = EffectValue> +export type Endpoint5_9Output = void export type SessionMoveOperation = (input: Endpoint5_9Input) => Effect.Effect -type Endpoint5_10Request = Parameters[0] export type Endpoint5_10Input = { - readonly sessionID: Endpoint5_10Request["params"]["sessionID"] - readonly id?: Endpoint5_10Request["payload"]["id"] - readonly text: Endpoint5_10Request["payload"]["text"] - readonly files?: Endpoint5_10Request["payload"]["files"] - readonly agents?: Endpoint5_10Request["payload"]["agents"] - readonly metadata?: Endpoint5_10Request["payload"]["metadata"] - readonly delivery?: Endpoint5_10Request["payload"]["delivery"] - readonly resume?: Endpoint5_10Request["payload"]["resume"] -} -export type Endpoint5_10Output = EffectValue>["data"] + readonly sessionID: Session.ID + readonly id?: SessionMessage.ID | undefined + readonly text: string + readonly files?: ReadonlyArray | undefined + readonly agents?: ReadonlyArray | undefined + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly delivery?: "steer" | "queue" | undefined + readonly resume?: boolean | undefined +} +export type Endpoint5_10Output = SessionPending.User export type SessionPromptOperation = (input: Endpoint5_10Input) => Effect.Effect -type Endpoint5_11Request = Parameters[0] export type Endpoint5_11Input = { - readonly sessionID: Endpoint5_11Request["params"]["sessionID"] - readonly id?: Endpoint5_11Request["payload"]["id"] - readonly command: Endpoint5_11Request["payload"]["command"] - readonly arguments?: Endpoint5_11Request["payload"]["arguments"] - readonly agent?: Endpoint5_11Request["payload"]["agent"] - readonly model?: Endpoint5_11Request["payload"]["model"] - readonly files?: Endpoint5_11Request["payload"]["files"] - readonly agents?: Endpoint5_11Request["payload"]["agents"] - readonly delivery?: Endpoint5_11Request["payload"]["delivery"] - readonly resume?: Endpoint5_11Request["payload"]["resume"] -} -export type Endpoint5_11Output = EffectValue>["data"] + readonly sessionID: Session.ID + readonly id?: SessionMessage.ID | undefined + readonly command: string + readonly arguments?: string | undefined + readonly agent?: Agent.ID | undefined + readonly model?: Model.Ref | undefined + readonly files?: ReadonlyArray | undefined + readonly agents?: ReadonlyArray | undefined + readonly delivery?: "steer" | "queue" | undefined + readonly resume?: boolean | undefined +} +export type Endpoint5_11Output = SessionPending.User export type SessionCommandOperation = (input: Endpoint5_11Input) => Effect.Effect -type Endpoint5_12Request = Parameters[0] export type Endpoint5_12Input = { - readonly sessionID: Endpoint5_12Request["params"]["sessionID"] - readonly id?: Endpoint5_12Request["payload"]["id"] - readonly skill: Endpoint5_12Request["payload"]["skill"] - readonly resume?: Endpoint5_12Request["payload"]["resume"] + readonly sessionID: Session.ID + readonly id?: SessionMessage.ID | undefined + readonly skill: Skill.ID + readonly resume?: boolean | undefined } -export type Endpoint5_12Output = EffectValue> +export type Endpoint5_12Output = void export type SessionSkillOperation = (input: Endpoint5_12Input) => Effect.Effect -type Endpoint5_13Request = Parameters[0] export type Endpoint5_13Input = { - readonly sessionID: Endpoint5_13Request["params"]["sessionID"] - readonly id?: Endpoint5_13Request["payload"]["id"] - readonly text: Endpoint5_13Request["payload"]["text"] - readonly description?: Endpoint5_13Request["payload"]["description"] - readonly metadata?: Endpoint5_13Request["payload"]["metadata"] - readonly delivery?: Endpoint5_13Request["payload"]["delivery"] - readonly resume?: Endpoint5_13Request["payload"]["resume"] -} -export type Endpoint5_13Output = EffectValue>["data"] + readonly sessionID: Session.ID + readonly id?: SessionMessage.ID | undefined + readonly text: string + readonly description?: string | undefined + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly delivery?: "steer" | "queue" | undefined + readonly resume?: boolean | undefined +} +export type Endpoint5_13Output = SessionPending.Synthetic export type SessionSyntheticOperation = (input: Endpoint5_13Input) => Effect.Effect -type Endpoint5_14Request = Parameters[0] export type Endpoint5_14Input = { - readonly sessionID: Endpoint5_14Request["params"]["sessionID"] - readonly id?: Endpoint5_14Request["payload"]["id"] - readonly command: Endpoint5_14Request["payload"]["command"] + readonly sessionID: Session.ID + readonly id?: Event.ID | undefined + readonly command: string } -export type Endpoint5_14Output = EffectValue> +export type Endpoint5_14Output = void export type SessionShellOperation = (input: Endpoint5_14Input) => Effect.Effect -type Endpoint5_15Request = Parameters[0] -export type Endpoint5_15Input = { - readonly sessionID: Endpoint5_15Request["params"]["sessionID"] - readonly id?: Endpoint5_15Request["payload"]["id"] -} -export type Endpoint5_15Output = EffectValue>["data"] +export type Endpoint5_15Input = { readonly sessionID: Session.ID; readonly id?: SessionMessage.ID | undefined } +export type Endpoint5_15Output = SessionPending.Compaction export type SessionCompactOperation = (input: Endpoint5_15Input) => Effect.Effect -type Endpoint5_16Request = Parameters[0] -export type Endpoint5_16Input = { readonly sessionID: Endpoint5_16Request["params"]["sessionID"] } -export type Endpoint5_16Output = EffectValue> +export type Endpoint5_16Input = { readonly sessionID: Session.ID } +export type Endpoint5_16Output = void export type SessionWaitOperation = (input: Endpoint5_16Input) => Effect.Effect -type Endpoint5_17Request = Parameters[0] export type Endpoint5_17Input = { - readonly sessionID: Endpoint5_17Request["params"]["sessionID"] - readonly messageID: Endpoint5_17Request["payload"]["messageID"] - readonly files?: Endpoint5_17Request["payload"]["files"] + readonly sessionID: Session.ID + readonly messageID: SessionMessage.ID + readonly files?: boolean | undefined } -export type Endpoint5_17Output = EffectValue>["data"] +export type Endpoint5_17Output = Session.Revert export type SessionRevertStageOperation = (input: Endpoint5_17Input) => Effect.Effect -type Endpoint5_18Request = Parameters[0] -export type Endpoint5_18Input = { readonly sessionID: Endpoint5_18Request["params"]["sessionID"] } -export type Endpoint5_18Output = EffectValue> +export type Endpoint5_18Input = { readonly sessionID: Session.ID } +export type Endpoint5_18Output = void export type SessionRevertClearOperation = (input: Endpoint5_18Input) => Effect.Effect -type Endpoint5_19Request = Parameters[0] -export type Endpoint5_19Input = { readonly sessionID: Endpoint5_19Request["params"]["sessionID"] } -export type Endpoint5_19Output = EffectValue> +export type Endpoint5_19Input = { readonly sessionID: Session.ID } +export type Endpoint5_19Output = void export type SessionRevertCommitOperation = (input: Endpoint5_19Input) => Effect.Effect -type Endpoint5_20Request = Parameters[0] -export type Endpoint5_20Input = { readonly sessionID: Endpoint5_20Request["params"]["sessionID"] } -export type Endpoint5_20Output = EffectValue>["data"] +export type Endpoint5_20Input = { readonly sessionID: Session.ID } +export type Endpoint5_20Output = ReadonlyArray export type SessionContextOperation = (input: Endpoint5_20Input) => Effect.Effect -type Endpoint5_21Request = Parameters[0] -export type Endpoint5_21Input = { readonly sessionID: Endpoint5_21Request["params"]["sessionID"] } -export type Endpoint5_21Output = EffectValue>["data"] +export type Endpoint5_21Input = { readonly sessionID: Session.ID } +export type Endpoint5_21Output = ReadonlyArray export type SessionPendingListOperation = (input: Endpoint5_21Input) => Effect.Effect -type Endpoint5_22Request = Parameters[0] -export type Endpoint5_22Input = { readonly sessionID: Endpoint5_22Request["params"]["sessionID"] } -export type Endpoint5_22Output = EffectValue< - ReturnType ->["data"] +export type Endpoint5_22Input = { readonly sessionID: Session.ID } +export type Endpoint5_22Output = ReadonlyArray export type SessionInstructionsEntryListOperation = ( input: Endpoint5_22Input, ) => Effect.Effect -type Endpoint5_23Request = Parameters[0] export type Endpoint5_23Input = { - readonly sessionID: Endpoint5_23Request["params"]["sessionID"] - readonly key: Endpoint5_23Request["params"]["key"] - readonly value: Endpoint5_23Request["payload"]["value"] + readonly sessionID: Session.ID + readonly key: InstructionEntry.Key + readonly value: Schema.Json } -export type Endpoint5_23Output = EffectValue> +export type Endpoint5_23Output = void export type SessionInstructionsEntryPutOperation = ( input: Endpoint5_23Input, ) => Effect.Effect -type Endpoint5_24Request = Parameters[0] -export type Endpoint5_24Input = { - readonly sessionID: Endpoint5_24Request["params"]["sessionID"] - readonly key: Endpoint5_24Request["params"]["key"] -} -export type Endpoint5_24Output = EffectValue< - ReturnType -> +export type Endpoint5_24Input = { readonly sessionID: Session.ID; readonly key: InstructionEntry.Key } +export type Endpoint5_24Output = void export type SessionInstructionsEntryRemoveOperation = ( input: Endpoint5_24Input, ) => Effect.Effect -type Endpoint5_25Request = Parameters[0] -export type Endpoint5_25Input = { - readonly sessionID: Endpoint5_25Request["params"]["sessionID"] - readonly prompt: Endpoint5_25Request["payload"]["prompt"] -} -export type Endpoint5_25Output = EffectValue>["data"] +export type Endpoint5_25Input = { readonly sessionID: Session.ID; readonly prompt: string } +export type Endpoint5_25Output = { readonly text: string } export type SessionGenerateOperation = (input: Endpoint5_25Input) => Effect.Effect -type Endpoint5_26Request = Parameters[0] export type Endpoint5_26Input = { - readonly sessionID: Endpoint5_26Request["params"]["sessionID"] - readonly after?: Endpoint5_26Request["query"]["after"] - readonly follow?: Endpoint5_26Request["query"]["follow"] -} -export type Endpoint5_26Output = StreamValue>> + readonly sessionID: Session.ID + readonly after?: Event.Seq | undefined + readonly follow?: boolean | undefined +} +export type Endpoint5_26Output = + | ( + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.agent.selected" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID; readonly agent: Agent.ID } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.model.selected" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID; readonly model: Model.Ref } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.moved" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly location: Location.Ref + readonly projectID?: Project.ID | undefined + readonly subpath?: RelativePath | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.renamed" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID; readonly title: string } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.deleted" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.forked" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly parentID: Session.ID + readonly parentSeq: number + readonly from?: SessionMessage.ID | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.input.promoted" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.input.admitted" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly inputID: SessionMessage.ID + readonly input: SessionPending.Message + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.execution.started" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.execution.succeeded" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.execution.failed" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly error: { readonly type: string; readonly message: string } + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.execution.interrupted" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID; readonly reason: "user" | "shutdown" | "superseded" } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.instructions.updated" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly delta: { readonly [x: string]: (string & Brand.Brand<"Instruction.Hash">) | "removed" } + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.synthetic" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly text: string + readonly description?: string | undefined + readonly metadata?: { readonly [x: string]: unknown } | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.skill.activated" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly id: Skill.ID + readonly name: Skill.Name + readonly text: string + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.shell.started" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID; readonly shell: Shell.Info } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.shell.ended" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly shell: Shell.Info + readonly output: { + readonly output: string + readonly cursor: number + readonly size: number + readonly truncated: boolean + } + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.step.started" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly agent: Agent.ID + readonly model: Model.Ref + readonly snapshot?: (string & Brand.Brand<"Snapshot.ID">) | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.step.ended" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" + readonly cost: number & Brand.Brand<"Money.USD"> + readonly tokens: { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { readonly read: number; readonly write: number } + } + readonly snapshot?: (string & Brand.Brand<"Snapshot.ID">) | undefined + readonly files?: ReadonlyArray | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.step.failed" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly error: { readonly type: string; readonly message: string } + readonly cost?: (number & Brand.Brand<"Money.USD">) | undefined + readonly tokens?: + | { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { readonly read: number; readonly write: number } + } + | undefined + readonly snapshot?: (string & Brand.Brand<"Snapshot.ID">) | undefined + readonly files?: ReadonlyArray | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.text.started" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly ordinal: number + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.text.ended" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly ordinal: number + readonly text: string + readonly state?: SessionMessage.ProviderState | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.reasoning.started" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly ordinal: number + readonly state?: SessionMessage.ProviderState | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.reasoning.ended" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly ordinal: number + readonly text: string + readonly state?: SessionMessage.ProviderState | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.tool.input.started" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly callID: string + readonly name: string + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.tool.input.ended" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly callID: string + readonly text: string + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.tool.called" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly callID: string + readonly input: { readonly [x: string]: unknown } + readonly executed: boolean + readonly state?: SessionMessage.ProviderState | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.tool.success" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly callID: string + readonly content: readonly [ + ( + | { readonly type: "text"; readonly text: string } + | { + readonly type: "file" + readonly uri: string + readonly mime: string + readonly name?: string | undefined + } + ), + ...Array< + | { readonly type: "text"; readonly text: string } + | { + readonly type: "file" + readonly uri: string + readonly mime: string + readonly name?: string | undefined + } + >, + ] + readonly metadata?: { readonly [x: string]: Schema.Json } | undefined + readonly executed: boolean + readonly resultState?: SessionMessage.ProviderState | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.tool.failed" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly callID: string + readonly error: { readonly type: string; readonly message: string } + readonly content?: + | readonly [ + ( + | { readonly type: "text"; readonly text: string } + | { + readonly type: "file" + readonly uri: string + readonly mime: string + readonly name?: string | undefined + } + ), + ...Array< + | { readonly type: "text"; readonly text: string } + | { + readonly type: "file" + readonly uri: string + readonly mime: string + readonly name?: string | undefined + } + >, + ] + | undefined + readonly metadata?: { readonly [x: string]: Schema.Json } | undefined + readonly executed: boolean + readonly resultState?: SessionMessage.ProviderState | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.retry.scheduled" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly attempt: number + readonly at: number + readonly error: { readonly type: string; readonly message: string } + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.compaction.admitted" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.compaction.started" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly reason: "auto" | "manual" + readonly recent: string + readonly inputID?: SessionMessage.ID | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.compaction.ended" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly reason: "auto" | "manual" + readonly text: string + readonly recent: string + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.compaction.failed" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly reason: "auto" | "manual" + readonly error: { readonly type: string; readonly message: string } + readonly inputID?: SessionMessage.ID | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.revert.staged" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID; readonly revert: Session.Revert } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.revert.cleared" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.revert.committed" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID; readonly to: SessionMessage.ID } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.usage.recorded" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly source: "title" | "compaction" + readonly cost: number & Brand.Brand<"Money.USD"> + readonly tokens: { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { readonly read: number; readonly write: number } + } + } + } + ) + | EventLog.Synced export type SessionLogOperation = (input: Endpoint5_26Input) => Stream.Stream -type Endpoint5_27Request = Parameters[0] -export type Endpoint5_27Input = { readonly sessionID: Endpoint5_27Request["params"]["sessionID"] } -export type Endpoint5_27Output = EffectValue> +export type Endpoint5_27Input = { readonly sessionID: Session.ID } +export type Endpoint5_27Output = void export type SessionInterruptOperation = (input: Endpoint5_27Input) => Effect.Effect -type Endpoint5_28Request = Parameters[0] -export type Endpoint5_28Input = { readonly sessionID: Endpoint5_28Request["params"]["sessionID"] } -export type Endpoint5_28Output = EffectValue> +export type Endpoint5_28Input = { readonly sessionID: Session.ID } +export type Endpoint5_28Output = void export type SessionBackgroundOperation = (input: Endpoint5_28Input) => Effect.Effect -type Endpoint5_29Request = Parameters[0] -export type Endpoint5_29Input = { - readonly sessionID: Endpoint5_29Request["params"]["sessionID"] - readonly messageID: Endpoint5_29Request["params"]["messageID"] -} -export type Endpoint5_29Output = EffectValue>["data"] +export type Endpoint5_29Input = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID } +export type Endpoint5_29Output = SessionMessage.Info export type SessionMessageOperation = (input: Endpoint5_29Input) => Effect.Effect export interface SessionApi { @@ -343,28 +878,32 @@ export interface SessionApi { readonly message: SessionMessageOperation } -type Endpoint6_0Request = Parameters[0] export type Endpoint6_0Input = { - readonly sessionID: Endpoint6_0Request["params"]["sessionID"] - readonly limit?: Endpoint6_0Request["query"]["limit"] - readonly order?: Endpoint6_0Request["query"]["order"] - readonly cursor?: Endpoint6_0Request["query"]["cursor"] + readonly sessionID: Session.ID + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly cursor?: string | undefined +} +export type Endpoint6_0Output = { + readonly data: ReadonlyArray + readonly cursor: { readonly previous?: string | undefined; readonly next?: string | undefined } } -export type Endpoint6_0Output = EffectValue> export type MessageListOperation = (input: Endpoint6_0Input) => Effect.Effect export interface MessageApi { readonly list: MessageListOperation } -type Endpoint7_0Request = Parameters[0] -export type Endpoint7_0Input = { readonly location?: Endpoint7_0Request["query"]["location"] } -export type Endpoint7_0Output = EffectValue> +export type Endpoint7_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint7_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type ModelListOperation = (input?: Endpoint7_0Input) => Effect.Effect -type Endpoint7_1Request = Parameters[0] -export type Endpoint7_1Input = { readonly location?: Endpoint7_1Request["query"]["location"] } -export type Endpoint7_1Output = EffectValue> +export type Endpoint7_1Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint7_1Output = { readonly location: Location.Info; readonly data: Model.Info | undefined } export type ModelDefaultOperation = (input?: Endpoint7_1Input) => Effect.Effect export interface ModelApi { @@ -372,30 +911,29 @@ export interface ModelApi { readonly default: ModelDefaultOperation } -type Endpoint8_0Request = Parameters[0] export type Endpoint8_0Input = { - readonly location?: Endpoint8_0Request["query"]["location"] - readonly prompt: Endpoint8_0Request["payload"]["prompt"] - readonly model?: Endpoint8_0Request["payload"]["model"] + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly prompt: string + readonly model?: Model.Ref | undefined } -export type Endpoint8_0Output = EffectValue>["data"] +export type Endpoint8_0Output = { readonly text: string } export type GenerateTextOperation = (input: Endpoint8_0Input) => Effect.Effect export interface GenerateApi { readonly text: GenerateTextOperation } -type Endpoint9_0Request = Parameters[0] -export type Endpoint9_0Input = { readonly location?: Endpoint9_0Request["query"]["location"] } -export type Endpoint9_0Output = EffectValue> +export type Endpoint9_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint9_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type ProviderListOperation = (input?: Endpoint9_0Input) => Effect.Effect -type Endpoint9_1Request = Parameters[0] export type Endpoint9_1Input = { - readonly providerID: Endpoint9_1Request["params"]["providerID"] - readonly location?: Endpoint9_1Request["query"]["location"] + readonly providerID: Provider.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint9_1Output = EffectValue> +export type Endpoint9_1Output = { readonly location: Location.Info; readonly data: Provider.Info } export type ProviderGetOperation = (input: Endpoint9_1Input) => Effect.Effect export interface ProviderApi { @@ -403,118 +941,109 @@ export interface ProviderApi { readonly get: ProviderGetOperation } -type Endpoint10_0Request = Parameters[0] -export type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["location"] } -export type Endpoint10_0Output = EffectValue> +export type Endpoint10_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint10_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type IntegrationListOperation = (input?: Endpoint10_0Input) => Effect.Effect -type Endpoint10_1Request = Parameters[0] export type Endpoint10_1Input = { - readonly integrationID: Endpoint10_1Request["params"]["integrationID"] - readonly location?: Endpoint10_1Request["query"]["location"] + readonly integrationID: Integration.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint10_1Output = EffectValue> +export type Endpoint10_1Output = { readonly location: Location.Info; readonly data: Integration.Info | undefined } export type IntegrationGetOperation = (input: Endpoint10_1Input) => Effect.Effect -type Endpoint10_2Request = Parameters[0] export type Endpoint10_2Input = { - readonly location?: Endpoint10_2Request["query"]["location"] - readonly url: Endpoint10_2Request["payload"]["url"] + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly url: string } -export type Endpoint10_2Output = EffectValue> +export type Endpoint10_2Output = void export type IntegrationWellknownAddOperation = ( input: Endpoint10_2Input, ) => Effect.Effect -type Endpoint10_3Request = Parameters[0] export type Endpoint10_3Input = { - readonly integrationID: Endpoint10_3Request["params"]["integrationID"] - readonly location?: Endpoint10_3Request["query"]["location"] - readonly key: Endpoint10_3Request["payload"]["key"] - readonly label?: Endpoint10_3Request["payload"]["label"] + readonly integrationID: Integration.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly key: string + readonly label?: string | undefined } -export type Endpoint10_3Output = EffectValue> +export type Endpoint10_3Output = void export type IntegrationConnectKeyOperation = ( input: Endpoint10_3Input, ) => Effect.Effect -type Endpoint10_4Request = Parameters[0] export type Endpoint10_4Input = { - readonly integrationID: Endpoint10_4Request["params"]["integrationID"] - readonly location?: Endpoint10_4Request["query"]["location"] - readonly methodID: Endpoint10_4Request["payload"]["methodID"] - readonly inputs: Endpoint10_4Request["payload"]["inputs"] - readonly label?: Endpoint10_4Request["payload"]["label"] + readonly integrationID: Integration.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly methodID: Integration.MethodID + readonly inputs: { readonly [x: string]: string } + readonly label?: string | undefined } -export type Endpoint10_4Output = EffectValue> +export type Endpoint10_4Output = { readonly location: Location.Info; readonly data: Integration.Attempt } export type IntegrationOauthConnectOperation = ( input: Endpoint10_4Input, ) => Effect.Effect -type Endpoint10_5Request = Parameters[0] export type Endpoint10_5Input = { - readonly integrationID: Endpoint10_5Request["params"]["integrationID"] - readonly attemptID: Endpoint10_5Request["params"]["attemptID"] - readonly location?: Endpoint10_5Request["query"]["location"] + readonly integrationID: Integration.ID + readonly attemptID: Integration.AttemptID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint10_5Output = EffectValue> +export type Endpoint10_5Output = { readonly location: Location.Info; readonly data: Integration.AttemptStatus } export type IntegrationOauthStatusOperation = ( input: Endpoint10_5Input, ) => Effect.Effect -type Endpoint10_6Request = Parameters[0] export type Endpoint10_6Input = { - readonly integrationID: Endpoint10_6Request["params"]["integrationID"] - readonly attemptID: Endpoint10_6Request["params"]["attemptID"] - readonly location?: Endpoint10_6Request["query"]["location"] - readonly code?: Endpoint10_6Request["payload"]["code"] + readonly integrationID: Integration.ID + readonly attemptID: Integration.AttemptID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly code?: string | undefined } -export type Endpoint10_6Output = EffectValue> +export type Endpoint10_6Output = void export type IntegrationOauthCompleteOperation = ( input: Endpoint10_6Input, ) => Effect.Effect -type Endpoint10_7Request = Parameters[0] export type Endpoint10_7Input = { - readonly integrationID: Endpoint10_7Request["params"]["integrationID"] - readonly attemptID: Endpoint10_7Request["params"]["attemptID"] - readonly location?: Endpoint10_7Request["query"]["location"] + readonly integrationID: Integration.ID + readonly attemptID: Integration.AttemptID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint10_7Output = EffectValue> +export type Endpoint10_7Output = void export type IntegrationOauthCancelOperation = ( input: Endpoint10_7Input, ) => Effect.Effect -type Endpoint10_8Request = Parameters[0] export type Endpoint10_8Input = { - readonly integrationID: Endpoint10_8Request["params"]["integrationID"] - readonly location?: Endpoint10_8Request["query"]["location"] - readonly methodID: Endpoint10_8Request["payload"]["methodID"] - readonly label?: Endpoint10_8Request["payload"]["label"] + readonly integrationID: Integration.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly methodID: Integration.MethodID + readonly label?: string | undefined } -export type Endpoint10_8Output = EffectValue> +export type Endpoint10_8Output = { readonly location: Location.Info; readonly data: Integration.CommandAttempt } export type IntegrationCommandConnectOperation = ( input: Endpoint10_8Input, ) => Effect.Effect -type Endpoint10_9Request = Parameters[0] export type Endpoint10_9Input = { - readonly integrationID: Endpoint10_9Request["params"]["integrationID"] - readonly attemptID: Endpoint10_9Request["params"]["attemptID"] - readonly location?: Endpoint10_9Request["query"]["location"] + readonly integrationID: Integration.ID + readonly attemptID: Integration.AttemptID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint10_9Output = EffectValue> +export type Endpoint10_9Output = { readonly location: Location.Info; readonly data: Integration.CommandAttemptStatus } export type IntegrationCommandStatusOperation = ( input: Endpoint10_9Input, ) => Effect.Effect -type Endpoint10_10Request = Parameters[0] export type Endpoint10_10Input = { - readonly integrationID: Endpoint10_10Request["params"]["integrationID"] - readonly attemptID: Endpoint10_10Request["params"]["attemptID"] - readonly location?: Endpoint10_10Request["query"]["location"] + readonly integrationID: Integration.ID + readonly attemptID: Integration.AttemptID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint10_10Output = EffectValue> +export type Endpoint10_10Output = void export type IntegrationCommandCancelOperation = ( input: Endpoint10_10Input, ) => Effect.Effect @@ -537,47 +1066,45 @@ export interface IntegrationApi { } } -type Endpoint11_0Request = Parameters[0] -export type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] } -export type Endpoint11_0Output = EffectValue> +export type Endpoint11_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint11_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type McpListOperation = (input?: Endpoint11_0Input) => Effect.Effect -type Endpoint11_1Request = Parameters[0] export type Endpoint11_1Input = { - readonly server: Endpoint11_1Request["params"]["server"] - readonly location?: Endpoint11_1Request["query"]["location"] - readonly config: Endpoint11_1Request["payload"]["config"] + readonly server: string + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly config: Mcp.LocalConfig | Mcp.RemoteConfig } -export type Endpoint11_1Output = EffectValue> +export type Endpoint11_1Output = void export type McpAddOperation = (input: Endpoint11_1Input) => Effect.Effect -type Endpoint11_2Request = Parameters[0] export type Endpoint11_2Input = { - readonly server: Endpoint11_2Request["params"]["server"] - readonly location?: Endpoint11_2Request["query"]["location"] + readonly server: string + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint11_2Output = EffectValue> +export type Endpoint11_2Output = void export type McpRemoveOperation = (input: Endpoint11_2Input) => Effect.Effect -type Endpoint11_3Request = Parameters[0] export type Endpoint11_3Input = { - readonly server: Endpoint11_3Request["params"]["server"] - readonly location?: Endpoint11_3Request["query"]["location"] + readonly server: string + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint11_3Output = EffectValue> +export type Endpoint11_3Output = void export type McpConnectOperation = (input: Endpoint11_3Input) => Effect.Effect -type Endpoint11_4Request = Parameters[0] export type Endpoint11_4Input = { - readonly server: Endpoint11_4Request["params"]["server"] - readonly location?: Endpoint11_4Request["query"]["location"] + readonly server: string + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint11_4Output = EffectValue> +export type Endpoint11_4Output = void export type McpDisconnectOperation = (input: Endpoint11_4Input) => Effect.Effect -type Endpoint11_5Request = Parameters[0] -export type Endpoint11_5Input = { readonly location?: Endpoint11_5Request["query"]["location"] } -export type Endpoint11_5Output = EffectValue> +export type Endpoint11_5Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint11_5Output = { readonly location: Location.Info; readonly data: Mcp.ResourceCatalog } export type McpResourceCatalogOperation = (input?: Endpoint11_5Input) => Effect.Effect export interface McpApi { @@ -589,21 +1116,19 @@ export interface McpApi { readonly resource: { readonly catalog: McpResourceCatalogOperation } } -type Endpoint12_0Request = Parameters[0] export type Endpoint12_0Input = { - readonly credentialID: Endpoint12_0Request["params"]["credentialID"] - readonly location?: Endpoint12_0Request["query"]["location"] - readonly label: Endpoint12_0Request["payload"]["label"] + readonly credentialID: Credential.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly label: string } -export type Endpoint12_0Output = EffectValue> +export type Endpoint12_0Output = void export type CredentialUpdateOperation = (input: Endpoint12_0Input) => Effect.Effect -type Endpoint12_1Request = Parameters[0] export type Endpoint12_1Input = { - readonly credentialID: Endpoint12_1Request["params"]["credentialID"] - readonly location?: Endpoint12_1Request["query"]["location"] + readonly credentialID: Credential.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint12_1Output = EffectValue> +export type Endpoint12_1Output = void export type CredentialRemoveOperation = (input: Endpoint12_1Input) => Effect.Effect export interface CredentialApi { @@ -611,20 +1136,20 @@ export interface CredentialApi { readonly remove: CredentialRemoveOperation } -export type Endpoint13_0Output = EffectValue> +export type Endpoint13_0Output = ReadonlyArray export type ProjectListOperation = () => Effect.Effect -type Endpoint13_1Request = Parameters[0] -export type Endpoint13_1Input = { readonly location?: Endpoint13_1Request["query"]["location"] } -export type Endpoint13_1Output = EffectValue> +export type Endpoint13_1Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint13_1Output = Project.Current export type ProjectCurrentOperation = (input?: Endpoint13_1Input) => Effect.Effect -type Endpoint13_2Request = Parameters[0] export type Endpoint13_2Input = { - readonly projectID: Endpoint13_2Request["params"]["projectID"] - readonly location?: Endpoint13_2Request["query"]["location"] + readonly projectID: Project.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint13_2Output = EffectValue> +export type Endpoint13_2Output = Project.Directories export type ProjectDirectoriesOperation = (input: Endpoint13_2Input) => Effect.Effect export interface ProjectApi { @@ -633,58 +1158,40 @@ export interface ProjectApi { readonly directories: ProjectDirectoriesOperation } -type Endpoint14_0Request = Parameters[0] -export type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] } -export type Endpoint14_0Output = EffectValue> +export type Endpoint14_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint14_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type FormRequestListOperation = (input?: Endpoint14_0Input) => Effect.Effect -type Endpoint14_1Request = Parameters[0] -export type Endpoint14_1Input = { readonly sessionID: Endpoint14_1Request["params"]["sessionID"] } -export type Endpoint14_1Output = EffectValue>["data"] +export type Endpoint14_1Input = { readonly sessionID: string } +export type Endpoint14_1Output = ReadonlyArray export type FormListOperation = (input: Endpoint14_1Input) => Effect.Effect -type Endpoint14_2Request = Parameters[0] export type Endpoint14_2Input = { - readonly sessionID: Endpoint14_2Request["params"]["sessionID"] - readonly id?: Endpoint14_2Request["payload"]["id"] - readonly title: Endpoint14_2Request["payload"]["title"] - readonly metadata?: Endpoint14_2Request["payload"]["metadata"] - readonly fields: Endpoint14_2Request["payload"]["fields"] + readonly sessionID: string + readonly id?: Form.ID | undefined + readonly title: string + readonly metadata?: Form.Metadata | undefined + readonly fields: Form.Fields } -export type Endpoint14_2Output = EffectValue>["data"] +export type Endpoint14_2Output = Form.Info export type FormCreateOperation = (input: Endpoint14_2Input) => Effect.Effect -type Endpoint14_3Request = Parameters[0] -export type Endpoint14_3Input = { - readonly sessionID: Endpoint14_3Request["params"]["sessionID"] - readonly formID: Endpoint14_3Request["params"]["formID"] -} -export type Endpoint14_3Output = EffectValue>["data"] +export type Endpoint14_3Input = { readonly sessionID: string; readonly formID: Form.ID } +export type Endpoint14_3Output = Form.Info export type FormGetOperation = (input: Endpoint14_3Input) => Effect.Effect -type Endpoint14_4Request = Parameters[0] -export type Endpoint14_4Input = { - readonly sessionID: Endpoint14_4Request["params"]["sessionID"] - readonly formID: Endpoint14_4Request["params"]["formID"] -} -export type Endpoint14_4Output = EffectValue>["data"] +export type Endpoint14_4Input = { readonly sessionID: string; readonly formID: Form.ID } +export type Endpoint14_4Output = Form.State export type FormStateOperation = (input: Endpoint14_4Input) => Effect.Effect -type Endpoint14_5Request = Parameters[0] -export type Endpoint14_5Input = { - readonly sessionID: Endpoint14_5Request["params"]["sessionID"] - readonly formID: Endpoint14_5Request["params"]["formID"] - readonly answer: Endpoint14_5Request["payload"]["answer"] -} -export type Endpoint14_5Output = EffectValue> +export type Endpoint14_5Input = { readonly sessionID: string; readonly formID: Form.ID; readonly answer: Form.Answer } +export type Endpoint14_5Output = void export type FormReplyOperation = (input: Endpoint14_5Input) => Effect.Effect -type Endpoint14_6Request = Parameters[0] -export type Endpoint14_6Input = { - readonly sessionID: Endpoint14_6Request["params"]["sessionID"] - readonly formID: Endpoint14_6Request["params"]["formID"] -} -export type Endpoint14_6Output = EffectValue> +export type Endpoint14_6Input = { readonly sessionID: string; readonly formID: Form.ID } +export type Endpoint14_6Output = void export type FormCancelOperation = (input: Endpoint14_6Input) => Effect.Effect export interface FormApi { @@ -697,70 +1204,54 @@ export interface FormApi { readonly cancel: FormCancelOperation } -type Endpoint15_0Request = Parameters[0] -export type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] } -export type Endpoint15_0Output = EffectValue> +export type Endpoint15_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint15_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type PermissionRequestListOperation = ( input?: Endpoint15_0Input, ) => Effect.Effect -type Endpoint15_1Request = Parameters[0] -export type Endpoint15_1Input = { readonly projectID?: Endpoint15_1Request["query"]["projectID"] } -export type Endpoint15_1Output = EffectValue< - ReturnType ->["data"] +export type Endpoint15_1Input = { readonly projectID?: Project.ID | undefined } +export type Endpoint15_1Output = ReadonlyArray export type PermissionSavedListOperation = ( input?: Endpoint15_1Input, ) => Effect.Effect -type Endpoint15_2Request = Parameters[0] -export type Endpoint15_2Input = { readonly id: Endpoint15_2Request["params"]["id"] } -export type Endpoint15_2Output = EffectValue> +export type Endpoint15_2Input = { readonly id: PermissionSaved.ID } +export type Endpoint15_2Output = void export type PermissionSavedRemoveOperation = ( input: Endpoint15_2Input, ) => Effect.Effect -type Endpoint15_3Request = Parameters[0] export type Endpoint15_3Input = { - readonly sessionID: Endpoint15_3Request["params"]["sessionID"] - readonly id?: Endpoint15_3Request["payload"]["id"] - readonly action: Endpoint15_3Request["payload"]["action"] - readonly resources: Endpoint15_3Request["payload"]["resources"] - readonly save?: Endpoint15_3Request["payload"]["save"] - readonly metadata?: Endpoint15_3Request["payload"]["metadata"] - readonly source?: Endpoint15_3Request["payload"]["source"] - readonly agent?: Endpoint15_3Request["payload"]["agent"] -} -export type Endpoint15_3Output = EffectValue< - ReturnType ->["data"] + readonly sessionID: Session.ID + readonly id?: Permission.ID | undefined + readonly action: string + readonly resources: ReadonlyArray + readonly save?: ReadonlyArray | undefined + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly source?: Permission.Source | undefined + readonly agent?: Agent.ID | undefined +} +export type Endpoint15_3Output = { readonly id: Permission.ID; readonly effect: Permission.Effect } export type PermissionCreateOperation = (input: Endpoint15_3Input) => Effect.Effect -type Endpoint15_4Request = Parameters[0] -export type Endpoint15_4Input = { readonly sessionID: Endpoint15_4Request["params"]["sessionID"] } -export type Endpoint15_4Output = EffectValue< - ReturnType ->["data"] +export type Endpoint15_4Input = { readonly sessionID: Session.ID } +export type Endpoint15_4Output = ReadonlyArray export type PermissionListOperation = (input: Endpoint15_4Input) => Effect.Effect -type Endpoint15_5Request = Parameters[0] -export type Endpoint15_5Input = { - readonly sessionID: Endpoint15_5Request["params"]["sessionID"] - readonly requestID: Endpoint15_5Request["params"]["requestID"] -} -export type Endpoint15_5Output = EffectValue< - ReturnType ->["data"] +export type Endpoint15_5Input = { readonly sessionID: Session.ID; readonly requestID: Permission.ID } +export type Endpoint15_5Output = Permission.Request export type PermissionGetOperation = (input: Endpoint15_5Input) => Effect.Effect -type Endpoint15_6Request = Parameters[0] export type Endpoint15_6Input = { - readonly sessionID: Endpoint15_6Request["params"]["sessionID"] - readonly requestID: Endpoint15_6Request["params"]["requestID"] - readonly reply: Endpoint15_6Request["payload"]["reply"] - readonly message?: Endpoint15_6Request["payload"]["message"] + readonly sessionID: Session.ID + readonly requestID: Permission.ID + readonly reply: Permission.Reply + readonly message?: string | undefined } -export type Endpoint15_6Output = EffectValue> +export type Endpoint15_6Output = void export type PermissionReplyOperation = (input: Endpoint15_6Input) => Effect.Effect export interface PermissionApi { @@ -772,22 +1263,20 @@ export interface PermissionApi { readonly reply: PermissionReplyOperation } -type Endpoint16_0Request = Parameters[0] export type Endpoint16_0Input = { - readonly location?: Endpoint16_0Request["query"]["location"] - readonly path?: Endpoint16_0Request["query"]["path"] + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly path?: RelativePath | undefined } -export type Endpoint16_0Output = EffectValue> +export type Endpoint16_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type FileListOperation = (input?: Endpoint16_0Input) => Effect.Effect -type Endpoint16_1Request = Parameters[0] export type Endpoint16_1Input = { - readonly location?: Endpoint16_1Request["query"]["location"] - readonly query: Endpoint16_1Request["query"]["query"] - readonly type?: Endpoint16_1Request["query"]["type"] - readonly limit?: Endpoint16_1Request["query"]["limit"] + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly query: string + readonly type?: "file" | "directory" | undefined + readonly limit?: number | undefined } -export type Endpoint16_1Output = EffectValue> +export type Endpoint16_1Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type FileFindOperation = (input: Endpoint16_1Input) => Effect.Effect export interface FileApi { @@ -795,72 +1284,71 @@ export interface FileApi { readonly find: FileFindOperation } -type Endpoint17_0Request = Parameters[0] -export type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] } -export type Endpoint17_0Output = EffectValue> +export type Endpoint17_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint17_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type CommandListOperation = (input?: Endpoint17_0Input) => Effect.Effect export interface CommandApi { readonly list: CommandListOperation } -type Endpoint18_0Request = Parameters[0] -export type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] } -export type Endpoint18_0Output = EffectValue> +export type Endpoint18_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint18_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type SkillListOperation = (input?: Endpoint18_0Input) => Effect.Effect export interface SkillApi { readonly list: SkillListOperation } -export type Endpoint19_0Output = StreamValue>> +export type Endpoint19_0Output = OpenCodeEvent export type EventSubscribeOperation = () => Stream.Stream export interface EventApi { readonly subscribe: EventSubscribeOperation } -type Endpoint20_0Request = Parameters[0] -export type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] } -export type Endpoint20_0Output = EffectValue> +export type Endpoint20_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint20_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type PtyListOperation = (input?: Endpoint20_0Input) => Effect.Effect -type Endpoint20_1Request = Parameters[0] export type Endpoint20_1Input = { - readonly location?: Endpoint20_1Request["query"]["location"] - readonly command?: Endpoint20_1Request["payload"]["command"] - readonly args?: Endpoint20_1Request["payload"]["args"] - readonly cwd?: Endpoint20_1Request["payload"]["cwd"] - readonly title?: Endpoint20_1Request["payload"]["title"] - readonly env?: Endpoint20_1Request["payload"]["env"] -} -export type Endpoint20_1Output = EffectValue> + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly command?: string | undefined + readonly args?: ReadonlyArray | undefined + readonly cwd?: string | undefined + readonly title?: string | undefined + readonly env?: { readonly [x: string]: string } | undefined +} +export type Endpoint20_1Output = { readonly location: Location.Info; readonly data: Pty.Info } export type PtyCreateOperation = (input?: Endpoint20_1Input) => Effect.Effect -type Endpoint20_2Request = Parameters[0] export type Endpoint20_2Input = { - readonly ptyID: Endpoint20_2Request["params"]["ptyID"] - readonly location?: Endpoint20_2Request["query"]["location"] + readonly ptyID: Pty.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint20_2Output = EffectValue> +export type Endpoint20_2Output = { readonly location: Location.Info; readonly data: Pty.Info } export type PtyGetOperation = (input: Endpoint20_2Input) => Effect.Effect -type Endpoint20_3Request = Parameters[0] export type Endpoint20_3Input = { - readonly ptyID: Endpoint20_3Request["params"]["ptyID"] - readonly location?: Endpoint20_3Request["query"]["location"] - readonly title?: Endpoint20_3Request["payload"]["title"] - readonly size?: Endpoint20_3Request["payload"]["size"] + readonly ptyID: Pty.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly title?: string | undefined + readonly size?: { readonly rows: number; readonly cols: number } | undefined } -export type Endpoint20_3Output = EffectValue> +export type Endpoint20_3Output = { readonly location: Location.Info; readonly data: Pty.Info } export type PtyUpdateOperation = (input: Endpoint20_3Input) => Effect.Effect -type Endpoint20_4Request = Parameters[0] export type Endpoint20_4Input = { - readonly ptyID: Endpoint20_4Request["params"]["ptyID"] - readonly location?: Endpoint20_4Request["query"]["location"] + readonly ptyID: Pty.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint20_4Output = EffectValue> +export type Endpoint20_4Output = void export type PtyRemoveOperation = (input: Endpoint20_4Input) => Effect.Effect export interface PtyApi { @@ -871,55 +1359,59 @@ export interface PtyApi { readonly remove: PtyRemoveOperation } -type Endpoint21_0Request = Parameters[0] -export type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] } -export type Endpoint21_0Output = EffectValue> +export type Endpoint21_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint21_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type ShellListOperation = (input?: Endpoint21_0Input) => Effect.Effect -type Endpoint21_1Request = Parameters[0] export type Endpoint21_1Input = { - readonly location?: Endpoint21_1Request["query"]["location"] - readonly command: Endpoint21_1Request["payload"]["command"] - readonly cwd?: Endpoint21_1Request["payload"]["cwd"] - readonly timeout: Endpoint21_1Request["payload"]["timeout"] - readonly metadata?: Endpoint21_1Request["payload"]["metadata"] + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly command: string + readonly cwd?: string | undefined + readonly timeout: number + readonly metadata?: { readonly [x: string]: unknown } | undefined } -export type Endpoint21_1Output = EffectValue> +export type Endpoint21_1Output = { readonly location: Location.Info; readonly data: Shell.Info } export type ShellCreateOperation = (input: Endpoint21_1Input) => Effect.Effect -type Endpoint21_2Request = Parameters[0] export type Endpoint21_2Input = { - readonly id: Endpoint21_2Request["params"]["id"] - readonly location?: Endpoint21_2Request["query"]["location"] + readonly id: Shell.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint21_2Output = EffectValue> +export type Endpoint21_2Output = { readonly location: Location.Info; readonly data: Shell.Info } export type ShellGetOperation = (input: Endpoint21_2Input) => Effect.Effect -type Endpoint21_3Request = Parameters[0] export type Endpoint21_3Input = { - readonly id: Endpoint21_3Request["params"]["id"] - readonly location?: Endpoint21_3Request["query"]["location"] - readonly timeout: Endpoint21_3Request["payload"]["timeout"] + readonly id: Shell.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly timeout: number } -export type Endpoint21_3Output = EffectValue> +export type Endpoint21_3Output = { readonly location: Location.Info; readonly data: Shell.Info } export type ShellTimeoutOperation = (input: Endpoint21_3Input) => Effect.Effect -type Endpoint21_4Request = Parameters[0] export type Endpoint21_4Input = { - readonly id: Endpoint21_4Request["params"]["id"] - readonly location?: Endpoint21_4Request["query"]["location"] - readonly cursor?: Endpoint21_4Request["query"]["cursor"] - readonly limit?: Endpoint21_4Request["query"]["limit"] + readonly id: Shell.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly cursor?: number | undefined + readonly limit?: number | undefined +} +export type Endpoint21_4Output = { + readonly location: Location.Info + readonly data: { + readonly output: string + readonly cursor: number + readonly size: number + readonly truncated: boolean + } } -export type Endpoint21_4Output = EffectValue> export type ShellOutputOperation = (input: Endpoint21_4Input) => Effect.Effect -type Endpoint21_5Request = Parameters[0] export type Endpoint21_5Input = { - readonly id: Endpoint21_5Request["params"]["id"] - readonly location?: Endpoint21_5Request["query"]["location"] + readonly id: Shell.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint21_5Output = EffectValue> +export type Endpoint21_5Output = void export type ShellRemoveOperation = (input: Endpoint21_5Input) => Effect.Effect export interface ShellApi { @@ -931,33 +1423,28 @@ export interface ShellApi { readonly remove: ShellRemoveOperation } -type Endpoint22_0Request = Parameters[0] -export type Endpoint22_0Input = { readonly location?: Endpoint22_0Request["query"]["location"] } -export type Endpoint22_0Output = EffectValue> +export type Endpoint22_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint22_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type QuestionRequestListOperation = ( input?: Endpoint22_0Input, ) => Effect.Effect -type Endpoint22_1Request = Parameters[0] -export type Endpoint22_1Input = { readonly sessionID: Endpoint22_1Request["params"]["sessionID"] } -export type Endpoint22_1Output = EffectValue>["data"] +export type Endpoint22_1Input = { readonly sessionID: Session.ID } +export type Endpoint22_1Output = ReadonlyArray export type QuestionListOperation = (input: Endpoint22_1Input) => Effect.Effect -type Endpoint22_2Request = Parameters[0] export type Endpoint22_2Input = { - readonly sessionID: Endpoint22_2Request["params"]["sessionID"] - readonly requestID: Endpoint22_2Request["params"]["requestID"] - readonly answers: Endpoint22_2Request["payload"]["answers"] + readonly sessionID: Session.ID + readonly requestID: Question.ID + readonly answers: ReadonlyArray } -export type Endpoint22_2Output = EffectValue> +export type Endpoint22_2Output = void export type QuestionReplyOperation = (input: Endpoint22_2Input) => Effect.Effect -type Endpoint22_3Request = Parameters[0] -export type Endpoint22_3Input = { - readonly sessionID: Endpoint22_3Request["params"]["sessionID"] - readonly requestID: Endpoint22_3Request["params"]["requestID"] -} -export type Endpoint22_3Output = EffectValue> +export type Endpoint22_3Input = { readonly sessionID: Session.ID; readonly requestID: Question.ID } +export type Endpoint22_3Output = void export type QuestionRejectOperation = (input: Endpoint22_3Input) => Effect.Effect export interface QuestionApi { @@ -967,42 +1454,40 @@ export interface QuestionApi { readonly reject: QuestionRejectOperation } -type Endpoint23_0Request = Parameters[0] -export type Endpoint23_0Input = { readonly location?: Endpoint23_0Request["query"]["location"] } -export type Endpoint23_0Output = EffectValue> +export type Endpoint23_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint23_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type ReferenceListOperation = (input?: Endpoint23_0Input) => Effect.Effect export interface ReferenceApi { readonly list: ReferenceListOperation } -type Endpoint24_0Request = Parameters[0] export type Endpoint24_0Input = { - readonly projectID: Endpoint24_0Request["params"]["projectID"] - readonly location?: Endpoint24_0Request["query"]["location"] - readonly strategy: Endpoint24_0Request["payload"]["strategy"] - readonly directory: Endpoint24_0Request["payload"]["directory"] - readonly name?: Endpoint24_0Request["payload"]["name"] + readonly projectID: Project.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly strategy: ProjectCopy.StrategyID + readonly directory: AbsolutePath + readonly name?: string | undefined } -export type Endpoint24_0Output = EffectValue> +export type Endpoint24_0Output = ProjectCopy.Copy export type ProjectCopyCreateOperation = (input: Endpoint24_0Input) => Effect.Effect -type Endpoint24_1Request = Parameters[0] export type Endpoint24_1Input = { - readonly projectID: Endpoint24_1Request["params"]["projectID"] - readonly location?: Endpoint24_1Request["query"]["location"] - readonly directory: Endpoint24_1Request["payload"]["directory"] - readonly force: Endpoint24_1Request["payload"]["force"] + readonly projectID: Project.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly directory: AbsolutePath + readonly force: boolean } -export type Endpoint24_1Output = EffectValue> +export type Endpoint24_1Output = void export type ProjectCopyRemoveOperation = (input: Endpoint24_1Input) => Effect.Effect -type Endpoint24_2Request = Parameters[0] export type Endpoint24_2Input = { - readonly projectID: Endpoint24_2Request["params"]["projectID"] - readonly location?: Endpoint24_2Request["query"]["location"] + readonly projectID: Project.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint24_2Output = EffectValue> +export type Endpoint24_2Output = void export type ProjectCopyRefreshOperation = (input: Endpoint24_2Input) => Effect.Effect export interface ProjectCopyApi { @@ -1011,18 +1496,18 @@ export interface ProjectCopyApi { readonly refresh: ProjectCopyRefreshOperation } -type Endpoint25_0Request = Parameters[0] -export type Endpoint25_0Input = { readonly location?: Endpoint25_0Request["query"]["location"] } -export type Endpoint25_0Output = EffectValue> +export type Endpoint25_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint25_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type VcsStatusOperation = (input?: Endpoint25_0Input) => Effect.Effect -type Endpoint25_1Request = Parameters[0] export type Endpoint25_1Input = { - readonly location?: Endpoint25_1Request["query"]["location"] - readonly mode: Endpoint25_1Request["query"]["mode"] - readonly context?: Endpoint25_1Request["query"]["context"] + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly mode: Vcs.Mode + readonly context?: number | undefined } -export type Endpoint25_1Output = EffectValue> +export type Endpoint25_1Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type VcsDiffOperation = (input: Endpoint25_1Input) => Effect.Effect export interface VcsApi { @@ -1030,30 +1515,31 @@ export interface VcsApi { readonly diff: VcsDiffOperation } -export type Endpoint26_0Output = EffectValue> +export type Endpoint26_0Output = ReadonlyArray export type DebugLocationListOperation = () => Effect.Effect -type Endpoint26_1Request = Parameters[0] -export type Endpoint26_1Input = { readonly location?: Endpoint26_1Request["query"]["location"] } -export type Endpoint26_1Output = EffectValue> +export type Endpoint26_1Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint26_1Output = void export type DebugLocationEvictOperation = (input?: Endpoint26_1Input) => Effect.Effect export interface DebugApi { readonly location: { readonly list: DebugLocationListOperation; readonly evict: DebugLocationEvictOperation } } -type Endpoint27_0Request = Parameters[0] -export type Endpoint27_0Input = { readonly location?: Endpoint27_0Request["query"]["location"] } -export type Endpoint27_0Output = EffectValue> +export type Endpoint27_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint27_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type WebsearchProvidersOperation = (input?: Endpoint27_0Input) => Effect.Effect -type Endpoint27_1Request = Parameters[0] export type Endpoint27_1Input = { - readonly location?: Endpoint27_1Request["query"]["location"] - readonly query: Endpoint27_1Request["payload"]["query"] - readonly providerID?: Endpoint27_1Request["payload"]["providerID"] + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly query: string + readonly providerID?: WebSearch.ID | undefined } -export type Endpoint27_1Output = EffectValue> +export type Endpoint27_1Output = { readonly location: Location.Info; readonly data: WebSearch.Response } export type WebsearchQueryOperation = (input: Endpoint27_1Input) => Effect.Effect export interface WebsearchApi { diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts index ce791ce4f178..8b457db56fd8 100644 --- a/packages/client/src/effect/generated/client.ts +++ b/packages/client/src/effect/generated/client.ts @@ -4,6 +4,220 @@ import { Sse } from "effect/unstable/encoding" import { HttpClientError } from "effect/unstable/http" import { HttpApiClient } from "effect/unstable/httpapi" import { ClientApi } from "../../contract" +import type { + Endpoint0_0Output, + Endpoint0_1Input, + Endpoint0_1Output, + Endpoint1_0Output, + Endpoint2_0Input, + Endpoint2_0Output, + Endpoint3_0Input, + Endpoint3_0Output, + Endpoint3_1Input, + Endpoint3_1Output, + Endpoint4_0Input, + Endpoint4_0Output, + Endpoint5_0Input, + Endpoint5_0Output, + Endpoint5_1Input, + Endpoint5_1Output, + Endpoint5_2Output, + Endpoint5_3Input, + Endpoint5_3Output, + Endpoint5_4Input, + Endpoint5_4Output, + Endpoint5_5Input, + Endpoint5_5Output, + Endpoint5_6Input, + Endpoint5_6Output, + Endpoint5_7Input, + Endpoint5_7Output, + Endpoint5_8Input, + Endpoint5_8Output, + Endpoint5_9Input, + Endpoint5_9Output, + Endpoint5_10Input, + Endpoint5_10Output, + Endpoint5_11Input, + Endpoint5_11Output, + Endpoint5_12Input, + Endpoint5_12Output, + Endpoint5_13Input, + Endpoint5_13Output, + Endpoint5_14Input, + Endpoint5_14Output, + Endpoint5_15Input, + Endpoint5_15Output, + Endpoint5_16Input, + Endpoint5_16Output, + Endpoint5_17Input, + Endpoint5_17Output, + Endpoint5_18Input, + Endpoint5_18Output, + Endpoint5_19Input, + Endpoint5_19Output, + Endpoint5_20Input, + Endpoint5_20Output, + Endpoint5_21Input, + Endpoint5_21Output, + Endpoint5_22Input, + Endpoint5_22Output, + Endpoint5_23Input, + Endpoint5_23Output, + Endpoint5_24Input, + Endpoint5_24Output, + Endpoint5_25Input, + Endpoint5_25Output, + Endpoint5_26Input, + Endpoint5_26Output, + Endpoint5_27Input, + Endpoint5_27Output, + Endpoint5_28Input, + Endpoint5_28Output, + Endpoint5_29Input, + Endpoint5_29Output, + Endpoint6_0Input, + Endpoint6_0Output, + Endpoint7_0Input, + Endpoint7_0Output, + Endpoint7_1Input, + Endpoint7_1Output, + Endpoint8_0Input, + Endpoint8_0Output, + Endpoint9_0Input, + Endpoint9_0Output, + Endpoint9_1Input, + Endpoint9_1Output, + Endpoint10_0Input, + Endpoint10_0Output, + Endpoint10_1Input, + Endpoint10_1Output, + Endpoint10_2Input, + Endpoint10_2Output, + Endpoint10_3Input, + Endpoint10_3Output, + Endpoint10_4Input, + Endpoint10_4Output, + Endpoint10_5Input, + Endpoint10_5Output, + Endpoint10_6Input, + Endpoint10_6Output, + Endpoint10_7Input, + Endpoint10_7Output, + Endpoint10_8Input, + Endpoint10_8Output, + Endpoint10_9Input, + Endpoint10_9Output, + Endpoint10_10Input, + Endpoint10_10Output, + Endpoint11_0Input, + Endpoint11_0Output, + Endpoint11_1Input, + Endpoint11_1Output, + Endpoint11_2Input, + Endpoint11_2Output, + Endpoint11_3Input, + Endpoint11_3Output, + Endpoint11_4Input, + Endpoint11_4Output, + Endpoint11_5Input, + Endpoint11_5Output, + Endpoint12_0Input, + Endpoint12_0Output, + Endpoint12_1Input, + Endpoint12_1Output, + Endpoint13_0Output, + Endpoint13_1Input, + Endpoint13_1Output, + Endpoint13_2Input, + Endpoint13_2Output, + Endpoint14_0Input, + Endpoint14_0Output, + Endpoint14_1Input, + Endpoint14_1Output, + Endpoint14_2Input, + Endpoint14_2Output, + Endpoint14_3Input, + Endpoint14_3Output, + Endpoint14_4Input, + Endpoint14_4Output, + Endpoint14_5Input, + Endpoint14_5Output, + Endpoint14_6Input, + Endpoint14_6Output, + Endpoint15_0Input, + Endpoint15_0Output, + Endpoint15_1Input, + Endpoint15_1Output, + Endpoint15_2Input, + Endpoint15_2Output, + Endpoint15_3Input, + Endpoint15_3Output, + Endpoint15_4Input, + Endpoint15_4Output, + Endpoint15_5Input, + Endpoint15_5Output, + Endpoint15_6Input, + Endpoint15_6Output, + Endpoint16_0Input, + Endpoint16_0Output, + Endpoint16_1Input, + Endpoint16_1Output, + Endpoint17_0Input, + Endpoint17_0Output, + Endpoint18_0Input, + Endpoint18_0Output, + Endpoint19_0Output, + Endpoint20_0Input, + Endpoint20_0Output, + Endpoint20_1Input, + Endpoint20_1Output, + Endpoint20_2Input, + Endpoint20_2Output, + Endpoint20_3Input, + Endpoint20_3Output, + Endpoint20_4Input, + Endpoint20_4Output, + Endpoint21_0Input, + Endpoint21_0Output, + Endpoint21_1Input, + Endpoint21_1Output, + Endpoint21_2Input, + Endpoint21_2Output, + Endpoint21_3Input, + Endpoint21_3Output, + Endpoint21_4Input, + Endpoint21_4Output, + Endpoint21_5Input, + Endpoint21_5Output, + Endpoint22_0Input, + Endpoint22_0Output, + Endpoint22_1Input, + Endpoint22_1Output, + Endpoint22_2Input, + Endpoint22_2Output, + Endpoint22_3Input, + Endpoint22_3Output, + Endpoint23_0Input, + Endpoint23_0Output, + Endpoint24_0Input, + Endpoint24_0Output, + Endpoint24_1Input, + Endpoint24_1Output, + Endpoint24_2Input, + Endpoint24_2Output, + Endpoint25_0Input, + Endpoint25_0Output, + Endpoint25_1Input, + Endpoint25_1Output, + Endpoint26_0Output, + Endpoint26_1Input, + Endpoint26_1Output, + Endpoint27_0Input, + Endpoint27_0Output, + Endpoint27_1Input, + Endpoint27_1Output, +} from "../api/api.js" import { ClientError } from "./client-error" type RawClient = HttpApiClient.ForApi @@ -13,401 +227,327 @@ const mapClientError = (error: E) => ? new ClientError({ cause: error }) : error +const preserveEffect = + () => + (effect: Effect.Effect) => + effect +const preserveStream = + () => + (stream: Stream.Stream) => + stream + const Endpoint0_0 = (raw: RawClient["server.health"]) => () => - raw["health.get"]({}).pipe(Effect.mapError(mapClientError)) + preserveEffect()(raw["health.get"]({}).pipe(Effect.mapError(mapClientError))) -type Endpoint0_1Request = Parameters[0] -type Endpoint0_1Input = { readonly instanceID: Endpoint0_1Request["payload"]["instanceID"] } const Endpoint0_1 = (raw: RawClient["server.health"]) => (input: Endpoint0_1Input) => - raw["health.stop"]({ payload: { instanceID: input["instanceID"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["health.stop"]({ payload: { instanceID: input["instanceID"] } }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup0 = (raw: RawClient["server.health"]) => ({ get: Endpoint0_0(raw), stop: Endpoint0_1(raw) }) const Endpoint1_0 = (raw: RawClient["server.server"]) => () => - raw["server.get"]({}).pipe(Effect.mapError(mapClientError)) + preserveEffect()(raw["server.get"]({}).pipe(Effect.mapError(mapClientError))) const adaptGroup1 = (raw: RawClient["server.server"]) => ({ get: Endpoint1_0(raw) }) -type Endpoint2_0Request = Parameters[0] -type Endpoint2_0Input = { readonly location?: Endpoint2_0Request["query"]["location"] } const Endpoint2_0 = (raw: RawClient["server.location"]) => (input?: Endpoint2_0Input) => - raw["location.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["location.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup2 = (raw: RawClient["server.location"]) => ({ get: Endpoint2_0(raw) }) -type Endpoint3_0Request = Parameters[0] -type Endpoint3_0Input = { readonly location?: Endpoint3_0Request["query"]["location"] } const Endpoint3_0 = (raw: RawClient["server.agent"]) => (input?: Endpoint3_0Input) => - raw["agent.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["agent.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) + +const Endpoint3_1 = (raw: RawClient["server.agent"]) => (input: Endpoint3_1Input) => + preserveEffect()( + raw["agent.get"]({ params: { agentID: input["agentID"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ), + ) -const adaptGroup3 = (raw: RawClient["server.agent"]) => ({ list: Endpoint3_0(raw) }) +const adaptGroup3 = (raw: RawClient["server.agent"]) => ({ list: Endpoint3_0(raw), get: Endpoint3_1(raw) }) -type Endpoint4_0Request = Parameters[0] -type Endpoint4_0Input = { readonly location?: Endpoint4_0Request["query"]["location"] } const Endpoint4_0 = (raw: RawClient["server.plugin"]) => (input?: Endpoint4_0Input) => - raw["plugin.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["plugin.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup4 = (raw: RawClient["server.plugin"]) => ({ list: Endpoint4_0(raw) }) -type Endpoint5_0Request = Parameters[0] -type Endpoint5_0Input = { - readonly workspace?: Endpoint5_0Request["query"]["workspace"] - readonly limit?: Endpoint5_0Request["query"]["limit"] - readonly order?: Endpoint5_0Request["query"]["order"] - readonly search?: Endpoint5_0Request["query"]["search"] - readonly parentID?: Endpoint5_0Request["query"]["parentID"] - readonly directory?: Endpoint5_0Request["query"]["directory"] - readonly project?: Endpoint5_0Request["query"]["project"] - readonly subpath?: Endpoint5_0Request["query"]["subpath"] - readonly cursor?: Endpoint5_0Request["query"]["cursor"] -} const Endpoint5_0 = (raw: RawClient["server.session"]) => (input?: Endpoint5_0Input) => - raw["session.list"]({ - query: { - workspace: input?.["workspace"], - limit: input?.["limit"], - order: input?.["order"], - search: input?.["search"], - parentID: input?.["parentID"], - directory: input?.["directory"], - project: input?.["project"], - subpath: input?.["subpath"], - cursor: input?.["cursor"], - }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_1Request = Parameters[0] -type Endpoint5_1Input = { - readonly id?: Endpoint5_1Request["payload"]["id"] - readonly agent?: Endpoint5_1Request["payload"]["agent"] - readonly model?: Endpoint5_1Request["payload"]["model"] - readonly location?: Endpoint5_1Request["payload"]["location"] -} + preserveEffect()( + raw["session.list"]({ + query: { + workspace: input?.["workspace"], + limit: input?.["limit"], + order: input?.["order"], + search: input?.["search"], + parentID: input?.["parentID"], + directory: input?.["directory"], + project: input?.["project"], + subpath: input?.["subpath"], + cursor: input?.["cursor"], + }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint5_1 = (raw: RawClient["server.session"]) => (input?: Endpoint5_1Input) => - raw["session.create"]({ - payload: { id: input?.["id"], agent: input?.["agent"], model: input?.["model"], location: input?.["location"] }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.create"]({ + payload: { id: input?.["id"], agent: input?.["agent"], model: input?.["model"], location: input?.["location"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) const Endpoint5_2 = (raw: RawClient["server.session"]) => () => - raw["session.active"]({}).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.active"]({}).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint5_3Request = Parameters[0] -type Endpoint5_3Input = { readonly sessionID: Endpoint5_3Request["params"]["sessionID"] } const Endpoint5_3 = (raw: RawClient["server.session"]) => (input: Endpoint5_3Input) => - raw["session.get"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.get"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint5_4Request = Parameters[0] -type Endpoint5_4Input = { readonly sessionID: Endpoint5_4Request["params"]["sessionID"] } const Endpoint5_4 = (raw: RawClient["server.session"]) => (input: Endpoint5_4Input) => - raw["session.remove"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["session.remove"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint5_5Request = Parameters[0] -type Endpoint5_5Input = { - readonly sessionID: Endpoint5_5Request["params"]["sessionID"] - readonly messageID?: Endpoint5_5Request["payload"]["messageID"] -} const Endpoint5_5 = (raw: RawClient["server.session"]) => (input: Endpoint5_5Input) => - raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { messageID: input["messageID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { messageID: input["messageID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint5_6Request = Parameters[0] -type Endpoint5_6Input = { - readonly sessionID: Endpoint5_6Request["params"]["sessionID"] - readonly agent: Endpoint5_6Request["payload"]["agent"] -} const Endpoint5_6 = (raw: RawClient["server.session"]) => (input: Endpoint5_6Input) => - raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe( + Effect.mapError(mapClientError), + ), ) -type Endpoint5_7Request = Parameters[0] -type Endpoint5_7Input = { - readonly sessionID: Endpoint5_7Request["params"]["sessionID"] - readonly model: Endpoint5_7Request["payload"]["model"] -} const Endpoint5_7 = (raw: RawClient["server.session"]) => (input: Endpoint5_7Input) => - raw["session.switchModel"]({ params: { sessionID: input["sessionID"] }, payload: { model: input["model"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["session.switchModel"]({ params: { sessionID: input["sessionID"] }, payload: { model: input["model"] } }).pipe( + Effect.mapError(mapClientError), + ), ) -type Endpoint5_8Request = Parameters[0] -type Endpoint5_8Input = { - readonly sessionID: Endpoint5_8Request["params"]["sessionID"] - readonly title: Endpoint5_8Request["payload"]["title"] -} const Endpoint5_8 = (raw: RawClient["server.session"]) => (input: Endpoint5_8Input) => - raw["session.rename"]({ params: { sessionID: input["sessionID"] }, payload: { title: input["title"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["session.rename"]({ params: { sessionID: input["sessionID"] }, payload: { title: input["title"] } }).pipe( + Effect.mapError(mapClientError), + ), ) -type Endpoint5_9Request = Parameters[0] -type Endpoint5_9Input = { - readonly sessionID: Endpoint5_9Request["params"]["sessionID"] - readonly directory: Endpoint5_9Request["payload"]["directory"] - readonly workspaceID?: Endpoint5_9Request["payload"]["workspaceID"] -} const Endpoint5_9 = (raw: RawClient["server.session"]) => (input: Endpoint5_9Input) => - raw["session.move"]({ - params: { sessionID: input["sessionID"] }, - payload: { directory: input["directory"], workspaceID: input["workspaceID"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_10Request = Parameters[0] -type Endpoint5_10Input = { - readonly sessionID: Endpoint5_10Request["params"]["sessionID"] - readonly id?: Endpoint5_10Request["payload"]["id"] - readonly text: Endpoint5_10Request["payload"]["text"] - readonly files?: Endpoint5_10Request["payload"]["files"] - readonly agents?: Endpoint5_10Request["payload"]["agents"] - readonly metadata?: Endpoint5_10Request["payload"]["metadata"] - readonly delivery?: Endpoint5_10Request["payload"]["delivery"] - readonly resume?: Endpoint5_10Request["payload"]["resume"] -} + preserveEffect()( + raw["session.move"]({ + params: { sessionID: input["sessionID"] }, + payload: { directory: input["directory"], workspaceID: input["workspaceID"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint5_10 = (raw: RawClient["server.session"]) => (input: Endpoint5_10Input) => - raw["session.prompt"]({ - params: { sessionID: input["sessionID"] }, - payload: { - id: input["id"], - text: input["text"], - files: input["files"], - agents: input["agents"], - metadata: input["metadata"], - delivery: input["delivery"], - resume: input["resume"], - }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_11Request = Parameters[0] -type Endpoint5_11Input = { - readonly sessionID: Endpoint5_11Request["params"]["sessionID"] - readonly id?: Endpoint5_11Request["payload"]["id"] - readonly command: Endpoint5_11Request["payload"]["command"] - readonly arguments?: Endpoint5_11Request["payload"]["arguments"] - readonly agent?: Endpoint5_11Request["payload"]["agent"] - readonly model?: Endpoint5_11Request["payload"]["model"] - readonly files?: Endpoint5_11Request["payload"]["files"] - readonly agents?: Endpoint5_11Request["payload"]["agents"] - readonly delivery?: Endpoint5_11Request["payload"]["delivery"] - readonly resume?: Endpoint5_11Request["payload"]["resume"] -} + preserveEffect()( + raw["session.prompt"]({ + params: { sessionID: input["sessionID"] }, + payload: { + id: input["id"], + text: input["text"], + files: input["files"], + agents: input["agents"], + metadata: input["metadata"], + delivery: input["delivery"], + resume: input["resume"], + }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + const Endpoint5_11 = (raw: RawClient["server.session"]) => (input: Endpoint5_11Input) => - raw["session.command"]({ - params: { sessionID: input["sessionID"] }, - payload: { - id: input["id"], - command: input["command"], - arguments: input["arguments"], - agent: input["agent"], - model: input["model"], - files: input["files"], - agents: input["agents"], - delivery: input["delivery"], - resume: input["resume"], - }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_12Request = Parameters[0] -type Endpoint5_12Input = { - readonly sessionID: Endpoint5_12Request["params"]["sessionID"] - readonly id?: Endpoint5_12Request["payload"]["id"] - readonly skill: Endpoint5_12Request["payload"]["skill"] - readonly resume?: Endpoint5_12Request["payload"]["resume"] -} + preserveEffect()( + raw["session.command"]({ + params: { sessionID: input["sessionID"] }, + payload: { + id: input["id"], + command: input["command"], + arguments: input["arguments"], + agent: input["agent"], + model: input["model"], + files: input["files"], + agents: input["agents"], + delivery: input["delivery"], + resume: input["resume"], + }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + const Endpoint5_12 = (raw: RawClient["server.session"]) => (input: Endpoint5_12Input) => - raw["session.skill"]({ - params: { sessionID: input["sessionID"] }, - payload: { id: input["id"], skill: input["skill"], resume: input["resume"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_13Request = Parameters[0] -type Endpoint5_13Input = { - readonly sessionID: Endpoint5_13Request["params"]["sessionID"] - readonly id?: Endpoint5_13Request["payload"]["id"] - readonly text: Endpoint5_13Request["payload"]["text"] - readonly description?: Endpoint5_13Request["payload"]["description"] - readonly metadata?: Endpoint5_13Request["payload"]["metadata"] - readonly delivery?: Endpoint5_13Request["payload"]["delivery"] - readonly resume?: Endpoint5_13Request["payload"]["resume"] -} + preserveEffect()( + raw["session.skill"]({ + params: { sessionID: input["sessionID"] }, + payload: { id: input["id"], skill: input["skill"], resume: input["resume"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint5_13 = (raw: RawClient["server.session"]) => (input: Endpoint5_13Input) => - raw["session.synthetic"]({ - params: { sessionID: input["sessionID"] }, - payload: { - id: input["id"], - text: input["text"], - description: input["description"], - metadata: input["metadata"], - delivery: input["delivery"], - resume: input["resume"], - }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_14Request = Parameters[0] -type Endpoint5_14Input = { - readonly sessionID: Endpoint5_14Request["params"]["sessionID"] - readonly id?: Endpoint5_14Request["payload"]["id"] - readonly command: Endpoint5_14Request["payload"]["command"] -} + preserveEffect()( + raw["session.synthetic"]({ + params: { sessionID: input["sessionID"] }, + payload: { + id: input["id"], + text: input["text"], + description: input["description"], + metadata: input["metadata"], + delivery: input["delivery"], + resume: input["resume"], + }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + const Endpoint5_14 = (raw: RawClient["server.session"]) => (input: Endpoint5_14Input) => - raw["session.shell"]({ - params: { sessionID: input["sessionID"] }, - payload: { id: input["id"], command: input["command"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_15Request = Parameters[0] -type Endpoint5_15Input = { - readonly sessionID: Endpoint5_15Request["params"]["sessionID"] - readonly id?: Endpoint5_15Request["payload"]["id"] -} + preserveEffect()( + raw["session.shell"]({ + params: { sessionID: input["sessionID"] }, + payload: { id: input["id"], command: input["command"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint5_15 = (raw: RawClient["server.session"]) => (input: Endpoint5_15Input) => - raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint5_16Request = Parameters[0] -type Endpoint5_16Input = { readonly sessionID: Endpoint5_16Request["params"]["sessionID"] } const Endpoint5_16 = (raw: RawClient["server.session"]) => (input: Endpoint5_16Input) => - raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_17Request = Parameters[0] -type Endpoint5_17Input = { - readonly sessionID: Endpoint5_17Request["params"]["sessionID"] - readonly messageID: Endpoint5_17Request["payload"]["messageID"] - readonly files?: Endpoint5_17Request["payload"]["files"] -} + preserveEffect()( + raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint5_17 = (raw: RawClient["server.session"]) => (input: Endpoint5_17Input) => - raw["session.revert.stage"]({ - params: { sessionID: input["sessionID"] }, - payload: { messageID: input["messageID"], files: input["files"] }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.revert.stage"]({ + params: { sessionID: input["sessionID"] }, + payload: { messageID: input["messageID"], files: input["files"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint5_18Request = Parameters[0] -type Endpoint5_18Input = { readonly sessionID: Endpoint5_18Request["params"]["sessionID"] } const Endpoint5_18 = (raw: RawClient["server.session"]) => (input: Endpoint5_18Input) => - raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint5_19Request = Parameters[0] -type Endpoint5_19Input = { readonly sessionID: Endpoint5_19Request["params"]["sessionID"] } const Endpoint5_19 = (raw: RawClient["server.session"]) => (input: Endpoint5_19Input) => - raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint5_20Request = Parameters[0] -type Endpoint5_20Input = { readonly sessionID: Endpoint5_20Request["params"]["sessionID"] } const Endpoint5_20 = (raw: RawClient["server.session"]) => (input: Endpoint5_20Input) => - raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint5_21Request = Parameters[0] -type Endpoint5_21Input = { readonly sessionID: Endpoint5_21Request["params"]["sessionID"] } const Endpoint5_21 = (raw: RawClient["server.session"]) => (input: Endpoint5_21Input) => - raw["session.pending.list"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.pending.list"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint5_22Request = Parameters[0] -type Endpoint5_22Input = { readonly sessionID: Endpoint5_22Request["params"]["sessionID"] } const Endpoint5_22 = (raw: RawClient["server.session"]) => (input: Endpoint5_22Input) => - raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint5_23Request = Parameters[0] -type Endpoint5_23Input = { - readonly sessionID: Endpoint5_23Request["params"]["sessionID"] - readonly key: Endpoint5_23Request["params"]["key"] - readonly value: Endpoint5_23Request["payload"]["value"] -} const Endpoint5_23 = (raw: RawClient["server.session"]) => (input: Endpoint5_23Input) => - raw["session.instructions.entry.put"]({ - params: { sessionID: input["sessionID"], key: input["key"] }, - payload: { value: input["value"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_24Request = Parameters[0] -type Endpoint5_24Input = { - readonly sessionID: Endpoint5_24Request["params"]["sessionID"] - readonly key: Endpoint5_24Request["params"]["key"] -} + preserveEffect()( + raw["session.instructions.entry.put"]({ + params: { sessionID: input["sessionID"], key: input["key"] }, + payload: { value: input["value"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) => - raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe( + Effect.mapError(mapClientError), + ), ) -type Endpoint5_25Request = Parameters[0] -type Endpoint5_25Input = { - readonly sessionID: Endpoint5_25Request["params"]["sessionID"] - readonly prompt: Endpoint5_25Request["payload"]["prompt"] -} const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) => - raw["session.generate"]({ params: { sessionID: input["sessionID"] }, payload: { prompt: input["prompt"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.generate"]({ params: { sessionID: input["sessionID"] }, payload: { prompt: input["prompt"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint5_26Request = Parameters[0] -type Endpoint5_26Input = { - readonly sessionID: Endpoint5_26Request["params"]["sessionID"] - readonly after?: Endpoint5_26Request["query"]["after"] - readonly follow?: Endpoint5_26Request["query"]["follow"] -} const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) => - Stream.unwrap( - raw["session.log"]({ - params: { sessionID: input["sessionID"] }, - query: { after: input["after"], follow: input["follow"] }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))), + preserveStream()( + Stream.unwrap( + raw["session.log"]({ + params: { sessionID: input["sessionID"] }, + query: { after: input["after"], follow: input["follow"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))), + ), ), ) -type Endpoint5_27Request = Parameters[0] -type Endpoint5_27Input = { readonly sessionID: Endpoint5_27Request["params"]["sessionID"] } const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) => - raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint5_28Request = Parameters[0] -type Endpoint5_28Input = { readonly sessionID: Endpoint5_28Request["params"]["sessionID"] } const Endpoint5_28 = (raw: RawClient["server.session"]) => (input: Endpoint5_28Input) => - raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint5_29Request = Parameters[0] -type Endpoint5_29Input = { - readonly sessionID: Endpoint5_29Request["params"]["sessionID"] - readonly messageID: Endpoint5_29Request["params"]["messageID"] -} const Endpoint5_29 = (raw: RawClient["server.session"]) => (input: Endpoint5_29Input) => - raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) const adaptGroup5 = (raw: RawClient["server.session"]) => ({ @@ -439,197 +579,142 @@ const adaptGroup5 = (raw: RawClient["server.session"]) => ({ message: Endpoint5_29(raw), }) -type Endpoint6_0Request = Parameters[0] -type Endpoint6_0Input = { - readonly sessionID: Endpoint6_0Request["params"]["sessionID"] - readonly limit?: Endpoint6_0Request["query"]["limit"] - readonly order?: Endpoint6_0Request["query"]["order"] - readonly cursor?: Endpoint6_0Request["query"]["cursor"] -} const Endpoint6_0 = (raw: RawClient["server.message"]) => (input: Endpoint6_0Input) => - raw["session.messages"]({ - params: { sessionID: input["sessionID"] }, - query: { limit: input["limit"], order: input["order"], cursor: input["cursor"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["session.messages"]({ + params: { sessionID: input["sessionID"] }, + query: { limit: input["limit"], order: input["order"], cursor: input["cursor"] }, + }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup6 = (raw: RawClient["server.message"]) => ({ list: Endpoint6_0(raw) }) -type Endpoint7_0Request = Parameters[0] -type Endpoint7_0Input = { readonly location?: Endpoint7_0Request["query"]["location"] } const Endpoint7_0 = (raw: RawClient["server.model"]) => (input?: Endpoint7_0Input) => - raw["model.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["model.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint7_1Request = Parameters[0] -type Endpoint7_1Input = { readonly location?: Endpoint7_1Request["query"]["location"] } const Endpoint7_1 = (raw: RawClient["server.model"]) => (input?: Endpoint7_1Input) => - raw["model.default"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["model.default"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup7 = (raw: RawClient["server.model"]) => ({ list: Endpoint7_0(raw), default: Endpoint7_1(raw) }) -type Endpoint8_0Request = Parameters[0] -type Endpoint8_0Input = { - readonly location?: Endpoint8_0Request["query"]["location"] - readonly prompt: Endpoint8_0Request["payload"]["prompt"] - readonly model?: Endpoint8_0Request["payload"]["model"] -} const Endpoint8_0 = (raw: RawClient["server.generate"]) => (input: Endpoint8_0Input) => - raw["generate.text"]({ - query: { location: input["location"] }, - payload: { prompt: input["prompt"], model: input["model"] }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["generate.text"]({ + query: { location: input["location"] }, + payload: { prompt: input["prompt"], model: input["model"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) const adaptGroup8 = (raw: RawClient["server.generate"]) => ({ text: Endpoint8_0(raw) }) -type Endpoint9_0Request = Parameters[0] -type Endpoint9_0Input = { readonly location?: Endpoint9_0Request["query"]["location"] } const Endpoint9_0 = (raw: RawClient["server.provider"]) => (input?: Endpoint9_0Input) => - raw["provider.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["provider.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint9_1Request = Parameters[0] -type Endpoint9_1Input = { - readonly providerID: Endpoint9_1Request["params"]["providerID"] - readonly location?: Endpoint9_1Request["query"]["location"] -} const Endpoint9_1 = (raw: RawClient["server.provider"]) => (input: Endpoint9_1Input) => - raw["provider.get"]({ params: { providerID: input["providerID"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["provider.get"]({ params: { providerID: input["providerID"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ), ) const adaptGroup9 = (raw: RawClient["server.provider"]) => ({ list: Endpoint9_0(raw), get: Endpoint9_1(raw) }) -type Endpoint10_0Request = Parameters[0] -type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["location"] } const Endpoint10_0 = (raw: RawClient["server.integration"]) => (input?: Endpoint10_0Input) => - raw["integration.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["integration.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint10_1Request = Parameters[0] -type Endpoint10_1Input = { - readonly integrationID: Endpoint10_1Request["params"]["integrationID"] - readonly location?: Endpoint10_1Request["query"]["location"] -} const Endpoint10_1 = (raw: RawClient["server.integration"]) => (input: Endpoint10_1Input) => - raw["integration.get"]({ - params: { integrationID: input["integrationID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint10_2Request = Parameters[0] -type Endpoint10_2Input = { - readonly location?: Endpoint10_2Request["query"]["location"] - readonly url: Endpoint10_2Request["payload"]["url"] -} + preserveEffect()( + raw["integration.get"]({ + params: { integrationID: input["integrationID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint10_2 = (raw: RawClient["server.integration"]) => (input: Endpoint10_2Input) => - raw["integration.wellknown.add"]({ query: { location: input["location"] }, payload: { url: input["url"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["integration.wellknown.add"]({ query: { location: input["location"] }, payload: { url: input["url"] } }).pipe( + Effect.mapError(mapClientError), + ), ) -type Endpoint10_3Request = Parameters[0] -type Endpoint10_3Input = { - readonly integrationID: Endpoint10_3Request["params"]["integrationID"] - readonly location?: Endpoint10_3Request["query"]["location"] - readonly key: Endpoint10_3Request["payload"]["key"] - readonly label?: Endpoint10_3Request["payload"]["label"] -} const Endpoint10_3 = (raw: RawClient["server.integration"]) => (input: Endpoint10_3Input) => - raw["integration.connect.key"]({ - params: { integrationID: input["integrationID"] }, - query: { location: input["location"] }, - payload: { key: input["key"], label: input["label"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint10_4Request = Parameters[0] -type Endpoint10_4Input = { - readonly integrationID: Endpoint10_4Request["params"]["integrationID"] - readonly location?: Endpoint10_4Request["query"]["location"] - readonly methodID: Endpoint10_4Request["payload"]["methodID"] - readonly inputs: Endpoint10_4Request["payload"]["inputs"] - readonly label?: Endpoint10_4Request["payload"]["label"] -} + preserveEffect()( + raw["integration.connect.key"]({ + params: { integrationID: input["integrationID"] }, + query: { location: input["location"] }, + payload: { key: input["key"], label: input["label"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint10_4 = (raw: RawClient["server.integration"]) => (input: Endpoint10_4Input) => - raw["integration.oauth.connect"]({ - params: { integrationID: input["integrationID"] }, - query: { location: input["location"] }, - payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint10_5Request = Parameters[0] -type Endpoint10_5Input = { - readonly integrationID: Endpoint10_5Request["params"]["integrationID"] - readonly attemptID: Endpoint10_5Request["params"]["attemptID"] - readonly location?: Endpoint10_5Request["query"]["location"] -} + preserveEffect()( + raw["integration.oauth.connect"]({ + params: { integrationID: input["integrationID"] }, + query: { location: input["location"] }, + payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint10_5 = (raw: RawClient["server.integration"]) => (input: Endpoint10_5Input) => - raw["integration.oauth.status"]({ - params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint10_6Request = Parameters[0] -type Endpoint10_6Input = { - readonly integrationID: Endpoint10_6Request["params"]["integrationID"] - readonly attemptID: Endpoint10_6Request["params"]["attemptID"] - readonly location?: Endpoint10_6Request["query"]["location"] - readonly code?: Endpoint10_6Request["payload"]["code"] -} + preserveEffect()( + raw["integration.oauth.status"]({ + params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint10_6 = (raw: RawClient["server.integration"]) => (input: Endpoint10_6Input) => - raw["integration.oauth.complete"]({ - params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, - query: { location: input["location"] }, - payload: { code: input["code"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint10_7Request = Parameters[0] -type Endpoint10_7Input = { - readonly integrationID: Endpoint10_7Request["params"]["integrationID"] - readonly attemptID: Endpoint10_7Request["params"]["attemptID"] - readonly location?: Endpoint10_7Request["query"]["location"] -} + preserveEffect()( + raw["integration.oauth.complete"]({ + params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, + query: { location: input["location"] }, + payload: { code: input["code"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint10_7 = (raw: RawClient["server.integration"]) => (input: Endpoint10_7Input) => - raw["integration.oauth.cancel"]({ - params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint10_8Request = Parameters[0] -type Endpoint10_8Input = { - readonly integrationID: Endpoint10_8Request["params"]["integrationID"] - readonly location?: Endpoint10_8Request["query"]["location"] - readonly methodID: Endpoint10_8Request["payload"]["methodID"] - readonly label?: Endpoint10_8Request["payload"]["label"] -} + preserveEffect()( + raw["integration.oauth.cancel"]({ + params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint10_8 = (raw: RawClient["server.integration"]) => (input: Endpoint10_8Input) => - raw["integration.command.connect"]({ - params: { integrationID: input["integrationID"] }, - query: { location: input["location"] }, - payload: { methodID: input["methodID"], label: input["label"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint10_9Request = Parameters[0] -type Endpoint10_9Input = { - readonly integrationID: Endpoint10_9Request["params"]["integrationID"] - readonly attemptID: Endpoint10_9Request["params"]["attemptID"] - readonly location?: Endpoint10_9Request["query"]["location"] -} + preserveEffect()( + raw["integration.command.connect"]({ + params: { integrationID: input["integrationID"] }, + query: { location: input["location"] }, + payload: { methodID: input["methodID"], label: input["label"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint10_9 = (raw: RawClient["server.integration"]) => (input: Endpoint10_9Input) => - raw["integration.command.status"]({ - params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint10_10Request = Parameters[0] -type Endpoint10_10Input = { - readonly integrationID: Endpoint10_10Request["params"]["integrationID"] - readonly attemptID: Endpoint10_10Request["params"]["attemptID"] - readonly location?: Endpoint10_10Request["query"]["location"] -} + preserveEffect()( + raw["integration.command.status"]({ + params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint10_10 = (raw: RawClient["server.integration"]) => (input: Endpoint10_10Input) => - raw["integration.command.cancel"]({ - params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["integration.command.cancel"]({ + params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup10 = (raw: RawClient["server.integration"]) => ({ list: Endpoint10_0(raw), @@ -645,58 +730,45 @@ const adaptGroup10 = (raw: RawClient["server.integration"]) => ({ command: { connect: Endpoint10_8(raw), status: Endpoint10_9(raw), cancel: Endpoint10_10(raw) }, }) -type Endpoint11_0Request = Parameters[0] -type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] } const Endpoint11_0 = (raw: RawClient["server.mcp"]) => (input?: Endpoint11_0Input) => - raw["mcp.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint11_1Request = Parameters[0] -type Endpoint11_1Input = { - readonly server: Endpoint11_1Request["params"]["server"] - readonly location?: Endpoint11_1Request["query"]["location"] - readonly config: Endpoint11_1Request["payload"]["config"] -} + preserveEffect()( + raw["mcp.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint11_1 = (raw: RawClient["server.mcp"]) => (input: Endpoint11_1Input) => - raw["mcp.add"]({ - params: { server: input["server"] }, - query: { location: input["location"] }, - payload: { config: input["config"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint11_2Request = Parameters[0] -type Endpoint11_2Input = { - readonly server: Endpoint11_2Request["params"]["server"] - readonly location?: Endpoint11_2Request["query"]["location"] -} + preserveEffect()( + raw["mcp.add"]({ + params: { server: input["server"] }, + query: { location: input["location"] }, + payload: { config: input["config"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint11_2 = (raw: RawClient["server.mcp"]) => (input: Endpoint11_2Input) => - raw["mcp.remove"]({ params: { server: input["server"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["mcp.remove"]({ params: { server: input["server"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ), ) -type Endpoint11_3Request = Parameters[0] -type Endpoint11_3Input = { - readonly server: Endpoint11_3Request["params"]["server"] - readonly location?: Endpoint11_3Request["query"]["location"] -} const Endpoint11_3 = (raw: RawClient["server.mcp"]) => (input: Endpoint11_3Input) => - raw["mcp.connect"]({ params: { server: input["server"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["mcp.connect"]({ params: { server: input["server"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ), ) -type Endpoint11_4Request = Parameters[0] -type Endpoint11_4Input = { - readonly server: Endpoint11_4Request["params"]["server"] - readonly location?: Endpoint11_4Request["query"]["location"] -} const Endpoint11_4 = (raw: RawClient["server.mcp"]) => (input: Endpoint11_4Input) => - raw["mcp.disconnect"]({ params: { server: input["server"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["mcp.disconnect"]({ params: { server: input["server"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ), ) -type Endpoint11_5Request = Parameters[0] -type Endpoint11_5Input = { readonly location?: Endpoint11_5Request["query"]["location"] } const Endpoint11_5 = (raw: RawClient["server.mcp"]) => (input?: Endpoint11_5Input) => - raw["mcp.resource.catalog"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["mcp.resource.catalog"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup11 = (raw: RawClient["server.mcp"]) => ({ list: Endpoint11_0(raw), @@ -707,50 +779,40 @@ const adaptGroup11 = (raw: RawClient["server.mcp"]) => ({ resource: { catalog: Endpoint11_5(raw) }, }) -type Endpoint12_0Request = Parameters[0] -type Endpoint12_0Input = { - readonly credentialID: Endpoint12_0Request["params"]["credentialID"] - readonly location?: Endpoint12_0Request["query"]["location"] - readonly label: Endpoint12_0Request["payload"]["label"] -} const Endpoint12_0 = (raw: RawClient["server.credential"]) => (input: Endpoint12_0Input) => - raw["credential.update"]({ - params: { credentialID: input["credentialID"] }, - query: { location: input["location"] }, - payload: { label: input["label"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint12_1Request = Parameters[0] -type Endpoint12_1Input = { - readonly credentialID: Endpoint12_1Request["params"]["credentialID"] - readonly location?: Endpoint12_1Request["query"]["location"] -} + preserveEffect()( + raw["credential.update"]({ + params: { credentialID: input["credentialID"] }, + query: { location: input["location"] }, + payload: { label: input["label"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint12_1 = (raw: RawClient["server.credential"]) => (input: Endpoint12_1Input) => - raw["credential.remove"]({ - params: { credentialID: input["credentialID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["credential.remove"]({ + params: { credentialID: input["credentialID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup12 = (raw: RawClient["server.credential"]) => ({ update: Endpoint12_0(raw), remove: Endpoint12_1(raw) }) const Endpoint13_0 = (raw: RawClient["server.project"]) => () => - raw["project.list"]({}).pipe(Effect.mapError(mapClientError)) + preserveEffect()(raw["project.list"]({}).pipe(Effect.mapError(mapClientError))) -type Endpoint13_1Request = Parameters[0] -type Endpoint13_1Input = { readonly location?: Endpoint13_1Request["query"]["location"] } const Endpoint13_1 = (raw: RawClient["server.project"]) => (input?: Endpoint13_1Input) => - raw["project.current"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["project.current"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint13_2Request = Parameters[0] -type Endpoint13_2Input = { - readonly projectID: Endpoint13_2Request["params"]["projectID"] - readonly location?: Endpoint13_2Request["query"]["location"] -} const Endpoint13_2 = (raw: RawClient["server.project"]) => (input: Endpoint13_2Input) => - raw["project.directories"]({ - params: { projectID: input["projectID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["project.directories"]({ + params: { projectID: input["projectID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup13 = (raw: RawClient["server.project"]) => ({ list: Endpoint13_0(raw), @@ -758,78 +820,59 @@ const adaptGroup13 = (raw: RawClient["server.project"]) => ({ directories: Endpoint13_2(raw), }) -type Endpoint14_0Request = Parameters[0] -type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] } const Endpoint14_0 = (raw: RawClient["server.form"]) => (input?: Endpoint14_0Input) => - raw["form.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["form.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint14_1Request = Parameters[0] -type Endpoint14_1Input = { readonly sessionID: Endpoint14_1Request["params"]["sessionID"] } const Endpoint14_1 = (raw: RawClient["server.form"]) => (input: Endpoint14_1Input) => - raw["session.form.list"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint14_2Request = Parameters[0] -type Endpoint14_2Input = { - readonly sessionID: Endpoint14_2Request["params"]["sessionID"] - readonly id?: Endpoint14_2Request["payload"]["id"] - readonly title: Endpoint14_2Request["payload"]["title"] - readonly metadata?: Endpoint14_2Request["payload"]["metadata"] - readonly fields: Endpoint14_2Request["payload"]["fields"] -} + preserveEffect()( + raw["session.form.list"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + const Endpoint14_2 = (raw: RawClient["server.form"]) => (input: Endpoint14_2Input) => - raw["session.form.create"]({ - params: { sessionID: input["sessionID"] }, - payload: { id: input["id"], title: input["title"], metadata: input["metadata"], fields: input["fields"] }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint14_3Request = Parameters[0] -type Endpoint14_3Input = { - readonly sessionID: Endpoint14_3Request["params"]["sessionID"] - readonly formID: Endpoint14_3Request["params"]["formID"] -} + preserveEffect()( + raw["session.form.create"]({ + params: { sessionID: input["sessionID"] }, + payload: { id: input["id"], title: input["title"], metadata: input["metadata"], fields: input["fields"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + const Endpoint14_3 = (raw: RawClient["server.form"]) => (input: Endpoint14_3Input) => - raw["session.form.get"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.form.get"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint14_4Request = Parameters[0] -type Endpoint14_4Input = { - readonly sessionID: Endpoint14_4Request["params"]["sessionID"] - readonly formID: Endpoint14_4Request["params"]["formID"] -} const Endpoint14_4 = (raw: RawClient["server.form"]) => (input: Endpoint14_4Input) => - raw["session.form.state"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.form.state"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint14_5Request = Parameters[0] -type Endpoint14_5Input = { - readonly sessionID: Endpoint14_5Request["params"]["sessionID"] - readonly formID: Endpoint14_5Request["params"]["formID"] - readonly answer: Endpoint14_5Request["payload"]["answer"] -} const Endpoint14_5 = (raw: RawClient["server.form"]) => (input: Endpoint14_5Input) => - raw["session.form.reply"]({ - params: { sessionID: input["sessionID"], formID: input["formID"] }, - payload: { answer: input["answer"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint14_6Request = Parameters[0] -type Endpoint14_6Input = { - readonly sessionID: Endpoint14_6Request["params"]["sessionID"] - readonly formID: Endpoint14_6Request["params"]["formID"] -} + preserveEffect()( + raw["session.form.reply"]({ + params: { sessionID: input["sessionID"], formID: input["formID"] }, + payload: { answer: input["answer"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint14_6 = (raw: RawClient["server.form"]) => (input: Endpoint14_6Input) => - raw["session.form.cancel"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["session.form.cancel"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( + Effect.mapError(mapClientError), + ), ) const adaptGroup14 = (raw: RawClient["server.form"]) => ({ @@ -842,83 +885,66 @@ const adaptGroup14 = (raw: RawClient["server.form"]) => ({ cancel: Endpoint14_6(raw), }) -type Endpoint15_0Request = Parameters[0] -type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] } const Endpoint15_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint15_0Input) => - raw["permission.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["permission.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint15_1Request = Parameters[0] -type Endpoint15_1Input = { readonly projectID?: Endpoint15_1Request["query"]["projectID"] } const Endpoint15_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint15_1Input) => - raw["permission.saved.list"]({ query: { projectID: input?.["projectID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["permission.saved.list"]({ query: { projectID: input?.["projectID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint15_2Request = Parameters[0] -type Endpoint15_2Input = { readonly id: Endpoint15_2Request["params"]["id"] } const Endpoint15_2 = (raw: RawClient["server.permission"]) => (input: Endpoint15_2Input) => - raw["permission.saved.remove"]({ params: { id: input["id"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint15_3Request = Parameters[0] -type Endpoint15_3Input = { - readonly sessionID: Endpoint15_3Request["params"]["sessionID"] - readonly id?: Endpoint15_3Request["payload"]["id"] - readonly action: Endpoint15_3Request["payload"]["action"] - readonly resources: Endpoint15_3Request["payload"]["resources"] - readonly save?: Endpoint15_3Request["payload"]["save"] - readonly metadata?: Endpoint15_3Request["payload"]["metadata"] - readonly source?: Endpoint15_3Request["payload"]["source"] - readonly agent?: Endpoint15_3Request["payload"]["agent"] -} + preserveEffect()( + raw["permission.saved.remove"]({ params: { id: input["id"] } }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint15_3 = (raw: RawClient["server.permission"]) => (input: Endpoint15_3Input) => - raw["session.permission.create"]({ - params: { sessionID: input["sessionID"] }, - payload: { - id: input["id"], - action: input["action"], - resources: input["resources"], - save: input["save"], - metadata: input["metadata"], - source: input["source"], - agent: input["agent"], - }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint15_4Request = Parameters[0] -type Endpoint15_4Input = { readonly sessionID: Endpoint15_4Request["params"]["sessionID"] } + preserveEffect()( + raw["session.permission.create"]({ + params: { sessionID: input["sessionID"] }, + payload: { + id: input["id"], + action: input["action"], + resources: input["resources"], + save: input["save"], + metadata: input["metadata"], + source: input["source"], + agent: input["agent"], + }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + const Endpoint15_4 = (raw: RawClient["server.permission"]) => (input: Endpoint15_4Input) => - raw["session.permission.list"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.permission.list"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint15_5Request = Parameters[0] -type Endpoint15_5Input = { - readonly sessionID: Endpoint15_5Request["params"]["sessionID"] - readonly requestID: Endpoint15_5Request["params"]["requestID"] -} const Endpoint15_5 = (raw: RawClient["server.permission"]) => (input: Endpoint15_5Input) => - raw["session.permission.get"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint15_6Request = Parameters[0] -type Endpoint15_6Input = { - readonly sessionID: Endpoint15_6Request["params"]["sessionID"] - readonly requestID: Endpoint15_6Request["params"]["requestID"] - readonly reply: Endpoint15_6Request["payload"]["reply"] - readonly message?: Endpoint15_6Request["payload"]["message"] -} + preserveEffect()( + raw["session.permission.get"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + const Endpoint15_6 = (raw: RawClient["server.permission"]) => (input: Endpoint15_6Input) => - raw["session.permission.reply"]({ - params: { sessionID: input["sessionID"], requestID: input["requestID"] }, - payload: { reply: input["reply"], message: input["message"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["session.permission.reply"]({ + params: { sessionID: input["sessionID"], requestID: input["requestID"] }, + payload: { reply: input["reply"], message: input["message"] }, + }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup15 = (raw: RawClient["server.permission"]) => ({ request: { list: Endpoint15_0(raw) }, @@ -929,112 +955,88 @@ const adaptGroup15 = (raw: RawClient["server.permission"]) => ({ reply: Endpoint15_6(raw), }) -type Endpoint16_0Request = Parameters[0] -type Endpoint16_0Input = { - readonly location?: Endpoint16_0Request["query"]["location"] - readonly path?: Endpoint16_0Request["query"]["path"] -} const Endpoint16_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint16_0Input) => - raw["fs.list"]({ query: { location: input?.["location"], path: input?.["path"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["fs.list"]({ query: { location: input?.["location"], path: input?.["path"] } }).pipe( + Effect.mapError(mapClientError), + ), ) -type Endpoint16_1Request = Parameters[0] -type Endpoint16_1Input = { - readonly location?: Endpoint16_1Request["query"]["location"] - readonly query: Endpoint16_1Request["query"]["query"] - readonly type?: Endpoint16_1Request["query"]["type"] - readonly limit?: Endpoint16_1Request["query"]["limit"] -} const Endpoint16_1 = (raw: RawClient["server.fs"]) => (input: Endpoint16_1Input) => - raw["fs.find"]({ - query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["fs.find"]({ + query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] }, + }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup16 = (raw: RawClient["server.fs"]) => ({ list: Endpoint16_0(raw), find: Endpoint16_1(raw) }) -type Endpoint17_0Request = Parameters[0] -type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] } const Endpoint17_0 = (raw: RawClient["server.command"]) => (input?: Endpoint17_0Input) => - raw["command.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["command.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup17 = (raw: RawClient["server.command"]) => ({ list: Endpoint17_0(raw) }) -type Endpoint18_0Request = Parameters[0] -type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] } const Endpoint18_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint18_0Input) => - raw["skill.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["skill.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup18 = (raw: RawClient["server.skill"]) => ({ list: Endpoint18_0(raw) }) const Endpoint19_0 = (raw: RawClient["server.event"]) => () => - Stream.unwrap( - raw["event.subscribe"]({}).pipe( - Effect.mapError(mapClientError), - Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))), + preserveStream()( + Stream.unwrap( + raw["event.subscribe"]({}).pipe( + Effect.mapError(mapClientError), + Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))), + ), ), ) const adaptGroup19 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint19_0(raw) }) -type Endpoint20_0Request = Parameters[0] -type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] } const Endpoint20_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint20_0Input) => - raw["pty.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint20_1Request = Parameters[0] -type Endpoint20_1Input = { - readonly location?: Endpoint20_1Request["query"]["location"] - readonly command?: Endpoint20_1Request["payload"]["command"] - readonly args?: Endpoint20_1Request["payload"]["args"] - readonly cwd?: Endpoint20_1Request["payload"]["cwd"] - readonly title?: Endpoint20_1Request["payload"]["title"] - readonly env?: Endpoint20_1Request["payload"]["env"] -} + preserveEffect()( + raw["pty.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint20_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint20_1Input) => - raw["pty.create"]({ - query: { location: input?.["location"] }, - payload: { - command: input?.["command"], - args: input?.["args"], - cwd: input?.["cwd"], - title: input?.["title"], - env: input?.["env"], - }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint20_2Request = Parameters[0] -type Endpoint20_2Input = { - readonly ptyID: Endpoint20_2Request["params"]["ptyID"] - readonly location?: Endpoint20_2Request["query"]["location"] -} + preserveEffect()( + raw["pty.create"]({ + query: { location: input?.["location"] }, + payload: { + command: input?.["command"], + args: input?.["args"], + cwd: input?.["cwd"], + title: input?.["title"], + env: input?.["env"], + }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint20_2 = (raw: RawClient["server.pty"]) => (input: Endpoint20_2Input) => - raw["pty.get"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["pty.get"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ), ) -type Endpoint20_3Request = Parameters[0] -type Endpoint20_3Input = { - readonly ptyID: Endpoint20_3Request["params"]["ptyID"] - readonly location?: Endpoint20_3Request["query"]["location"] - readonly title?: Endpoint20_3Request["payload"]["title"] - readonly size?: Endpoint20_3Request["payload"]["size"] -} const Endpoint20_3 = (raw: RawClient["server.pty"]) => (input: Endpoint20_3Input) => - raw["pty.update"]({ - params: { ptyID: input["ptyID"] }, - query: { location: input["location"] }, - payload: { title: input["title"], size: input["size"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint20_4Request = Parameters[0] -type Endpoint20_4Input = { - readonly ptyID: Endpoint20_4Request["params"]["ptyID"] - readonly location?: Endpoint20_4Request["query"]["location"] -} + preserveEffect()( + raw["pty.update"]({ + params: { ptyID: input["ptyID"] }, + query: { location: input["location"] }, + payload: { title: input["title"], size: input["size"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint20_4 = (raw: RawClient["server.pty"]) => (input: Endpoint20_4Input) => - raw["pty.remove"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["pty.remove"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ), ) const adaptGroup20 = (raw: RawClient["server.pty"]) => ({ @@ -1045,69 +1047,48 @@ const adaptGroup20 = (raw: RawClient["server.pty"]) => ({ remove: Endpoint20_4(raw), }) -type Endpoint21_0Request = Parameters[0] -type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] } const Endpoint21_0 = (raw: RawClient["server.shell"]) => (input?: Endpoint21_0Input) => - raw["shell.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint21_1Request = Parameters[0] -type Endpoint21_1Input = { - readonly location?: Endpoint21_1Request["query"]["location"] - readonly command: Endpoint21_1Request["payload"]["command"] - readonly cwd?: Endpoint21_1Request["payload"]["cwd"] - readonly timeout: Endpoint21_1Request["payload"]["timeout"] - readonly metadata?: Endpoint21_1Request["payload"]["metadata"] -} + preserveEffect()( + raw["shell.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint21_1 = (raw: RawClient["server.shell"]) => (input: Endpoint21_1Input) => - raw["shell.create"]({ - query: { location: input["location"] }, - payload: { command: input["command"], cwd: input["cwd"], timeout: input["timeout"], metadata: input["metadata"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint21_2Request = Parameters[0] -type Endpoint21_2Input = { - readonly id: Endpoint21_2Request["params"]["id"] - readonly location?: Endpoint21_2Request["query"]["location"] -} + preserveEffect()( + raw["shell.create"]({ + query: { location: input["location"] }, + payload: { command: input["command"], cwd: input["cwd"], timeout: input["timeout"], metadata: input["metadata"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint21_2 = (raw: RawClient["server.shell"]) => (input: Endpoint21_2Input) => - raw["shell.get"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["shell.get"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ), ) -type Endpoint21_3Request = Parameters[0] -type Endpoint21_3Input = { - readonly id: Endpoint21_3Request["params"]["id"] - readonly location?: Endpoint21_3Request["query"]["location"] - readonly timeout: Endpoint21_3Request["payload"]["timeout"] -} const Endpoint21_3 = (raw: RawClient["server.shell"]) => (input: Endpoint21_3Input) => - raw["shell.timeout"]({ - params: { id: input["id"] }, - query: { location: input["location"] }, - payload: { timeout: input["timeout"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint21_4Request = Parameters[0] -type Endpoint21_4Input = { - readonly id: Endpoint21_4Request["params"]["id"] - readonly location?: Endpoint21_4Request["query"]["location"] - readonly cursor?: Endpoint21_4Request["query"]["cursor"] - readonly limit?: Endpoint21_4Request["query"]["limit"] -} + preserveEffect()( + raw["shell.timeout"]({ + params: { id: input["id"] }, + query: { location: input["location"] }, + payload: { timeout: input["timeout"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint21_4 = (raw: RawClient["server.shell"]) => (input: Endpoint21_4Input) => - raw["shell.output"]({ - params: { id: input["id"] }, - query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint21_5Request = Parameters[0] -type Endpoint21_5Input = { - readonly id: Endpoint21_5Request["params"]["id"] - readonly location?: Endpoint21_5Request["query"]["location"] -} + preserveEffect()( + raw["shell.output"]({ + params: { id: input["id"] }, + query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint21_5 = (raw: RawClient["server.shell"]) => (input: Endpoint21_5Input) => - raw["shell.remove"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["shell.remove"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ), ) const adaptGroup21 = (raw: RawClient["server.shell"]) => ({ @@ -1119,39 +1100,32 @@ const adaptGroup21 = (raw: RawClient["server.shell"]) => ({ remove: Endpoint21_5(raw), }) -type Endpoint22_0Request = Parameters[0] -type Endpoint22_0Input = { readonly location?: Endpoint22_0Request["query"]["location"] } const Endpoint22_0 = (raw: RawClient["server.question"]) => (input?: Endpoint22_0Input) => - raw["question.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["question.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint22_1Request = Parameters[0] -type Endpoint22_1Input = { readonly sessionID: Endpoint22_1Request["params"]["sessionID"] } const Endpoint22_1 = (raw: RawClient["server.question"]) => (input: Endpoint22_1Input) => - raw["session.question.list"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.question.list"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint22_2Request = Parameters[0] -type Endpoint22_2Input = { - readonly sessionID: Endpoint22_2Request["params"]["sessionID"] - readonly requestID: Endpoint22_2Request["params"]["requestID"] - readonly answers: Endpoint22_2Request["payload"]["answers"] -} const Endpoint22_2 = (raw: RawClient["server.question"]) => (input: Endpoint22_2Input) => - raw["session.question.reply"]({ - params: { sessionID: input["sessionID"], requestID: input["requestID"] }, - payload: { answers: input["answers"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint22_3Request = Parameters[0] -type Endpoint22_3Input = { - readonly sessionID: Endpoint22_3Request["params"]["sessionID"] - readonly requestID: Endpoint22_3Request["params"]["requestID"] -} + preserveEffect()( + raw["session.question.reply"]({ + params: { sessionID: input["sessionID"], requestID: input["requestID"] }, + payload: { answers: input["answers"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint22_3 = (raw: RawClient["server.question"]) => (input: Endpoint22_3Input) => - raw["session.question.reject"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["session.question.reject"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe( + Effect.mapError(mapClientError), + ), ) const adaptGroup22 = (raw: RawClient["server.question"]) => ({ @@ -1161,52 +1135,38 @@ const adaptGroup22 = (raw: RawClient["server.question"]) => ({ reject: Endpoint22_3(raw), }) -type Endpoint23_0Request = Parameters[0] -type Endpoint23_0Input = { readonly location?: Endpoint23_0Request["query"]["location"] } const Endpoint23_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint23_0Input) => - raw["reference.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["reference.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup23 = (raw: RawClient["server.reference"]) => ({ list: Endpoint23_0(raw) }) -type Endpoint24_0Request = Parameters[0] -type Endpoint24_0Input = { - readonly projectID: Endpoint24_0Request["params"]["projectID"] - readonly location?: Endpoint24_0Request["query"]["location"] - readonly strategy: Endpoint24_0Request["payload"]["strategy"] - readonly directory: Endpoint24_0Request["payload"]["directory"] - readonly name?: Endpoint24_0Request["payload"]["name"] -} const Endpoint24_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint24_0Input) => - raw["projectCopy.create"]({ - params: { projectID: input["projectID"] }, - query: { location: input["location"] }, - payload: { strategy: input["strategy"], directory: input["directory"], name: input["name"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint24_1Request = Parameters[0] -type Endpoint24_1Input = { - readonly projectID: Endpoint24_1Request["params"]["projectID"] - readonly location?: Endpoint24_1Request["query"]["location"] - readonly directory: Endpoint24_1Request["payload"]["directory"] - readonly force: Endpoint24_1Request["payload"]["force"] -} + preserveEffect()( + raw["projectCopy.create"]({ + params: { projectID: input["projectID"] }, + query: { location: input["location"] }, + payload: { strategy: input["strategy"], directory: input["directory"], name: input["name"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint24_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint24_1Input) => - raw["projectCopy.remove"]({ - params: { projectID: input["projectID"] }, - query: { location: input["location"] }, - payload: { directory: input["directory"], force: input["force"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint24_2Request = Parameters[0] -type Endpoint24_2Input = { - readonly projectID: Endpoint24_2Request["params"]["projectID"] - readonly location?: Endpoint24_2Request["query"]["location"] -} + preserveEffect()( + raw["projectCopy.remove"]({ + params: { projectID: input["projectID"] }, + query: { location: input["location"] }, + payload: { directory: input["directory"], force: input["force"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint24_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint24_2Input) => - raw["projectCopy.refresh"]({ - params: { projectID: input["projectID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["projectCopy.refresh"]({ + params: { projectID: input["projectID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup24 = (raw: RawClient["server.projectCopy"]) => ({ create: Endpoint24_0(raw), @@ -1214,52 +1174,44 @@ const adaptGroup24 = (raw: RawClient["server.projectCopy"]) => ({ refresh: Endpoint24_2(raw), }) -type Endpoint25_0Request = Parameters[0] -type Endpoint25_0Input = { readonly location?: Endpoint25_0Request["query"]["location"] } const Endpoint25_0 = (raw: RawClient["server.vcs"]) => (input?: Endpoint25_0Input) => - raw["vcs.status"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint25_1Request = Parameters[0] -type Endpoint25_1Input = { - readonly location?: Endpoint25_1Request["query"]["location"] - readonly mode: Endpoint25_1Request["query"]["mode"] - readonly context?: Endpoint25_1Request["query"]["context"] -} + preserveEffect()( + raw["vcs.status"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint25_1 = (raw: RawClient["server.vcs"]) => (input: Endpoint25_1Input) => - raw["vcs.diff"]({ query: { location: input["location"], mode: input["mode"], context: input["context"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["vcs.diff"]({ query: { location: input["location"], mode: input["mode"], context: input["context"] } }).pipe( + Effect.mapError(mapClientError), + ), ) const adaptGroup25 = (raw: RawClient["server.vcs"]) => ({ status: Endpoint25_0(raw), diff: Endpoint25_1(raw) }) const Endpoint26_0 = (raw: RawClient["server.debug"]) => () => - raw["debug.location"]({}).pipe(Effect.mapError(mapClientError)) + preserveEffect()(raw["debug.location"]({}).pipe(Effect.mapError(mapClientError))) -type Endpoint26_1Request = Parameters[0] -type Endpoint26_1Input = { readonly location?: Endpoint26_1Request["query"]["location"] } const Endpoint26_1 = (raw: RawClient["server.debug"]) => (input?: Endpoint26_1Input) => - raw["debug.location.evict"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["debug.location.evict"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup26 = (raw: RawClient["server.debug"]) => ({ location: { list: Endpoint26_0(raw), evict: Endpoint26_1(raw) }, }) -type Endpoint27_0Request = Parameters[0] -type Endpoint27_0Input = { readonly location?: Endpoint27_0Request["query"]["location"] } const Endpoint27_0 = (raw: RawClient["server.websearch"]) => (input?: Endpoint27_0Input) => - raw["websearch.providers"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint27_1Request = Parameters[0] -type Endpoint27_1Input = { - readonly location?: Endpoint27_1Request["query"]["location"] - readonly query: Endpoint27_1Request["payload"]["query"] - readonly providerID?: Endpoint27_1Request["payload"]["providerID"] -} + preserveEffect()( + raw["websearch.providers"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint27_1 = (raw: RawClient["server.websearch"]) => (input: Endpoint27_1Input) => - raw["websearch.query"]({ - query: { location: input["location"] }, - payload: { query: input["query"], providerID: input["providerID"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["websearch.query"]({ + query: { location: input["location"] }, + payload: { query: input["query"], providerID: input["providerID"] }, + }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup27 = (raw: RawClient["server.websearch"]) => ({ providers: Endpoint27_0(raw), diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts index b5fd3fd6c3f8..e6edc8a8568e 100644 --- a/packages/client/src/promise/generated/client.ts +++ b/packages/client/src/promise/generated/client.ts @@ -7,6 +7,8 @@ import type { LocationGetOutput, AgentListInput, AgentListOutput, + AgentGetInput, + AgentGetOutput, PluginListInput, PluginListOutput, SessionListInput, @@ -403,6 +405,18 @@ export function make(options: ClientOptions) { }, requestOptions, ), + get: (input: AgentGetInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/agent/${encodeURIComponent(input.agentID)}`, + query: { location: input["location"] }, + successStatus: 200, + declaredStatuses: [404, 401, 400], + empty: false, + }, + requestOptions, + ), }, plugin: { list: (input?: PluginListInput, requestOptions?: RequestOptions) => diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 19ae6c3f5dd8..d679249b15ea 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -6,11 +6,11 @@ export type ServiceStopResponse = { accepted: boolean } export type ModelRef = { id: string; providerID: string; variant?: string } -export type ProviderSettings = { [x: string]: JsonValue } +export type ProviderSettings = { [x: string]: any } export type AgentColor = string -export type PermissionV2Effect = "allow" | "deny" | "ask" +export type PermissionEffect = "allow" | "deny" | "ask" export type PluginInfo = { id: string } @@ -175,24 +175,24 @@ export type ModelCapabilities = { tools: boolean; input: Array; output: export type ModelVariant = { id: string - settings?: { [x: string]: JsonValue } + settings?: { [x: string]: any } headers?: { [x: string]: string } - body?: { [x: string]: JsonValue } + body?: { [x: string]: any } } export type MoneyUSDPerMillionTokens = number export type GenerateTextResponse = { data: { text: string } } -export type ProviderV2Info = { +export type ProviderInfo = { id: string integrationID?: string name: string disabled?: boolean package: string - settings?: { [x: string]: JsonValue } + settings?: { [x: string]: any } headers?: { [x: string]: string } - body?: { [x: string]: JsonValue } + body?: { [x: string]: any } } export type IntegrationWhen = { key: string; op: "eq" | "neq"; value: string } @@ -299,7 +299,7 @@ export type FormExternalField = { key: string; type: "external"; url: string; ti export type FormValue = string | number | boolean | Array -export type PermissionV2Source = { type: "tool"; messageID: string; callID: string } +export type PermissionSource = { type: "tool"; messageID: string; callID: string } export type PermissionSavedInfo = { id: string; projectID: string; action: string; resource: string } @@ -323,9 +323,9 @@ export type FileDiffLegacyInfo = { status?: "added" | "deleted" | "modified" } -export type PermissionAction = "allow" | "deny" | "ask" +export type PermissionV1Action = "allow" | "deny" | "ask" -export type JSONSchema = { [x: string]: any } +export type SessionV1JSONSchema = { [x: string]: any } export type ProviderAuthError = { name: "ProviderAuthError"; data: { providerID: string; message: string } } @@ -356,7 +356,7 @@ export type APIError = { } } -export type TextPart = { +export type SessionV1TextPart = { id: string sessionID: string messageID: string @@ -368,7 +368,7 @@ export type TextPart = { metadata?: { [x: string]: any } | undefined } -export type SubtaskPart = { +export type SessionV1SubtaskPart = { id: string sessionID: string messageID: string @@ -380,7 +380,7 @@ export type SubtaskPart = { command?: string | undefined } -export type ReasoningPart = { +export type SessionV1ReasoningPart = { id: string sessionID: string messageID: string @@ -390,13 +390,13 @@ export type ReasoningPart = { time: { start: number; end?: number | undefined } } -export type FilePartSourceText = { value: string; start: number; end: number } +export type SessionV1FilePartSourceText = { value: string; start: number; end: number } -export type Range = { start: { line: number; character: number }; end: { line: number; character: number } } +export type SessionV1Range = { start: { line: number; character: number }; end: { line: number; character: number } } -export type ToolStatePending = { status: "pending"; input: { [x: string]: any }; raw: string } +export type SessionV1ToolStatePending = { status: "pending"; input: { [x: string]: any }; raw: string } -export type ToolStateRunning = { +export type SessionV1ToolStateRunning = { status: "running" input: { [x: string]: any } title?: string | undefined @@ -404,7 +404,7 @@ export type ToolStateRunning = { time: { start: number } } -export type ToolStateError = { +export type SessionV1ToolStateError = { status: "error" input: { [x: string]: any } error: string @@ -412,7 +412,7 @@ export type ToolStateError = { time: { start: number; end: number } } -export type StepStartPart = { +export type SessionV1StepStartPart = { id: string sessionID: string messageID: string @@ -420,7 +420,7 @@ export type StepStartPart = { snapshot?: string | undefined } -export type StepFinishPart = { +export type SessionV1StepFinishPart = { id: string sessionID: string messageID: string @@ -437,9 +437,15 @@ export type StepFinishPart = { } } -export type SnapshotPart = { id: string; sessionID: string; messageID: string; type: "snapshot"; snapshot: string } +export type SessionV1SnapshotPart = { + id: string + sessionID: string + messageID: string + type: "snapshot" + snapshot: string +} -export type PatchPart = { +export type SessionV1PatchPart = { id: string sessionID: string messageID: string @@ -448,7 +454,7 @@ export type PatchPart = { files: Array } -export type AgentPart = { +export type SessionV1AgentPart = { id: string sessionID: string messageID: string @@ -457,7 +463,7 @@ export type AgentPart = { source?: { value: string; start: number; end: number } | undefined } -export type CompactionPart = { +export type SessionV1CompactionPart = { id: string sessionID: string messageID: string @@ -467,7 +473,7 @@ export type CompactionPart = { tail_start_id?: string | undefined } -export type PermissionV2Reply = "once" | "always" | "reject" +export type PermissionReply = "once" | "always" | "reject" export type Pty = { id: string @@ -480,11 +486,11 @@ export type Pty = { exitCode?: number } -export type QuestionV2Option = { label: string; description: string } +export type QuestionOption = { label: string; description: string } -export type QuestionV2Tool = { messageID: string; callID: string } +export type QuestionTool = { messageID: string; callID: string } -export type QuestionV2Answer = Array +export type QuestionAnswer = Array export type FormMetadata1 = { [x: string]: any } @@ -501,12 +507,6 @@ export type SessionStatus = } | { type: "busy" } -export type QuestionOption = { label: string; description: string } - -export type QuestionTool = { messageID: string; callID: string } - -export type QuestionAnswer = Array - export type ShellInfo1 = { id: string status: "running" | "exited" | "timeout" | "killed" @@ -564,10 +564,10 @@ export type CommandInfo = { export type ProviderRequest = { settings: ProviderSettings headers: { [x: string]: string } - body: { [x: string]: JsonValue } + body: { [x: string]: any } } -export type PermissionV2Rule = { action: string; resource: string; effect: PermissionV2Effect } +export type PermissionRule = { action: string; resource: string; effect: PermissionEffect } export type SessionAgentSelected = { id: string @@ -1044,11 +1044,11 @@ export type ShellDeleted = { data: { id: string } } -export type QuestionV2Rejected = { +export type QuestionRejected = { id: string created: number metadata?: { [x: string]: any } - type: "question.v2.rejected" + type: "question.rejected" location?: LocationRef data: { sessionID: string; requestID: string } } @@ -1186,41 +1186,6 @@ export type McpResourcesChanged = { data: { server: string } } -export type PermissionAsked = { - id: string - created: number - metadata?: { [x: string]: any } - type: "permission.asked" - location?: LocationRef - data: { - id: string - sessionID: string - permission: string - patterns: Array - metadata: { [x: string]: any } - always: Array - tool?: { messageID: string; callID: string } | undefined - } -} - -export type PermissionReplied = { - id: string - created: number - metadata?: { [x: string]: any } - type: "permission.replied" - location?: LocationRef - data: { sessionID: string; requestID: string; reply: "once" | "always" | "reject" } -} - -export type QuestionRejected = { - id: string - created: number - metadata?: { [x: string]: any } - type: "question.rejected" - location?: LocationRef - data: { sessionID: string; requestID: string } -} - export type V2EventServerConnected = { id: string metadata?: { [x: string]: any } | undefined @@ -1544,21 +1509,21 @@ export type FormMultiselectField = { export type FormAnswer = { [x: string]: FormValue } -export type PermissionV2Request = { +export type PermissionRequest = { id: string sessionID: string action: string resources: Array save?: Array metadata?: { [x: string]: JsonValue } - source?: PermissionV2Source + source?: PermissionSource } -export type PermissionV2Asked = { +export type PermissionAsked = { id: string created: number metadata?: { [x: string]: any } - type: "permission.v2.asked" + type: "permission.asked" location?: LocationRef data: { id: string @@ -1567,17 +1532,17 @@ export type PermissionV2Asked = { resources: Array save?: Array metadata?: { [x: string]: any } - source?: PermissionV2Source + source?: PermissionSource } } -export type PermissionRule = { permission: string; pattern: string; action: PermissionAction } +export type PermissionV1Rule = { permission: string; pattern: string; action: PermissionV1Action } -export type OutputFormat = +export type SessionV1OutputFormat = | { type: "text" } - | { type: "json_schema"; schema: JSONSchema; retryCount?: number | undefined | undefined } + | { type: "json_schema"; schema: SessionV1JSONSchema; retryCount?: number | undefined | undefined } -export type AssistantMessage = { +export type SessionV1AssistantMessage = { id: string sessionID: string role: "assistant" @@ -1612,7 +1577,7 @@ export type AssistantMessage = { finish?: string | undefined } -export type RetryPart = { +export type SessionV1RetryPart = { id: string sessionID: string messageID: string @@ -1643,26 +1608,31 @@ export type SessionError = { } } -export type FileSource = { text: FilePartSourceText; type: "file"; path: string } +export type SessionV1FileSource = { text: SessionV1FilePartSourceText; type: "file"; path: string } -export type ResourceSource = { text: FilePartSourceText; type: "resource"; clientName: string; uri: string } +export type SessionV1ResourceSource = { + text: SessionV1FilePartSourceText + type: "resource" + clientName: string + uri: string +} -export type SymbolSource = { - text: FilePartSourceText +export type SessionV1SymbolSource = { + text: SessionV1FilePartSourceText type: "symbol" path: string - range: Range + range: SessionV1Range name: string kind: number } -export type PermissionV2Replied = { +export type PermissionReplied = { id: string created: number metadata?: { [x: string]: any } - type: "permission.v2.replied" + type: "permission.replied" location?: LocationRef - data: { sessionID: string; requestID: string; reply: PermissionV2Reply } + data: { sessionID: string; requestID: string; reply: PermissionReply } } export type PtyCreated = { @@ -1683,21 +1653,21 @@ export type PtyUpdated = { data: { info: Pty } } -export type QuestionV2Info = { +export type QuestionInfo = { question: string header: string - options: Array + options: Array multiple?: boolean custom?: boolean } -export type QuestionV2Replied = { +export type QuestionReplied = { id: string created: number metadata?: { [x: string]: any } - type: "question.v2.replied" + type: "question.replied" location?: LocationRef - data: { sessionID: string; requestID: string; answers: Array } + data: { sessionID: string; requestID: string; answers: Array } } export type FormStringField1 = { @@ -1774,26 +1744,9 @@ export type SessionStatus2 = { data: { sessionID: string; status: SessionStatus } } -export type QuestionInfo = { - question: string - header: string - options: Array - multiple?: boolean | undefined - custom?: boolean | undefined -} - -export type QuestionReplied = { - id: string - created: number - metadata?: { [x: string]: any } - type: "question.replied" - location?: LocationRef - data: { sessionID: string; requestID: string; answers: Array } -} - export type ReferenceSource = ReferenceLocalSource | ReferenceGitSource -export type PermissionV2Ruleset = Array +export type PermissionRuleset = Array export type SessionInfo = { id: string @@ -1910,9 +1863,9 @@ export type ModelInfo = { name: string compatibility?: ModelCompatibility package?: string - settings?: { [x: string]: JsonValue } + settings?: { [x: string]: any } headers?: { [x: string]: string } - body?: { [x: string]: JsonValue } + body?: { [x: string]: any } capabilities: ModelCapabilities variants: Array time: { released: number } @@ -1948,14 +1901,14 @@ export type FormReplied = { data: { id: string; sessionID: string; answer: FormAnswer } } -export type PermissionRuleset = Array +export type PermissionV1Ruleset = Array -export type UserMessage = { +export type SessionV1UserMessage = { id: string sessionID: string role: "user" time: { created: number } - format?: OutputFormat | undefined + format?: SessionV1OutputFormat | undefined summary?: { title?: string | undefined; body?: string | undefined; diffs: Array } | undefined agent: string model: { providerID: string; modelID: string; variant?: string | undefined } @@ -1963,23 +1916,18 @@ export type UserMessage = { tools?: { [x: string]: boolean } | undefined } -export type FilePartSource = FileSource | SymbolSource | ResourceSource +export type SessionV1FilePartSource = SessionV1FileSource | SessionV1SymbolSource | SessionV1ResourceSource -export type QuestionV2Asked = { +export type QuestionAsked = { id: string created: number metadata?: { [x: string]: any } - type: "question.v2.asked" + type: "question.asked" location?: LocationRef - data: { id: string; sessionID: string; questions: Array; tool?: QuestionV2Tool } + data: { id: string; sessionID: string; questions: Array; tool?: QuestionTool } } -export type QuestionV2Request = { - id: string - sessionID: string - questions: Array - tool?: QuestionV2Tool -} +export type QuestionRequest = { id: string; sessionID: string; questions: Array; tool?: QuestionTool } export type FormField1 = | FormStringField1 @@ -1989,15 +1937,6 @@ export type FormField1 = | FormMultiselectField1 | FormExternalField -export type QuestionAsked = { - id: string - created: number - metadata?: { [x: string]: any } - type: "question.asked" - location?: LocationRef - data: { id: string; sessionID: string; questions: Array; tool?: QuestionTool | undefined } -} - export type ReferenceInfo = { name: string path: string @@ -2017,7 +1956,7 @@ export type AgentInfo = { hidden: boolean color?: AgentColor steps?: number - permissions: PermissionV2Ruleset + permissions: PermissionRuleset } export type SessionsResponse = { data: Array; cursor: { previous?: string | null; next?: string | null } } @@ -2075,13 +2014,13 @@ export type SessionV1Info = { version: string metadata?: { [x: string]: any } time: { created: number; updated: number; compacting?: number; archived?: number } - permission?: PermissionRuleset + permission?: PermissionV1Ruleset revert?: { messageID: string; partID?: string; snapshot?: string; diff?: string } } -export type Message = UserMessage | AssistantMessage +export type SessionV1Message = SessionV1UserMessage | SessionV1AssistantMessage -export type FilePart = { +export type SessionV1FilePart = { id: string sessionID: string messageID: string @@ -2089,7 +2028,7 @@ export type FilePart = { mime: string filename?: string | undefined url: string - source?: FilePartSource | undefined + source?: SessionV1FilePartSource | undefined } export type FormFields1 = [FormField1, ...Array] @@ -2160,17 +2099,17 @@ export type MessageUpdated = { type: "message.updated" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef - data: { sessionID: string; info: Message } + data: { sessionID: string; info: SessionV1Message } } -export type ToolStateCompleted = { +export type SessionV1ToolStateCompleted = { status: "completed" input: { [x: string]: any } output: string title: string metadata: { [x: string]: any } time: { start: number; end: number; compacted?: number | undefined } - attachments?: Array | undefined + attachments?: Array | undefined } export type FormInfo1 = { id: string; sessionID: string; title: string; metadata?: FormMetadata1; fields: FormFields1 } @@ -2196,7 +2135,11 @@ export type SessionMessageInfo = | SessionMessageAssistant | SessionMessageCompaction -export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError +export type SessionV1ToolState = + | SessionV1ToolStatePending + | SessionV1ToolStateRunning + | SessionV1ToolStateCompleted + | SessionV1ToolStateError export type FormCreated = { id: string @@ -2252,32 +2195,32 @@ export type SessionMessagesResponse = { cursor: { previous?: string | null; next?: string | null } } -export type ToolPart = { +export type SessionV1ToolPart = { id: string sessionID: string messageID: string type: "tool" callID: string tool: string - state: ToolState + state: SessionV1ToolState metadata?: { [x: string]: any } | undefined } export type SessionLogItem = SessionEventDurable | EventLogSynced -export type Part = - | TextPart - | SubtaskPart - | ReasoningPart - | FilePart - | ToolPart - | StepStartPart - | StepFinishPart - | SnapshotPart - | PatchPart - | AgentPart - | RetryPart - | CompactionPart +export type SessionV1Part = + | SessionV1TextPart + | SessionV1SubtaskPart + | SessionV1ReasoningPart + | SessionV1FilePart + | SessionV1ToolPart + | SessionV1StepStartPart + | SessionV1StepFinishPart + | SessionV1SnapshotPart + | SessionV1PatchPart + | SessionV1AgentPart + | SessionV1RetryPart + | SessionV1CompactionPart export type MessagePartUpdated = { id: string @@ -2286,7 +2229,7 @@ export type MessagePartUpdated = { type: "message.part.updated" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef - data: { sessionID: string; part: Part; time: number } + data: { sessionID: string; part: SessionV1Part; time: number } } export type V2Event = @@ -2347,8 +2290,8 @@ export type V2Event = | SessionRevertCommitted | FilesystemChanged | ReferenceUpdated - | PermissionV2Asked - | PermissionV2Replied + | PermissionAsked + | PermissionReplied | PluginAdded | PluginUpdated | ProjectDirectoriesUpdated @@ -2362,9 +2305,9 @@ export type V2Event = | ShellCreated | ShellExited | ShellDeleted - | QuestionV2Asked - | QuestionV2Replied - | QuestionV2Rejected + | QuestionAsked + | QuestionReplied + | QuestionRejected | FormCreated | FormReplied | FormCancelled @@ -2380,11 +2323,6 @@ export type V2Event = | VcsBranchUpdated | McpStatusChanged | McpResourcesChanged - | PermissionAsked - | PermissionReplied - | QuestionAsked - | QuestionReplied - | QuestionRejected | SessionError | V2EventServerConnected @@ -2401,6 +2339,14 @@ export type InvalidRequestError = { export const isInvalidRequestError = (value: unknown): value is InvalidRequestError => typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidRequestError" +export type AgentNotFoundError = { + readonly _tag: "AgentNotFoundError" + readonly agentID: string + readonly message: string +} +export const isAgentNotFoundError = (value: unknown): value is AgentNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "AgentNotFoundError" + export type InvalidCursorError = { readonly _tag: "InvalidCursorError"; readonly message: string } export const isInvalidCursorError = (value: unknown): value is InvalidCursorError => typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidCursorError" @@ -2584,6 +2530,18 @@ export type AgentListOutput = { data: Array } +export type AgentGetInput = { + readonly agentID: { readonly agentID: string }["agentID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type AgentGetOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: AgentInfo +} + export type PluginListInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined @@ -3312,7 +3270,7 @@ export type ProviderListInput = { export type ProviderListOutput = { location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array + data: Array } export type ProviderGetInput = { @@ -3324,7 +3282,7 @@ export type ProviderGetInput = { export type ProviderGetOutput = { location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: ProviderV2Info + data: ProviderInfo } export type IntegrationListInput = { @@ -4476,7 +4434,7 @@ export type PermissionRequestListInput = { export type PermissionRequestListOutput = { location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array + data: Array } export type PermissionSavedListInput = { readonly projectID?: { readonly projectID?: string | undefined }["projectID"] } @@ -4554,18 +4512,18 @@ export type PermissionCreateInput = { }["agent"] } -export type PermissionCreateOutput = { data: { id: string; effect: PermissionV2Effect } }["data"] +export type PermissionCreateOutput = { data: { id: string; effect: PermissionEffect } }["data"] export type PermissionListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } -export type PermissionListOutput = { data: Array }["data"] +export type PermissionListOutput = { data: Array }["data"] export type PermissionGetInput = { readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"] } -export type PermissionGetOutput = { data: PermissionV2Request }["data"] +export type PermissionGetOutput = { data: PermissionRequest }["data"] export type PermissionReplyInput = { readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] @@ -4864,12 +4822,12 @@ export type QuestionRequestListInput = { export type QuestionRequestListOutput = { location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array + data: Array } export type QuestionListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } -export type QuestionListOutput = { data: Array }["data"] +export type QuestionListOutput = { data: Array }["data"] export type QuestionReplyInput = { readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] diff --git a/packages/client/test/api.types.ts b/packages/client/test/api.types.ts index ba95bb429709..a4c3f0f0036b 100644 --- a/packages/client/test/api.types.ts +++ b/packages/client/test/api.types.ts @@ -1,5 +1,6 @@ import { Effect } from "effect" import { OpenCode as EffectOpenCode, type AppApi as EffectApi } from "../src/effect" +import type { Session } from "@opencode-ai/schema/session" type EffectClient = Effect.Success> type PromiseClient = ReturnType @@ -9,6 +10,10 @@ declare const promiseClient: PromiseClient const effectApi: EffectApi = effectClient +const effectSession: Effect.Effect = effectClient.session.get({ + sessionID: "ses_test" as Session.ID, +}) + declare const sessionID: Parameters[0]["sessionID"] const effectList: Effect.Effect< @@ -37,4 +42,4 @@ const promiseRemove: Promise = promiseClient.session.instructions.entry.re key: "review-notes", }) -void [effectList, effectPut, effectRemove, promiseList, promisePut, promiseRemove] +void [effectSession, effectList, effectPut, effectRemove, promiseList, promisePut, promiseRemove] diff --git a/packages/client/test/contract-identity.test.ts b/packages/client/test/contract-identity.test.ts index 8de2115fa78d..ace9d86a1854 100644 --- a/packages/client/test/contract-identity.test.ts +++ b/packages/client/test/contract-identity.test.ts @@ -14,6 +14,14 @@ test("effect entrypoint exposes canonical Schema contracts", () => { expect(Client.Session).toBe(Session) }) +test("generated Effect API names canonical and composed outputs", async () => { + const source = await Bun.file(new URL("../src/effect/api/api.ts", import.meta.url)).text() + + expect(source).toContain("export type Endpoint5_3Output = Session.Info") + expect(source).toContain("export type Endpoint19_0Output = OpenCodeEvent") + expect(source).not.toContain("HttpApiClient.ForApi") +}) + test("shared DTO schemas construct and decode plain objects", () => { const made = Prompt.make({ text: "hello" }) const decoded = Schema.decodeUnknownSync(Prompt)({ text: "hello" }) diff --git a/packages/core/package.json b/packages/core/package.json index 86f97f7d5099..de86b87b101b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -99,6 +99,7 @@ "@opencode-ai/schema": "workspace:*", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/util": "workspace:*", + "@standard-schema/spec": "catalog:", "@parcel/watcher": "2.5.1", "@silvia-odwyer/photon-node": "0.3.4", "@openrouter/ai-sdk-provider": "2.9.0", diff --git a/packages/core/src/account.ts b/packages/core/src/account.ts index 7e81780e95b2..d9d29d742d97 100644 --- a/packages/core/src/account.ts +++ b/packages/core/src/account.ts @@ -1,4 +1,4 @@ -export * as AccountV2 from "./account" +export * as Account from "./account" import { Schema } from "effect" import type { HttpClientError } from "effect/unstable/http" diff --git a/packages/core/src/account/sql.ts b/packages/core/src/account/sql.ts index 4f45651d78ec..706ddd34a59a 100644 --- a/packages/core/src/account/sql.ts +++ b/packages/core/src/account/sql.ts @@ -1,14 +1,14 @@ import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core" -import { AccountV2 } from "../account" +import { Account } from "../account" import { Timestamps } from "../database/schema.sql" export const AccountTable = sqliteTable("account", { - id: text().$type().primaryKey(), + id: text().$type().primaryKey(), email: text().notNull(), url: text().notNull(), - access_token: text().$type().notNull(), - refresh_token: text().$type().notNull(), + access_token: text().$type().notNull(), + refresh_token: text().$type().notNull(), token_expiry: integer(), ...Timestamps, }) @@ -16,9 +16,9 @@ export const AccountTable = sqliteTable("account", { export const AccountStateTable = sqliteTable("account_state", { id: integer().primaryKey(), active_account_id: text() - .$type() + .$type() .references(() => AccountTable.id, { onDelete: "set null" }), - active_org_id: text().$type(), + active_org_id: text().$type(), }) // LEGACY @@ -27,8 +27,8 @@ export const ControlAccountTable = sqliteTable( { email: text().notNull(), url: text().notNull(), - access_token: text().$type().notNull(), - refresh_token: text().$type().notNull(), + access_token: text().$type().notNull(), + refresh_token: text().$type().notNull(), token_expiry: integer(), active: integer({ mode: "boolean" }) .notNull() diff --git a/packages/core/src/agent.ts b/packages/core/src/agent.ts index 23f1fd1240b5..548dcdb2aa66 100644 --- a/packages/core/src/agent.ts +++ b/packages/core/src/agent.ts @@ -1,9 +1,9 @@ -export * as AgentV2 from "./agent" +export * as Agent from "./agent" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Array, Context, Effect, Layer, Types } from "effect" import { Agent } from "@opencode-ai/schema/agent" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { State } from "./state" export const ID = Agent.ID @@ -17,7 +17,7 @@ export const Color = Agent.Color export const Info = Agent.Info export type Info = Agent.Info -export const Event = Agent.Event +export { Event } from "@opencode-ai/schema/agent" export interface Selection { readonly id: ID @@ -45,12 +45,12 @@ export interface Interface extends State.Transformable { readonly list: () => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Agent") {} +export class Service extends Context.Service()("@opencode/Agent") {} const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const state = State.create({ name: "agent", initial: () => ({ agents: new Map() }), @@ -70,7 +70,7 @@ const layer = Layer.effect( draft.agents.delete(id) }, }), - finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid), + finalize: () => bus.publish(Agent.Event.Updated, {}).pipe(Effect.asVoid), }) const selectable = (agent: Info | undefined) => agent && agent.mode !== "subagent" && !agent.hidden ? agent : undefined @@ -89,17 +89,17 @@ const layer = Layer.effect( return Service.of({ transform: state.transform, reload: state.reload, - get: Effect.fn("AgentV2.get")(function* (id) { + get: Effect.fn("Agent.get")(function* (id) { return state.get().agents.get(id) }), - default: Effect.fn("AgentV2.default")(function* () { + default: Effect.fn("Agent.default")(function* () { return selectedDefault() }), - resolve: Effect.fn("AgentV2.resolve")(function* (id) { + resolve: Effect.fn("Agent.resolve")(function* (id) { if (id !== undefined) return state.get().agents.get(ID.make(id)) return selectedDefault() }), - select: Effect.fn("AgentV2.select")(function* (id) { + select: Effect.fn("Agent.select")(function* (id) { if (id !== undefined) { const selected = ID.make(id) return { id: selected, info: state.get().agents.get(selected) } @@ -107,11 +107,11 @@ const layer = Layer.effect( const info = selectedDefault() return { id: info?.id ?? defaultID, info } }), - list: Effect.fn("AgentV2.list")(function* () { + list: Effect.fn("Agent.list")(function* () { return Array.fromIterable(state.get().agents.values()) }), }) }), ) -export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] }) +export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node] }) diff --git a/packages/core/src/aisdk.ts b/packages/core/src/aisdk.ts index 7c9e54b4f757..7aaa5ec6c951 100644 --- a/packages/core/src/aisdk.ts +++ b/packages/core/src/aisdk.ts @@ -32,8 +32,8 @@ import { import { Auth, Endpoint, type AnyRoute } from "@opencode-ai/ai/route" import { ProviderShared } from "@opencode-ai/ai/protocols/shared" import { Cause, Context, Effect, Layer, Option, Schema, Scope, Stream } from "effect" -import { ModelV2 } from "./model" -import { ProviderV2 } from "./provider" +import type { ID, Info } from "./model" +import { Provider } from "./provider" import { State } from "./state" type SDK = any @@ -42,14 +42,14 @@ type AssistantContent = Extract[" type ToolResultContent = Extract export interface SDKEvent { - readonly model: ModelV2.Info + readonly model: Info readonly package: string readonly options: Record sdk?: SDK } export interface LanguageEvent { - readonly model: ModelV2.Info + readonly model: Info readonly sdk: SDK readonly options: Record language?: LanguageModelV3 @@ -103,7 +103,7 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) { }) } -function prepareOptions(model: ModelV2.Info, pkg: string) { +function prepareOptions(model: Info, pkg: string) { const projected = mapBodyToProviderOptions(model, pkg) const options: Record = { name: model.providerID, @@ -146,7 +146,7 @@ function prepareOptions(model: ModelV2.Info, pkg: string) { if (typeof opts.body === "string" && model.body !== undefined) { const decoded = Option.getOrUndefined(Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(opts.body)) if (Schema.is(Schema.Record(Schema.String, Schema.Json))(decoded)) { - opts.body = JSON.stringify(ProviderV2.mergeOverlay(decoded, model.body)) + opts.body = JSON.stringify(Provider.mergeOverlay(decoded, model.body)) } } @@ -162,11 +162,11 @@ function prepareOptions(model: ModelV2.Info, pkg: string) { } export class InitError extends Schema.TaggedErrorClass()("AISDK.InitError", { - providerID: ProviderV2.ID, + providerID: Provider.ID, cause: Schema.Defect(), }) {} -function initError(providerID: ProviderV2.ID) { +function initError(providerID: Provider.ID) { return Effect.catchCause((cause) => Effect.fail(new InitError({ providerID, cause: Cause.squash(cause) }))) } @@ -181,11 +181,11 @@ export interface Interface { } readonly runSDK: (event: SDKEvent) => Effect.Effect readonly runLanguage: (event: LanguageEvent) => Effect.Effect - readonly language: (model: ModelV2.Info) => Effect.Effect - readonly model: (model: ModelV2.Info) => Effect.Effect + readonly language: (model: Info) => Effect.Effect + readonly model: (model: Info) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/AISDK") {} +export class Service extends Context.Service()("@opencode/AISDK") {} export const locationLayer = Layer.effect( Service, @@ -260,13 +260,13 @@ export const locationLayer = Layer.effect( }) const existing = languages.get(key) if (existing) return existing - if (!ProviderV2.isAISDK(model.package)) + if (!Provider.isAISDK(model.package)) return yield* new InitError({ providerID: model.providerID, cause: new Error(`Unsupported package ${model.package}`), }) - const packageName = ProviderV2.packageName(model.package) + const packageName = Provider.packageName(model.package) const options = prepareOptions(model, packageName) const sdkKey = cacheKey({ providerID: model.providerID, @@ -301,8 +301,8 @@ export const locationLayer = Layer.effect( export const defaultLayer = locationLayer -function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) { - const packageName = ProviderV2.packageName(info.package!) +function modelFromLanguage(info: Info, language: LanguageModelV3) { + const packageName = Provider.packageName(info.package!) const projected = mapBodyToProviderOptions(info, packageName) const optionKey = providerOptionKey(packageName, info.providerID) const providerOptions = (() => { @@ -352,7 +352,7 @@ function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) { }) } -function gatewayProviderOptions(modelID: ModelV2.ID, settings: Readonly>) { +function gatewayProviderOptions(modelID: ID, settings: Readonly>) { const gateway = typeof settings.gateway === "object" && settings.gateway !== null && !Array.isArray(settings.gateway) ? Object.fromEntries(Object.entries(settings.gateway)) @@ -369,7 +369,7 @@ function gatewayProviderOptions(modelID: ModelV2.ID, settings: Readonly> | undefined return Object.keys(result).length === 0 ? undefined : result } -function mapBodyToProviderOptions(model: ModelV2.Info, packageName: string) { +function mapBodyToProviderOptions(model: Info, packageName: string) { const settings = requestSettings(model.settings) const pro = Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(model.body?.reasoning) const forceReasoning = ["@ai-sdk/openai", "@ai-sdk/azure", "@ai-sdk/amazon-bedrock/mantle"].includes(packageName) && (pro || settings?.reasoningEffort !== undefined || settings?.reasoningSummary !== undefined) - const normalized = forceReasoning ? ProviderV2.mergeOverlay(settings, { forceReasoning: true }) : settings + const normalized = forceReasoning ? Provider.mergeOverlay(settings, { forceReasoning: true }) : settings if (!pro) return { settings: normalized, body: model.body } const body = { ...model.body } delete body.reasoning return { - settings: ProviderV2.mergeOverlay(normalized, { reasoningMode: "pro" }), + settings: Provider.mergeOverlay(normalized, { reasoningMode: "pro" }), body: Object.keys(body).length === 0 ? undefined : body, } } diff --git a/packages/core/src/event.ts b/packages/core/src/bus.ts similarity index 88% rename from packages/core/src/event.ts rename to packages/core/src/bus.ts index 759a55721d33..2e498e0a397e 100644 --- a/packages/core/src/event.ts +++ b/packages/core/src/bus.ts @@ -1,8 +1,7 @@ -export * as EventV2 from "./event" +export * as Bus from "./bus" import { Cause, Context, DateTime, Effect, Layer, Option, PubSub, Schema, Stream } from "effect" import { Event } from "@opencode-ai/schema/event" -import type { Data, Definition, Payload } from "@opencode-ai/schema/event" import type { EventLog } from "@opencode-ai/schema/event-log" import { and, asc, eq, gt, inArray, lte, sql } from "drizzle-orm" import { Database } from "./database/database" @@ -12,18 +11,10 @@ import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { isDeepStrictEqual } from "node:util" import { Durable } from "@opencode-ai/schema/durable-event-manifest" -export const ID = Event.ID -export type ID = import("@opencode-ai/schema/event").ID -export const Seq = Event.Seq -export type Seq = import("@opencode-ai/schema/event").Seq -export const Version = Event.Version -export type Version = import("@opencode-ai/schema/event").Version -export type { Data, Definition, Payload } from "@opencode-ai/schema/event" - -export type Subscriber = (event: Payload) => Effect.Effect +export type Subscriber = (event: Event.Payload) => Effect.Effect export type Unsubscribe = Effect.Effect -export const latestSequence = Effect.fn("EventV2.latestSequence")(function* ( +export const latestSequence = Effect.fn("Bus.latestSequence")(function* ( db: Database.Interface["db"], aggregateID: string, ) { @@ -36,7 +27,7 @@ export const latestSequence = Effect.fn("EventV2.latestSequence")(function* ( return row?.seq ?? -1 }) -export const reserveSequence = Effect.fn("EventV2.reserveSequence")(function* ( +export const reserveSequence = Effect.fn("Bus.reserveSequence")(function* ( db: Database.Interface["db"], aggregateID: string, seq: number, @@ -53,7 +44,7 @@ export const reserveSequence = Effect.fn("EventV2.reserveSequence")(function* ( }) export type SerializedEvent = { - readonly id: ID + readonly id: Event.ID readonly type: string readonly created?: DateTime.Utc readonly seq: number @@ -62,7 +53,7 @@ export type SerializedEvent = { } export class InvalidDurableEventError extends Schema.TaggedErrorClass()( - "EventV2.InvalidDurableEvent", + "Bus.InvalidDurableEvent", { type: Schema.String, message: Schema.String, @@ -71,11 +62,11 @@ export class InvalidDurableEventError extends Schema.TaggedErrorClass ({ aggregateID, - seq: Seq.make(seq), - version: Version.make(version), + seq: Event.Seq.make(seq), + version: Event.Version.make(version), }) -const decodeSerializedEvent = (event: SerializedEvent): Payload => { +const decodeSerializedEvent = (event: SerializedEvent): Event.Payload => { const definition = Durable.get(event.type) if (!definition?.durable) { throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }) @@ -94,7 +85,7 @@ export const durable = Event.durable export const ephemeral = Event.ephemeral export interface PublishOptions { - readonly id?: ID + readonly id?: Event.ID readonly metadata?: Record readonly location?: Location.Ref /** Local operational projection committed atomically with a new durable event. Not replayed or serialized. */ @@ -102,13 +93,13 @@ export interface PublishOptions { } /** Marker/event union emitted by `log`. */ -export type LogItem = Payload | EventLog.Synced +export type LogItem = Event.Payload | EventLog.Synced export const isSynced = (item: LogItem): item is EventLog.Synced => item.type === "log.synced" -export type SubscribePayload = D[number] extends infer Item - ? Item extends Definition - ? Payload +export type SubscribePayload = D[number] extends infer Item + ? Item extends Event.Definition + ? Event.Payload : never : never @@ -117,19 +108,19 @@ export interface Subscribe { * Volatile live channel: every event published from now on, nothing before or * across a disconnect. Consumers that need reliability combine it with `log`. */ - (): Stream.Stream - (definition: D): Stream.Stream> - (definitions: D): Stream.Stream> + (): Stream.Stream + (definition: D): Stream.Stream> + (definitions: D): Stream.Stream> } -const isDefinition = (input: Definition | readonly Definition[]): input is Definition => !Array.isArray(input) +const isDefinition = (input: Event.Definition | readonly Event.Definition[]): input is Event.Definition => !Array.isArray(input) export interface Interface { - readonly publish: ( + readonly publish: ( definition: D, - data: Data, + data: Event.Data, options?: PublishOptions, - ) => Effect.Effect> + ) => Effect.Effect> readonly subscribe: Subscribe /** * Durable, ordered per-aggregate log read. Forked aggregates may reserve an @@ -144,10 +135,10 @@ export interface Interface { readonly follow?: boolean }) => Stream.Stream /** Latest committed seq per aggregate. Aggregates without events are absent. */ - readonly sequences: (aggregateIDs: ReadonlyArray) => Effect.Effect> + readonly sequences: (aggregateIDs: ReadonlyArray) => Effect.Effect> /** @deprecated Use `subscribe()` and consume the returned stream. */ readonly listen: (listener: Subscriber) => Effect.Effect - readonly project: (definition: D, projector: Subscriber) => Effect.Effect + readonly project: (definition: D, projector: Subscriber) => Effect.Effect readonly replay: ( event: SerializedEvent, options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }, @@ -160,7 +151,8 @@ export interface Interface { readonly claim: (aggregateID: string, ownerID: string) => Effect.Effect } -export class Service extends Context.Service()("@opencode/Event") {} + +export class Service extends Context.Service()("@opencode/Bus") {} export interface LayerOptions { readonly beforeAggregateRead?: (aggregateID: string) => Effect.Effect @@ -173,20 +165,20 @@ export const layerWith = (options?: LayerOptions) => Service, Effect.gen(function* () { const pubsub = { - live: yield* PubSub.unbounded(), + live: yield* PubSub.unbounded(), durable: new Map>>(), - typed: new Map>(), + typed: new Map>(), } const projectors = new Map() const listeners = new Array() const { db } = yield* Database.Service const logReadPageSize = options?.logReadPageSize ?? 512 - const getOrCreate = (definition: Definition) => + const getOrCreate = (definition: Event.Definition) => Effect.gen(function* () { const existing = pubsub.typed.get(definition.type) if (existing) return existing - const created = yield* PubSub.unbounded() + const created = yield* PubSub.unbounded() pubsub.typed.set(definition.type, created) return created }) @@ -204,8 +196,8 @@ export const layerWith = (options?: LayerOptions) => ) function commitDurableEvent( - definition: Definition, - event: Payload, + definition: Event.Definition, + event: Event.Payload, input?: { readonly seq: number readonly aggregateID: string @@ -318,7 +310,7 @@ export const layerWith = (options?: LayerOptions) => const committed = { ...event, durable: { aggregateID, seq, version: durable.version }, - } as Payload + } as Event.Payload for (const projector of list) { yield* projector(committed) } @@ -369,7 +361,7 @@ export const layerWith = (options?: LayerOptions) => }) } - function publishEvent(definition: D, event: Payload, commit?: PublishOptions["commit"]) { + function publishEvent(definition: D, event: Event.Payload, commit?: PublishOptions["commit"]) { return Effect.gen(function* () { if (!definition?.durable && commit) return yield* Effect.die( @@ -379,22 +371,22 @@ export const layerWith = (options?: LayerOptions) => }), ) if (definition?.durable) { - const committed = yield* commitDurableEvent(definition, event as Payload, undefined, commit) + const committed = yield* commitDurableEvent(definition, event as Event.Payload, undefined, commit) if (committed) { event = { ...event, durable: envelope(committed.aggregateID, committed.seq, definition.durable.version), } - yield* notify(event as Payload, true) + yield* notify(event as Event.Payload, true) return event } } - yield* notify(event as Payload, false) + yield* notify(event as Event.Payload, false) return event }) } - const observe = (event: Payload, observer: (event: Payload) => Effect.Effect) => + const observe = (event: Event.Payload, observer: (event: Event.Payload) => Effect.Effect) => Effect.suspend(() => observer(event)).pipe( Effect.catchCauseIf( (cause) => !Cause.hasInterrupts(cause), @@ -402,7 +394,7 @@ export const layerWith = (options?: LayerOptions) => ), ) - function notify(event: Payload, isolateListeners: boolean) { + function notify(event: Event.Payload, isolateListeners: boolean) { return Effect.gen(function* () { yield* Effect.forEach( listeners, @@ -415,7 +407,7 @@ export const layerWith = (options?: LayerOptions) => }) } - function publish(definition: D, data: Data, options?: PublishOptions) { + function publish(definition: D, data: Event.Data, options?: PublishOptions) { return Effect.gen(function* () { const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service)) const location = @@ -426,13 +418,13 @@ export const layerWith = (options?: LayerOptions) => return yield* publishEvent( definition, { - id: options?.id ?? ID.create(), + id: options?.id ?? Event.ID.create(), created: yield* DateTime.now, ...(options?.metadata ? { metadata: options.metadata } : {}), type: definition.type, ...(location ? { location } : {}), data, - } as Payload, + } as Event.Payload, options?.commit, ) }) @@ -454,7 +446,7 @@ export const layerWith = (options?: LayerOptions) => created: event.created ?? DateTime.makeUnsafe(0), type: definition.type, data: Schema.decodeUnknownSync(definition.data)(event.data), - } as Payload + } as Event.Payload const committed = yield* commitDurableEvent(definition, payload, { seq: event.seq, aggregateID: event.aggregateID, @@ -516,7 +508,7 @@ export const layerWith = (options?: LayerOptions) => .pipe(Effect.orDie) } - const local = (stream: Stream.Stream) => + const local = (stream: Stream.Stream) => Stream.unwrap( Effect.serviceOption(Location.Service).pipe( Effect.map((location) => @@ -536,12 +528,12 @@ export const layerWith = (options?: LayerOptions) => ), ) - function subscribe(): Stream.Stream - function subscribe(definition: D): Stream.Stream> - function subscribe( + function subscribe(): Stream.Stream + function subscribe(definition: D): Stream.Stream> + function subscribe( definitions: D, ): Stream.Stream> - function subscribe(input?: Definition | readonly Definition[]): Stream.Stream { + function subscribe(input?: Event.Definition | readonly Event.Definition[]): Stream.Stream { if (input === undefined) return streamLive() if (isDefinition(input)) { return local(Stream.unwrap(getOrCreate(input).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub))))) @@ -550,7 +542,7 @@ export const layerWith = (options?: LayerOptions) => return streamLive().pipe(Stream.filter((event) => types.has(event.type))) } - const streamLive = (): Stream.Stream => local(Stream.fromPubSub(pubsub.live)) + const streamLive = (): Stream.Stream => local(Stream.fromPubSub(pubsub.live)) const readAfter = ( aggregateID: string, @@ -624,7 +616,7 @@ export const layerWith = (options?: LayerOptions) => Stream.unwrap( Effect.gen(function* () { let sequence = input.after ?? -1 - const readThrough = (through: number): Stream.Stream => + const readThrough = (through: number): Stream.Stream => Stream.paginate(sequence, (cursor) => readAfter(input.aggregateID, cursor, { through, limit: logReadPageSize }).pipe( Effect.tap((page) => @@ -648,7 +640,7 @@ export const layerWith = (options?: LayerOptions) => const marker: EventLog.Synced = { type: "log.synced", aggregateID: input.aggregateID, - ...(target >= 0 ? { seq: Seq.make(target) } : {}), + ...(target >= 0 ? { seq: Event.Seq.make(target) } : {}), } const replay: Stream.Stream = readThrough(target).pipe( Stream.map((event): LogItem => event), @@ -665,7 +657,7 @@ export const layerWith = (options?: LayerOptions) => }), ) - const sequences = (aggregateIDs: ReadonlyArray): Effect.Effect> => { + const sequences = (aggregateIDs: ReadonlyArray): Effect.Effect> => { if (aggregateIDs.length === 0) return Effect.succeed(new Map()) return db .select({ aggregateID: EventSequenceTable.aggregate_id, seq: EventSequenceTable.seq }) @@ -674,7 +666,7 @@ export const layerWith = (options?: LayerOptions) => .all() .pipe( Effect.orDie, - Effect.map((rows) => new Map(rows.map((row) => [row.aggregateID, Seq.make(row.seq)]))), + Effect.map((rows) => new Map(rows.map((row) => [row.aggregateID, Event.Seq.make(row.seq)]))), ) } @@ -687,11 +679,11 @@ export const layerWith = (options?: LayerOptions) => }) }) - const project = (definition: D, projector: Subscriber): Effect.Effect => + const project = (definition: D, projector: Subscriber): Effect.Effect => Effect.sync(() => { const key = definition.durable ? versionedType(definition.type, definition.durable.version) : definition.type const list = projectors.get(key) ?? [] - list.push((event) => projector(event as Payload)) + list.push((event) => projector(event as Event.Payload)) projectors.set(key, list) }) diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts index 8d2c10c1a883..9bc9611cf89e 100644 --- a/packages/core/src/catalog.ts +++ b/packages/core/src/catalog.ts @@ -3,82 +3,82 @@ export * as Catalog from "./catalog" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Array, Context, Effect, Layer, Order, pipe } from "effect" import { Catalog } from "@opencode-ai/schema/catalog" -import { ModelV2 } from "./model" -import { ProviderV2 } from "./provider" -import { EventV2 } from "./event" +import { Model } from "./model" +import { Provider } from "./provider" +import { Bus } from "./bus" import { State } from "./state" import { Integration } from "./integration" export type ProviderRecord = { - provider: ProviderV2.MutableInfo - models: Map + provider: Provider.MutableInfo + models: Map } -export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID } +export type DefaultModel = { providerID: Provider.ID; modelID: Model.ID } -export const Event = Catalog.Event +export { Event } from "@opencode-ai/schema/catalog" type Data = { - providers: Map + providers: Map defaultModel?: DefaultModel } export type Draft = { provider: { list: () => readonly ProviderRecord[] - get: (providerID: ProviderV2.ID) => ProviderRecord | undefined - update: (providerID: ProviderV2.ID, fn: (provider: ProviderV2.MutableInfo) => void) => void - remove: (providerID: ProviderV2.ID) => void + get: (providerID: Provider.ID) => ProviderRecord | undefined + update: (providerID: Provider.ID, fn: (provider: Provider.MutableInfo) => void) => void + remove: (providerID: Provider.ID) => void } model: { - get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => ModelV2.Info | undefined - update: (providerID: ProviderV2.ID, modelID: ModelV2.ID, fn: (model: ModelV2.MutableInfo) => void) => void - remove: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void + get: (providerID: Provider.ID, modelID: Model.ID) => Model.Info | undefined + update: (providerID: Provider.ID, modelID: Model.ID, fn: (model: Model.MutableInfo) => void) => void + remove: (providerID: Provider.ID, modelID: Model.ID) => void default: { get: () => DefaultModel | undefined - set: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void + set: (providerID: Provider.ID, modelID: Model.ID) => void } } } export interface Interface extends State.Transformable { readonly provider: { - readonly get: (providerID: ProviderV2.ID) => Effect.Effect - readonly all: () => Effect.Effect - readonly available: () => Effect.Effect + readonly get: (providerID: Provider.ID) => Effect.Effect + readonly all: () => Effect.Effect + readonly available: () => Effect.Effect } readonly model: { - readonly get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => Effect.Effect - readonly all: () => Effect.Effect - readonly available: () => Effect.Effect - readonly default: () => Effect.Effect - readonly small: (providerID: ProviderV2.ID) => Effect.Effect + readonly get: (providerID: Provider.ID, modelID: Model.ID) => Effect.Effect + readonly all: () => Effect.Effect + readonly available: () => Effect.Effect + readonly default: () => Effect.Effect + readonly small: (providerID: Provider.ID) => Effect.Effect } } -export class Service extends Context.Service()("@opencode/v2/Catalog") {} +export class Service extends Context.Service()("@opencode/Catalog") {} const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const integrations = yield* Integration.Service - const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => { + const available = (provider: Provider.Info, integration: Integration.Info | undefined) => { if (provider.disabled) return false if (typeof provider.settings?.apiKey === "string") return true if (integration?.connections.length) return true return provider.integrationID === undefined && !integration } - const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => { + const projectModel = (model: Model.Info, provider: Provider.Info) => { return { ...model, package: model.package ?? provider.package, - settings: ProviderV2.mergeOverlay(provider.settings, model.settings), - headers: ProviderV2.mergeHeaders(provider.headers, model.headers), - body: ProviderV2.mergeOverlay(provider.body, model.body), - } satisfies ModelV2.Info + settings: Provider.mergeOverlay(provider.settings, model.settings), + headers: Provider.mergeHeaders(provider.headers, model.headers), + body: Provider.mergeOverlay(provider.body, model.body), + } satisfies Model.Info } const state = State.create({ @@ -93,8 +93,8 @@ const layer = Layer.effect( let current = draft.providers.get(providerID) if (!current) { current = { - provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo, - models: new Map(), + provider: Provider.Info.empty(providerID) as Provider.MutableInfo, + models: new Map(), } draft.providers.set(providerID, current) } @@ -110,13 +110,13 @@ const layer = Layer.effect( let record = draft.providers.get(providerID) if (!record) { record = { - provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo, - models: new Map(), + provider: Provider.Info.empty(providerID) as Provider.MutableInfo, + models: new Map(), } draft.providers.set(providerID, record) } const model = - record.models.get(modelID) ?? (ModelV2.Info.default(providerID, modelID) as ModelV2.MutableInfo) + record.models.get(modelID) ?? (Model.Info.default(providerID, modelID) as Model.MutableInfo) if (!record.models.has(modelID)) record.models.set(modelID, model) fn(model) model.id = modelID @@ -135,8 +135,8 @@ const layer = Layer.effect( } return result }, - finalize: Effect.fn("CatalogV2.finalize")(function* (catalog) { - yield* events.publish(Event.Updated, {}) + finalize: Effect.fn("Catalog.finalize")(function* (catalog) { + yield* bus.publish(Catalog.Event.Updated, {}) }), }) const result: Interface = { @@ -144,15 +144,15 @@ const layer = Layer.effect( reload: state.reload, provider: { - get: Effect.fn("CatalogV2.provider.get")(function* (providerID) { + get: Effect.fn("Catalog.provider.get")(function* (providerID) { return state.get().providers.get(providerID)?.provider }), - all: Effect.fn("CatalogV2.provider.all")(function* () { + all: Effect.fn("Catalog.provider.all")(function* () { return Array.fromIterable(state.get().providers.values()).map((record) => record.provider) }), - available: Effect.fn("CatalogV2.provider.available")(function* () { + available: Effect.fn("Catalog.provider.available")(function* () { const active = new Map((yield* integrations.list()).map((integration) => [integration.id, integration])) return (yield* result.provider.all()).filter((provider) => available(provider, active.get(provider.integrationID ?? Integration.ID.make(provider.id))), @@ -161,14 +161,14 @@ const layer = Layer.effect( }, model: { - get: Effect.fn("CatalogV2.model.get")(function* (providerID, modelID) { + get: Effect.fn("Catalog.model.get")(function* (providerID, modelID) { const record = state.get().providers.get(providerID) if (!record) return const model = record.models.get(modelID) return model && projectModel(model, record.provider) }), - all: Effect.fn("CatalogV2.model.all")(function* () { + all: Effect.fn("Catalog.model.all")(function* () { return pipe( Array.fromIterable(state.get().providers.values()), Array.flatMap((record) => { @@ -178,9 +178,9 @@ const layer = Layer.effect( ) }), - available: Effect.fn("CatalogV2.model.available")(function* () { + available: Effect.fn("Catalog.model.available")(function* () { const providers = new Set((yield* result.provider.available()).map((provider) => provider.id)) - const models: ModelV2.Info[] = [] + const models: Model.Info[] = [] for (const record of state.get().providers.values()) { if (!providers.has(record.provider.id)) continue for (const model of record.models.values()) { @@ -194,7 +194,7 @@ const layer = Layer.effect( ) }), - default: Effect.fn("CatalogV2.model.default")(function* () { + default: Effect.fn("Catalog.model.default")(function* () { const defaultModel = state.get().defaultModel if (defaultModel) { const provider = yield* result.provider.get(defaultModel.providerID) @@ -207,18 +207,18 @@ const layer = Layer.effect( return (yield* result.model.available())[0] }), - small: Effect.fn("CatalogV2.model.small")(function* (providerID) { + small: Effect.fn("Catalog.model.small")(function* (providerID) { const record = state.get().providers.get(providerID) if (!record) return const provider = record.provider // TODO: Remove these provider-specific assumptions once model syncing reliably reports available deployments. - if (providerID === ProviderV2.ID.azure || providerID === ProviderV2.ID.make("azure-cognitive-services")) { + if (providerID === Provider.ID.azure || providerID === Provider.ID.make("azure-cognitive-services")) { return } - if (providerID === ProviderV2.ID.opencode) { - const gpt5Nano = record.models.get(ModelV2.ID.make("gpt-5-nano")) + if (providerID === Provider.ID.opencode) { + const gpt5Nano = record.models.get(Model.ID.make("gpt-5-nano")) if (gpt5Nano?.enabled && gpt5Nano.status === "active") return projectModel(gpt5Nano, provider) } @@ -268,4 +268,4 @@ const layer = Layer.effect( const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/ -export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Integration.node] }) +export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node, Integration.node] }) diff --git a/packages/core/src/codemode.ts b/packages/core/src/codemode.ts deleted file mode 100644 index 6d858097f8bc..000000000000 --- a/packages/core/src/codemode.ts +++ /dev/null @@ -1,77 +0,0 @@ -export * as CodeMode from "./codemode" - -import { Context, Effect, Layer, Scope } from "effect" -import { CodeModeCatalog } from "./codemode/catalog" -import { makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { PermissionV2 } from "./permission" -import { ExecuteTool } from "./tool/execute" -import type { Any, Registration } from "./tool/tool" -import { Wildcard } from "./util/wildcard" - -export interface Materialization { - readonly tool?: Any - readonly catalog?: ReadonlyArray -} - -export interface Interface { - readonly register: ( - registrations: ReadonlyArray, - ) => Effect.Effect - readonly materialize: (permissions?: PermissionV2.Ruleset) => Effect.Effect -} - -export class Service extends Context.Service()("@opencode/v2/CodeMode") {} - -const layer = Layer.effect( - Service, - Effect.gen(function* () { - const local = new Map>() - - return Service.of({ - register: Effect.fn("CodeMode.register")(function* (registrations) { - if (registrations.length === 0) return - yield* Effect.uninterruptible( - Effect.gen(function* () { - const token = {} - for (const registration of registrations) - local.set(registration.key, [ - ...(local.get(registration.key) ?? []), - { - token, - registration, - }, - ]) - yield* Effect.addFinalizer(() => - Effect.sync(() => { - for (const registration of registrations) { - const remaining = local.get(registration.key)?.filter((item) => item.token !== token) ?? [] - if (remaining.length > 0) local.set(registration.key, remaining) - else local.delete(registration.key) - } - }), - ) - }), - ) - }), - materialize: Effect.fn("CodeMode.materialize")(function* (permissions) { - const registrations = new Map() - const rules = permissions ?? [] - for (const [name, entries] of local) { - const registration = entries.at(-1)?.registration - if (!registration) continue - const rule = rules.findLast((rule) => Wildcard.match(registration.permission, rule.action)) - if (rule?.resource === "*" && rule.effect === "deny") continue - registrations.set(name, registration) - } - const executeRule = rules.findLast((rule) => Wildcard.match("execute", rule.action)) - if (executeRule?.resource === "*" && executeRule.effect === "deny") return {} - return { - tool: ExecuteTool.create(registrations), - catalog: ExecuteTool.catalog(registrations), - } - }), - }) - }), -) - -export const node = makeLocationNode({ service: Service, layer, deps: [] }) diff --git a/packages/core/src/tool/execute.ts b/packages/core/src/codemode/tool.ts similarity index 81% rename from packages/core/src/tool/execute.ts rename to packages/core/src/codemode/tool.ts index edb22fa78b71..7d9c5106be6a 100644 --- a/packages/core/src/tool/execute.ts +++ b/packages/core/src/codemode/tool.ts @@ -1,10 +1,9 @@ -export * as ExecuteTool from "./execute" -export type { Registration } from "./tool" +export * as CodeModeTool from "./tool" import { CodeMode, Tool, toolError } from "@opencode-ai/codemode" -import type { ToolContent } from "@opencode-ai/ai" +import type { Content, Context, Error, Info, Metadata, Result } from "@opencode-ai/schema/tool" import { Effect, Ref, Schema, Semaphore } from "effect" -import { execute, make, toLLMDefinition, type Content, type Metadata, type Registration } from "./tool" +import { definition } from "../tool/runtime" const ExecuteFile = Schema.Struct({ data: Schema.String, @@ -42,8 +41,17 @@ const description = [ "Await every call whose completion matters; pending calls are interrupted when execution ends. Run independent calls concurrently with `Promise.all`.", ].join("\n") -export const create = (registrations: ReadonlyMap) => { - return make({ +export const create = ( + registrations: ReadonlyMap, + executeTool: ( + name: string, + tool: Info, + input: unknown, + context: Context, + ) => Effect.Effect, +) => { + return ({ + name: "execute", description, input: CodeMode.Input, output: ExecuteOutput, @@ -59,17 +67,17 @@ export const create = (registrations: ReadonlyMap) => { ) const result = yield* runtime( registrations, - (name, registration, input) => + (name, tool, input) => Effect.gen(function* () { const index = yield* Ref.getAndUpdate(callIndex, (index) => index + 1) - const executed = yield* execute(registration.tool, input, { - sessionID: context.sessionID, - agent: context.agent, - messageID: context.messageID, - callID: context.callID, - progress: () => Effect.void, - }).pipe(Effect.mapError((failure) => toolError(failure.message, failure))) - const outputFileParts = outputFiles(executed.content) + const executed = yield* executeTool(name, tool, input, context).pipe( + Effect.mapError((failure) => toolError(failure.message, failure)), + ) + const content = + typeof executed.content === "string" + ? [{ type: "text" as const, text: executed.content }] + : executed.content ?? [] + const outputFileParts = outputFiles(content) if (outputFileParts.length > 0) yield* Ref.update(files, (items) => [...items, { index, files: outputFileParts }]) return executed.output @@ -107,11 +115,11 @@ export const create = (registrations: ReadonlyMap) => { files: collected, ...(result.ok ? {} : { error: true }), } - const content: [Content, ...Content[]] = [{ type: "text", text: value.output }] + const content: Array = [{ type: "text", text: value.output }] content.push( ...value.files.map((file) => ({ type: "file" as const, - data: file.data, + uri: `data:${file.mime};base64,${file.data}`, mime: file.mime, ...(file.name === undefined ? {} : { name: file.name }), })), @@ -126,23 +134,26 @@ export const create = (registrations: ReadonlyMap) => { metadata, } }), - }) + }) satisfies Info } -export const catalog = (registrations: ReadonlyMap) => { +export const catalog = (registrations: ReadonlyMap) => { return runtime(registrations, () => Effect.fail(toolError("Execute context is unavailable"))).catalog() } function runtime( - registrations: ReadonlyMap, - executeTool: (name: string, registration: Registration, input: unknown) => Effect.Effect, + registrations: ReadonlyMap, + executeTool: (name: string, tool: Info, input: unknown) => Effect.Effect, hooks?: CodeMode.ToolCallHooks, ) { const tools: Record> = {} for (const [name, registration] of registrations) { - const child = toLLMDefinition(name, registration.tool) + const child = definition(registration) + const normalized = registration.name.replace(/[^a-zA-Z0-9_-]/g, "_") const path = - registration.namespace === undefined ? registration.name : `${registration.namespace}.${registration.name}` + registration.options?.namespace === undefined + ? normalized + : `${registration.options.namespace}.${normalized}` tools[path] = Tool.make({ description: child.description, input: child.inputSchema, @@ -180,7 +191,7 @@ function formatValue(value: CodeMode.DataValue) { return JSON.stringify(value, null, 2) ?? String(value) } -function outputFiles(content: ReadonlyArray): Array { +function outputFiles(content: ReadonlyArray): Array { return content.flatMap((part) => { if (part.type !== "file") return [] const prefix = `data:${part.mime};base64,` diff --git a/packages/core/src/command.ts b/packages/core/src/command.ts index f673231beb5f..7ce432af8db6 100644 --- a/packages/core/src/command.ts +++ b/packages/core/src/command.ts @@ -1,11 +1,11 @@ -export * as CommandV2 from "./command" +export * as Command from "./command" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Context, Effect, Layer, Schema, Types } from "effect" import { Command } from "@opencode-ai/schema/command" import { State } from "./state" import { MCP } from "./mcp/index" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { AppProcess } from "@opencode-ai/util/process" import { ChildProcess } from "effect/unstable/process" import { Config } from "./config" @@ -14,7 +14,7 @@ import { ShellSelect } from "./shell/select" export const Info = Command.Info export type Info = Command.Info -export const Event = Command.Event +export { Event } from "@opencode-ai/schema/command" export type Evaluation = { readonly text: string @@ -50,13 +50,13 @@ export interface Interface extends State.Transformable { }) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Command") {} +export class Service extends Context.Service()("@opencode/Command") {} export const layer = (options?: ShellSelect.Options) => Layer.effect( Service, Effect.gen(function* () { const mcp = yield* MCP.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const processes = yield* AppProcess.Service const config = yield* Config.Service const location = yield* Location.Service @@ -76,7 +76,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( draft.commands.delete(name) }, }), - finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid), + finalize: () => bus.publish(Command.Event.Updated, {}).pipe(Effect.asVoid), }) const staticCommand = (name: string) => state.get().commands.get(name) as Info | undefined const mcpCommands = Effect.fnUntraced(function* () { @@ -92,12 +92,12 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( return Service.of({ reload: state.reload, transform: state.transform, - get: Effect.fn("CommandV2.get")(function* (name) { + get: Effect.fn("Command.get")(function* (name) { const command = staticCommand(name) if (command) return command return (yield* mcpCommands()).find((command) => command.name === name) }), - list: Effect.fn("CommandV2.list")(function* () { + list: Effect.fn("Command.list")(function* () { const commands = Array.from(state.get().commands.values()) as Info[] const names = new Set(commands.map((command) => command.name)) return [ @@ -105,7 +105,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( ...(yield* mcpCommands()).filter((command) => !names.has(command.name)), ] }), - evaluate: Effect.fn("CommandV2.evaluate")(function* (input) { + evaluate: Effect.fn("Command.evaluate")(function* (input) { const command = staticCommand(input.name) if (command) return yield* evaluateTemplate(input.name, command.template, input.arguments ?? "", { config, @@ -247,7 +247,7 @@ export function configured(options?: ShellSelect.Options) { return makeLocationNode({ service: Service, layer: layer(options), - deps: [MCP.node, EventV2.node, AppProcess.node, Config.node, Location.node], + deps: [MCP.node, Bus.node, AppProcess.node, Config.node, Location.node], }) } diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 99cdff45e13a..b24b30f27b58 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -9,7 +9,7 @@ import { Permission } from "@opencode-ai/schema/permission" import { Config as ConfigSchema } from "@opencode-ai/schema/config" import { Integration } from "@opencode-ai/schema/integration" import { Credential } from "./credential" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { Watcher } from "./filesystem/watcher" import { FSUtil } from "@opencode-ai/util/fs-util" import { Global } from "@opencode-ai/util/global" @@ -168,7 +168,7 @@ export const Options = Schema.Struct({ }) export type Options = typeof Options.Type -export class Service extends Context.Service()("@opencode/v2/Config") {} +export class Service extends Context.Service()("@opencode/Config") {} export const layer = (options?: Options) => Layer.effect( Service, @@ -177,7 +177,7 @@ export const layer = (options?: Options) => Layer.effect( const global = yield* Global.Service const location = yield* Location.Service const watcher = yield* Watcher.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const credentials = yield* Credential.Service const wellknown = yield* WellKnown.Service const names = ["opencode.json", "opencode.jsonc"] @@ -375,7 +375,7 @@ export const layer = (options?: Options) => Layer.effect( if (isDeepStrictEqual(configs, next)) return configs = next yield* reconcile(next) - yield* events.publish(ConfigSchema.Event.Updated, {}) + yield* bus.publish(ConfigSchema.Event.Updated, {}) }), ), ) @@ -389,7 +389,7 @@ export const layer = (options?: Options) => Layer.effect( ), Effect.forkScoped({ startImmediately: true }), ) - yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe( + yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe( Stream.filterEffect((event) => wellknown.entries().pipe( Effect.map((entries) => entries.some((entry) => entry.integrationID === event.data.integrationID)), @@ -401,7 +401,7 @@ export const layer = (options?: Options) => Layer.effect( ), Effect.forkScoped({ startImmediately: true }), ) - yield* events.subscribe(WellKnown.Event.Updated).pipe( + yield* bus.subscribe(WellKnown.Event.Updated).pipe( Stream.runForEach(() => reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown sources", { cause }))), ), @@ -438,7 +438,7 @@ export function configured(options?: Options) { return makeLocationNode({ service: Service, layer: layer(options), - deps: [Watcher.node, EventV2.node, FSUtil.node, Global.node, Location.node, Credential.node, WellKnown.node], + deps: [Watcher.node, Bus.node, FSUtil.node, Global.node, Location.node, Credential.node, WellKnown.node], }) } diff --git a/packages/core/src/config/agent.ts b/packages/core/src/config/agent.ts index 075feea25075..94c7efb97b35 100644 --- a/packages/core/src/config/agent.ts +++ b/packages/core/src/config/agent.ts @@ -8,7 +8,7 @@ import { PositiveInt } from "../schema" export const Color = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)) -export class Info extends Schema.Class("ConfigV2.Agent")({ +export class Info extends Schema.Class("Config.Agent")({ model: ConfigModel.Selection.pipe(Schema.optional), request: ConfigProvider.Request.pipe(Schema.optional), system: Schema.String.pipe(Schema.optional), diff --git a/packages/core/src/config/attachments.ts b/packages/core/src/config/attachments.ts index f14775ff3f7d..ea7862c0fabd 100644 --- a/packages/core/src/config/attachments.ts +++ b/packages/core/src/config/attachments.ts @@ -3,13 +3,13 @@ export * as ConfigAttachments from "./attachments" import { Schema } from "effect" import { PositiveInt } from "../schema" -export class Image extends Schema.Class("ConfigV2.Attachments.Image")({ +export class Image extends Schema.Class("Config.Attachments.Image")({ auto_resize: Schema.Boolean.pipe(Schema.optional), max_width: PositiveInt.pipe(Schema.optional), max_height: PositiveInt.pipe(Schema.optional), max_base64_bytes: PositiveInt.pipe(Schema.optional), }) {} -export class Info extends Schema.Class("ConfigV2.Attachments")({ +export class Info extends Schema.Class("Config.Attachments")({ image: Image.pipe(Schema.optional), }) {} diff --git a/packages/core/src/config/command.ts b/packages/core/src/config/command.ts index 8168107fd154..ff9fdf926adc 100644 --- a/packages/core/src/config/command.ts +++ b/packages/core/src/config/command.ts @@ -3,7 +3,7 @@ export * as ConfigCommand from "./command" import { Schema } from "effect" import { ConfigModel } from "./model" -export class Info extends Schema.Class("ConfigV2.Command")({ +export class Info extends Schema.Class("Config.Command")({ template: Schema.String, description: Schema.String.pipe(Schema.optional), agent: Schema.String.pipe(Schema.optional), diff --git a/packages/core/src/config/compaction.ts b/packages/core/src/config/compaction.ts index fde64cd3622f..c52e0da7998d 100644 --- a/packages/core/src/config/compaction.ts +++ b/packages/core/src/config/compaction.ts @@ -3,11 +3,11 @@ export * as ConfigCompaction from "./compaction" import { Schema } from "effect" import { NonNegativeInt } from "../schema" -export class Keep extends Schema.Class("ConfigV2.Compaction.Keep")({ +export class Keep extends Schema.Class("Config.Compaction.Keep")({ tokens: NonNegativeInt.pipe(Schema.optional), }) {} -export class Info extends Schema.Class("ConfigV2.Compaction")({ +export class Info extends Schema.Class("Config.Compaction")({ auto: Schema.Boolean.pipe(Schema.optional), keep: Keep.pipe(Schema.optional), buffer: NonNegativeInt.pipe(Schema.optional), diff --git a/packages/core/src/config/formatter.ts b/packages/core/src/config/formatter.ts index e1f90302d1a9..5730ceeec68c 100644 --- a/packages/core/src/config/formatter.ts +++ b/packages/core/src/config/formatter.ts @@ -2,7 +2,7 @@ export * as ConfigFormatter from "./formatter" import { Schema } from "effect" -export class Entry extends Schema.Class("ConfigV2.Formatter.Entry")({ +export class Entry extends Schema.Class("Config.Formatter.Entry")({ disabled: Schema.Boolean.pipe(Schema.optional), command: Schema.String.pipe(Schema.Array, Schema.optional), environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), diff --git a/packages/core/src/config/lsp.ts b/packages/core/src/config/lsp.ts index 651597befdde..14c37718c9ad 100644 --- a/packages/core/src/config/lsp.ts +++ b/packages/core/src/config/lsp.ts @@ -6,7 +6,7 @@ export const Disabled = Schema.Struct({ disabled: Schema.Literal(true), }) -export class Server extends Schema.Class("ConfigV2.LSP.Server")({ +export class Server extends Schema.Class("Config.LSP.Server")({ command: Schema.String.pipe(Schema.Array), extensions: Schema.String.pipe(Schema.Array, Schema.optional), disabled: Schema.Boolean.pipe(Schema.optional), diff --git a/packages/core/src/config/mcp.ts b/packages/core/src/config/mcp.ts index 12c3a98e3150..96bb2978b8b6 100644 --- a/packages/core/src/config/mcp.ts +++ b/packages/core/src/config/mcp.ts @@ -15,7 +15,7 @@ export const Remote = Mcp.RemoteConfig export type Remote = Mcp.RemoteConfig export const Server = Mcp.ServerConfig -export class Info extends Schema.Class("ConfigV2.MCP")({ +export class Info extends Schema.Class("Config.MCP")({ timeout: Timeout.pipe(Schema.optional), servers: Schema.Record(Schema.String, Server).pipe(Schema.optional), }) {} diff --git a/packages/core/src/config/plugin.ts b/packages/core/src/config/plugin.ts index e5fd6661ff52..4268a7f79d26 100644 --- a/packages/core/src/config/plugin.ts +++ b/packages/core/src/config/plugin.ts @@ -2,7 +2,7 @@ export * as ConfigPlugin from "./plugin" import { Schema } from "effect" -export class Entry extends Schema.Class("ConfigV2.Plugin.Entry")({ +export class Entry extends Schema.Class("Config.Plugin.Entry")({ package: Schema.String, options: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), }) {} diff --git a/packages/core/src/config/plugin/agent.ts b/packages/core/src/config/plugin/agent.ts index 1254576e22af..0cb4c43f1d1a 100644 --- a/packages/core/src/config/plugin/agent.ts +++ b/packages/core/src/config/plugin/agent.ts @@ -1,9 +1,9 @@ export * as ConfigAgentPlugin from "./agent" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import path from "path" import { Effect, Option, Schema, Stream } from "effect" -import { AgentV2 } from "../../agent" +import { Agent } from "../../agent" import { Config } from "../../config" import { ConfigAgent } from "../agent" import { ConfigMarkdown } from "../markdown" @@ -11,10 +11,10 @@ import { FSUtil } from "@opencode-ai/util/fs-util" import { ConfigAgentV1 } from "../../v1/config/agent" import { ConfigMigrateV1 } from "../../v1/config/migrate" import { Global } from "@opencode-ai/util/global" -import { PermissionV2 } from "../../permission" +import { Permission } from "../../permission" import type { LocationMutation } from "../../location-mutation" -import type { ReadTool } from "../../tool/read" -import type { EditTool } from "../../tool/edit" +import type { ReadTool } from "../../tool/plugin/read" +import type { EditTool } from "../../tool/plugin/edit" const legacySources = [ { pattern: "{agent,agents}/**/*.md", primary: false }, @@ -74,14 +74,14 @@ export const Plugin = define({ global.home, ) const configuredDefault = Config.latest(loaded.documents, "default_agent") - if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault)) + if (configuredDefault !== undefined) draft.default(Agent.ID.make(configuredDefault)) for (const current of draft.list()) { draft.update(current.id, (agent) => agent.permissions.push(...permissions)) } for (const document of loaded.documents) { for (const [id, item] of Object.entries(document.info.agents ?? {})) { - const agentID = AgentV2.ID.make(id) + const agentID = Agent.ID.make(id) if (item.disabled) { draft.remove(agentID) continue @@ -126,7 +126,7 @@ export const Plugin = define({ }), }) -function expandPermissions(rules: PermissionV2.Ruleset, home: string): PermissionV2.Ruleset { +function expandPermissions(rules: Permission.Ruleset, home: string): Permission.Ruleset { // Expand only resources tools resolve as filesystem paths. Bash resources are raw shell text: // rewriting `$HOME/private/**` would miss `$HOME/private/key`, and safe expansion needs shell-aware parsing. return rules.map((rule) => diff --git a/packages/core/src/config/plugin/command.ts b/packages/core/src/config/plugin/command.ts index 29de7eddb57e..f42d62d259fe 100644 --- a/packages/core/src/config/plugin/command.ts +++ b/packages/core/src/config/plugin/command.ts @@ -1,9 +1,9 @@ export * as ConfigCommandPlugin from "./command" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import path from "path" import { Effect, Option, Schema, Stream } from "effect" -import { CommandV2 } from "../../command" +import { Command } from "../../command" import { Config } from "../../config" import { FSUtil } from "@opencode-ai/util/fs-util" import { ConfigCommand } from "../command" diff --git a/packages/core/src/config/plugin/policy.ts b/packages/core/src/config/plugin/policy.ts index fd05939d1736..97bacc51163a 100644 --- a/packages/core/src/config/plugin/policy.ts +++ b/packages/core/src/config/plugin/policy.ts @@ -1,6 +1,6 @@ export * as ConfigPolicyPlugin from "./policy" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Effect, Stream } from "effect" import { Config } from "../../config" import { Wildcard } from "../../util/wildcard" diff --git a/packages/core/src/config/plugin/provider.ts b/packages/core/src/config/plugin/provider.ts index b0dceca26e37..ec00ddf36c7b 100644 --- a/packages/core/src/config/plugin/provider.ts +++ b/packages/core/src/config/plugin/provider.ts @@ -1,10 +1,10 @@ export * as ConfigProviderPlugin from "./provider" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Money } from "@opencode-ai/schema/money" import { Effect, Stream } from "effect" import { Config } from "../../config" -import { ProviderV2 } from "../../provider" +import { Provider } from "../../provider" export const Plugin = define({ id: "opencode.config.provider", @@ -49,9 +49,9 @@ export const Plugin = define({ if (item.name !== undefined) provider.name = item.name if (item.package !== undefined) provider.package = item.package if (item.settings !== undefined) - provider.settings = ProviderV2.mergeOverlay(provider.settings, item.settings) - if (item.headers !== undefined) provider.headers = ProviderV2.mergeHeaders(provider.headers, item.headers) - if (item.body !== undefined) provider.body = ProviderV2.mergeOverlay(provider.body, item.body) + provider.settings = Provider.mergeOverlay(provider.settings, item.settings) + if (item.headers !== undefined) provider.headers = Provider.mergeHeaders(provider.headers, item.headers) + if (item.body !== undefined) provider.body = Provider.mergeOverlay(provider.body, item.body) }) for (const [id, config] of Object.entries(item.models ?? {})) { catalog.model.update(providerID, id, (model) => { @@ -62,9 +62,9 @@ export const Plugin = define({ model.compatibility = { ...model.compatibility, ...config.compatibility } if (config.package !== undefined) model.package = config.package if (config.settings !== undefined) - model.settings = ProviderV2.mergeOverlay(model.settings, config.settings) - if (config.headers !== undefined) model.headers = ProviderV2.mergeHeaders(model.headers, config.headers) - if (config.body !== undefined) model.body = ProviderV2.mergeOverlay(model.body, config.body) + model.settings = Provider.mergeOverlay(model.settings, config.settings) + if (config.headers !== undefined) model.headers = Provider.mergeHeaders(model.headers, config.headers) + if (config.body !== undefined) model.body = Provider.mergeOverlay(model.body, config.body) if (config.capabilities !== undefined) { model.capabilities = { tools: config.capabilities.tools, @@ -81,10 +81,10 @@ export const Plugin = define({ model.variants.push(existing) } if (variant.settings !== undefined) - existing.settings = ProviderV2.mergeOverlay(existing.settings, variant.settings) + existing.settings = Provider.mergeOverlay(existing.settings, variant.settings) if (variant.headers !== undefined) - existing.headers = ProviderV2.mergeHeaders(existing.headers, variant.headers) - if (variant.body !== undefined) existing.body = ProviderV2.mergeOverlay(existing.body, variant.body) + existing.headers = Provider.mergeHeaders(existing.headers, variant.headers) + if (variant.body !== undefined) existing.body = Provider.mergeOverlay(existing.body, variant.body) } } if (config.cost !== undefined) { diff --git a/packages/core/src/config/plugin/reference.ts b/packages/core/src/config/plugin/reference.ts index a790a7cc5486..c0b62e00e75e 100644 --- a/packages/core/src/config/plugin/reference.ts +++ b/packages/core/src/config/plugin/reference.ts @@ -1,6 +1,6 @@ export * as ConfigReferencePlugin from "./reference" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import path from "path" import { Effect, Stream } from "effect" import { Config } from "../../config" diff --git a/packages/core/src/config/plugin/skill.ts b/packages/core/src/config/plugin/skill.ts index c4271c4843d5..427a7f974b38 100644 --- a/packages/core/src/config/plugin/skill.ts +++ b/packages/core/src/config/plugin/skill.ts @@ -1,11 +1,11 @@ export * as ConfigSkillPlugin from "./skill" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import path from "path" import { Effect, Stream } from "effect" import { Config } from "../../config" import { AbsolutePath } from "../../schema" -import { SkillV2 } from "../../skill" +import { Skill } from "../../skill" import { Global } from "@opencode-ai/util/global" import { Location } from "../../location" @@ -23,7 +23,7 @@ export const Plugin = define({ const items = loaded.entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : [])) for (const directory of [...claude, ...agents]) { draft.source( - SkillV2.DirectorySource.make({ + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")), }), @@ -31,10 +31,10 @@ export const Plugin = define({ } for (const directory of directories) { draft.source( - SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }), + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }), ) draft.source( - SkillV2.DirectorySource.make({ + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")), }), @@ -42,12 +42,12 @@ export const Plugin = define({ } for (const item of items) { if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) { - draft.source(SkillV2.UrlSource.make({ type: "url", url: item })) + draft.source(Skill.UrlSource.make({ type: "url", url: item })) continue } const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item draft.source( - SkillV2.DirectorySource.make({ + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)), }), diff --git a/packages/core/src/config/plugin/websearch.ts b/packages/core/src/config/plugin/websearch.ts index 1a063549facd..343d1dfd610d 100644 --- a/packages/core/src/config/plugin/websearch.ts +++ b/packages/core/src/config/plugin/websearch.ts @@ -1,6 +1,6 @@ export * as ConfigWebSearchPlugin from "./websearch" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Effect, Stream } from "effect" import { Config } from "../../config" diff --git a/packages/core/src/config/provider.ts b/packages/core/src/config/provider.ts index 8441ef0828c0..feb92a5c5e56 100644 --- a/packages/core/src/config/provider.ts +++ b/packages/core/src/config/provider.ts @@ -2,7 +2,7 @@ export * as ConfigProvider from "./provider" import { Schema } from "effect" import { Money } from "@opencode-ai/schema/money" -import { ModelV2 } from "../model" +import { Capabilities, Compatibility, Family, ID, VariantID } from "../model" const JsonRecord = Schema.Record(Schema.String, Schema.Json) @@ -12,17 +12,17 @@ export const Overlays = { body: JsonRecord.pipe(Schema.optional), } -export class Request extends Schema.Class("ConfigV2.Provider.Request")({ +export class Request extends Schema.Class("Config.Provider.Request")({ headers: Overlays.headers, body: Overlays.body, }) {} -class Cache extends Schema.Class("ConfigV2.Model.Cost.Cache")({ +class Cache extends Schema.Class("Config.Model.Cost.Cache")({ read: Money.USDPerMillionTokens.pipe(Schema.optional), write: Money.USDPerMillionTokens.pipe(Schema.optional), }) {} -class Cost extends Schema.Class("ConfigV2.Model.Cost")({ +class Cost extends Schema.Class("Config.Model.Cost")({ tier: Schema.Struct({ type: Schema.Literal("context"), size: Schema.Int, @@ -32,22 +32,22 @@ class Cost extends Schema.Class("ConfigV2.Model.Cost")({ cache: Cache.pipe(Schema.optional), }) {} -class Limit extends Schema.Class("ConfigV2.Model.Limit")({ +class Limit extends Schema.Class("Config.Model.Limit")({ context: Schema.Int.pipe(Schema.optional), input: Schema.Int.pipe(Schema.optional), output: Schema.Int.pipe(Schema.optional), }) {} -class Model extends Schema.Class("ConfigV2.Model")({ - modelID: ModelV2.ID.pipe(Schema.optional), - family: ModelV2.Family.pipe(Schema.optional), +class Model extends Schema.Class("Config.Model")({ + modelID: ID.pipe(Schema.optional), + family: Family.pipe(Schema.optional), name: Schema.String.pipe(Schema.optional), - compatibility: ModelV2.Compatibility.pipe(Schema.optional), + compatibility: Compatibility.pipe(Schema.optional), package: Schema.String.pipe(Schema.optional), ...Overlays, - capabilities: ModelV2.Capabilities.pipe(Schema.optional), + capabilities: Capabilities.pipe(Schema.optional), variants: Schema.Struct({ - id: ModelV2.VariantID, + id: VariantID, ...Overlays, }).pipe(Schema.Array, Schema.optional), cost: Schema.Union([Cost, Cost.pipe(Schema.Array)]).pipe(Schema.optional), @@ -55,7 +55,7 @@ class Model extends Schema.Class("ConfigV2.Model")({ limit: Limit.pipe(Schema.optional), }) {} -export class Info extends Schema.Class("ConfigV2.Provider")({ +export class Info extends Schema.Class("Config.Provider")({ name: Schema.String.pipe(Schema.optional), env: Schema.String.pipe(Schema.Array, Schema.optional), package: Schema.String.pipe(Schema.optional), diff --git a/packages/core/src/config/reference.ts b/packages/core/src/config/reference.ts index 4518eb4f87d6..d2fabb0a8997 100644 --- a/packages/core/src/config/reference.ts +++ b/packages/core/src/config/reference.ts @@ -2,14 +2,14 @@ export * as ConfigReference from "./reference" import { Schema } from "effect" -export class Git extends Schema.Class("ConfigV2.Reference.Git")({ +export class Git extends Schema.Class("Config.Reference.Git")({ repository: Schema.String, branch: Schema.String.pipe(Schema.optional), description: Schema.String.pipe(Schema.optional), hidden: Schema.Boolean.pipe(Schema.optional), }) {} -export class Local extends Schema.Class("ConfigV2.Reference.Local")({ +export class Local extends Schema.Class("Config.Reference.Local")({ path: Schema.String, description: Schema.String.pipe(Schema.optional), hidden: Schema.Boolean.pipe(Schema.optional), diff --git a/packages/core/src/config/tool-output.ts b/packages/core/src/config/tool-output.ts index 53e4d4d088b7..0eac04307dc0 100644 --- a/packages/core/src/config/tool-output.ts +++ b/packages/core/src/config/tool-output.ts @@ -3,7 +3,7 @@ export * as ConfigToolOutput from "./tool-output" import { Schema } from "effect" import { PositiveInt } from "../schema" -export class Info extends Schema.Class("ConfigV2.ToolOutput")({ +export class Info extends Schema.Class("Config.ToolOutput")({ max_lines: PositiveInt.pipe(Schema.optional), max_bytes: PositiveInt.pipe(Schema.optional), }) {} diff --git a/packages/core/src/config/warming.ts b/packages/core/src/config/warming.ts index 5e3b7d4a913a..cb90afc2eaf0 100644 --- a/packages/core/src/config/warming.ts +++ b/packages/core/src/config/warming.ts @@ -2,7 +2,7 @@ export * as ConfigWarming from "./warming" import { Schema } from "effect" -export class Info extends Schema.Class("ConfigV2.Warming")({ +export class Info extends Schema.Class("Config.Warming")({ prompt: Schema.String.pipe(Schema.optional).annotate({ description: "Prompt sent for keep-alive requests", }), diff --git a/packages/core/src/config/watcher.ts b/packages/core/src/config/watcher.ts index 2df6c876bfc6..be5c91a9bfeb 100644 --- a/packages/core/src/config/watcher.ts +++ b/packages/core/src/config/watcher.ts @@ -2,6 +2,6 @@ export * as ConfigWatcher from "./watcher" import { Schema } from "effect" -export class Info extends Schema.Class("ConfigV2.Watcher")({ +export class Info extends Schema.Class("Config.Watcher")({ ignore: Schema.String.pipe(Schema.Array, Schema.optional), }) {} diff --git a/packages/core/src/control-plane/move-session.ts b/packages/core/src/control-plane/move-session.ts index b5d229b7157e..220c503f6158 100644 --- a/packages/core/src/control-plane/move-session.ts +++ b/packages/core/src/control-plane/move-session.ts @@ -5,8 +5,8 @@ import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { FSUtil } from "@opencode-ai/util/fs-util" import { Git } from "../git" import { Global } from "@opencode-ai/util/global" -import { ProjectV2 } from "../project" -import { SessionV2 } from "../session" +import { Project } from "../project" +import { Session } from "../session" import { SessionExecution } from "../session/execution" import { SessionSchema } from "../session/schema" import { SessionStore } from "../session/store" @@ -28,8 +28,8 @@ export type Input = typeof Input.Type export class DestinationProjectMismatchError extends Schema.TaggedErrorClass()( "MoveSession.DestinationProjectMismatchError", { - expected: ProjectV2.ID, - actual: ProjectV2.ID, + expected: Project.ID, + actual: Project.ID, }, ) {} @@ -64,12 +64,12 @@ export class ResetSourceChangesError extends Schema.TaggedErrorClass().primaryKey(), + id: text().$type().primaryKey(), type: text().notNull(), name: text().notNull().default(""), branch: text(), directory: text(), extra: text({ mode: "json" }), project_id: text() - .$type() + .$type() .notNull() .references(() => ProjectTable.id, { onDelete: "cascade" }), time_used: integer() diff --git a/packages/core/src/credential.ts b/packages/core/src/credential.ts index be8b97db3d65..47f35c809b17 100644 --- a/packages/core/src/credential.ts +++ b/packages/core/src/credential.ts @@ -46,7 +46,7 @@ export interface Interface { readonly remove: (id: ID) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Credential") {} +export class Service extends Context.Service()("@opencode/Credential") {} const layer = Layer.effect( Service, diff --git a/packages/core/src/database/database.ts b/packages/core/src/database/database.ts index b45958867ef5..3150011717b1 100644 --- a/packages/core/src/database/database.ts +++ b/packages/core/src/database/database.ts @@ -20,7 +20,7 @@ export const Options = Schema.Struct({ }) export type Options = typeof Options.Type -export class Service extends Context.Service()("@opencode/v2/storage/Database") {} +export class Service extends Context.Service()("@opencode/storage/Database") {} const databaseLayer = Layer.effect( Service, diff --git a/packages/core/src/event-logger.ts b/packages/core/src/event-logger.ts index 12cc9c490ec0..eebdb10101ee 100644 --- a/packages/core/src/event-logger.ts +++ b/packages/core/src/event-logger.ts @@ -2,7 +2,7 @@ export * as EventLogger from "./event-logger" import { Effect, Layer } from "effect" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" -import { EventV2 } from "./event" +import { Bus } from "./bus" const Types = new Set([ "agent.updated", @@ -13,12 +13,12 @@ const Types = new Set([ export const layer = Layer.effectDiscard( Effect.gen(function* () { - const events = yield* EventV2.Service - const unsubscribe = yield* events.listen((event) => + const bus = yield* Bus.Service + const unsubscribe = yield* bus.listen((event) => Types.has(event.type) ? Effect.logInfo("event", { event }) : Effect.void, ) yield* Effect.addFinalizer(() => unsubscribe) }), ) -export const node = makeGlobalNode({ name: "event-logger", layer, deps: [EventV2.node] }) +export const node = makeGlobalNode({ name: "event-logger", layer, deps: [Bus.node] }) diff --git a/packages/core/src/event/sql.ts b/packages/core/src/event/sql.ts index 17c88cefd3ce..966003c3179c 100644 --- a/packages/core/src/event/sql.ts +++ b/packages/core/src/event/sql.ts @@ -1,5 +1,5 @@ import { sqliteTable, text, integer, index, uniqueIndex } from "drizzle-orm/sqlite-core" -import type { EventV2 } from "../event" +import { Event } from "@opencode-ai/schema/event" export const EventSequenceTable = sqliteTable("event_sequence", { aggregate_id: text().notNull().primaryKey(), @@ -10,7 +10,7 @@ export const EventSequenceTable = sqliteTable("event_sequence", { export const EventTable = sqliteTable( "event", { - id: text().$type().primaryKey(), + id: text().$type().primaryKey(), aggregate_id: text() .notNull() .references(() => EventSequenceTable.aggregate_id, { onDelete: "cascade" }), diff --git a/packages/core/src/file-mutation.ts b/packages/core/src/file-mutation.ts index d11144d5767e..82312b22da65 100644 --- a/packages/core/src/file-mutation.ts +++ b/packages/core/src/file-mutation.ts @@ -64,7 +64,7 @@ export interface Interface { readonly remove: (input: RemoveInput) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/FileMutation") {} +export class Service extends Context.Service()("@opencode/FileMutation") {} /** * Serialize file changes by canonical target. Conditional writes compare and @@ -194,12 +194,12 @@ function sameBytes(left: Uint8Array, right: Uint8Array) { export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node] }) /** - * Deferred until the corresponding V2 integrations exist. + * Deferred until the corresponding integrations exist. */ -// TODO: Add formatter integration after V2 formatter runtime exists. -// TODO: Publish watcher/file-edit events after V2 watcher integration exists. -// TODO: Add snapshots / undo after V2 snapshot design exists. -// TODO: Notify LSP and collect diagnostics after V2 LSP runtime exists. +// TODO: Add formatter integration after formatter runtime exists. +// TODO: Publish watcher/file-edit events after watcher integration exists. +// TODO: Add snapshots / undo after snapshot design exists. +// TODO: Notify LSP and collect diagnostics after LSP runtime exists. // TODO: Design multi-file transactions / rollback if patch needs atomic edits. // Until then, edits are sequential and report partial application. // TODO: Define crash recovery and idempotency for side effects between Tool.Called and durable settlement. diff --git a/packages/core/src/filesystem.ts b/packages/core/src/filesystem.ts index 3d0f54af6cba..0e5f24e677c5 100644 --- a/packages/core/src/filesystem.ts +++ b/packages/core/src/filesystem.ts @@ -56,7 +56,7 @@ export interface Interface { readonly grep: (input: GrepInput) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/FileSystem") {} +export class Service extends Context.Service()("@opencode/FileSystem") {} const baseLayer = Layer.effect( Service, diff --git a/packages/core/src/filesystem/location-watcher.ts b/packages/core/src/filesystem/location-watcher.ts index ff18d2cbee8e..25c983205990 100644 --- a/packages/core/src/filesystem/location-watcher.ts +++ b/packages/core/src/filesystem/location-watcher.ts @@ -6,7 +6,7 @@ import { FileSystem } from "@opencode-ai/schema/filesystem" import os from "os" import path from "path" import { Config } from "../config" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { FSUtil } from "@opencode-ai/util/fs-util" import { Git } from "../git" import { Location } from "../location" @@ -30,12 +30,12 @@ const layer = Layer.effect( Effect.gen(function* () { const location = yield* Location.Service const watcher = yield* Watcher.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const fs = yield* FSUtil.Service const git = yield* Git.Service const configService = yield* Config.Service const publish = (update: { type: "create" | "update" | "delete"; path: string }) => - events.publish(FileSystem.Event.Changed, { + bus.publish(FileSystem.Event.Changed, { file: update.path, event: update.type === "create" ? "add" : update.type === "update" ? "change" : "unlink", }) @@ -86,5 +86,5 @@ const layer = Layer.effect( export const node = makeLocationNode({ service: Service, layer, - deps: [Watcher.node, FSUtil.node, Location.node, Config.node, Git.node, EventV2.node], + deps: [Watcher.node, FSUtil.node, Location.node, Config.node, Git.node, Bus.node], }) diff --git a/packages/core/src/filesystem/search.ts b/packages/core/src/filesystem/search.ts index 2bc92a5994d9..23473381e543 100644 --- a/packages/core/src/filesystem/search.ts +++ b/packages/core/src/filesystem/search.ts @@ -22,7 +22,7 @@ export const Options = Schema.Struct({ }) export type Options = typeof Options.Type -export class Service extends Context.Service()("@opencode/v2/FileSystem/Search") {} +export class Service extends Context.Service()("@opencode/FileSystem/Search") {} export const ripgrepLayer = Layer.effect( Service, diff --git a/packages/core/src/form.ts b/packages/core/src/form.ts index bef4ad4f5426..0a23ef3efe90 100644 --- a/packages/core/src/form.ts +++ b/packages/core/src/form.ts @@ -3,7 +3,7 @@ export * as Form from "./form" import { Form } from "@opencode-ai/schema/form" import { Cache, Context, Deferred, Duration, Effect, Exit, Layer, Option, Schema } from "effect" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { EventV2 } from "./event" +import { Bus } from "./bus" const RETENTION = Duration.minutes(10) @@ -32,7 +32,7 @@ export type Answer = typeof Answer.Type export const Reply = Form.Reply export type Reply = typeof Reply.Type -export const Event = Form.Event +export { Event } from "@opencode-ai/schema/form" export class NotFoundError extends Schema.TaggedErrorClass()("Form.NotFoundError", { id: ID, @@ -99,7 +99,7 @@ interface Entry { export const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const forms = yield* Cache.makeWith( () => Effect.die(new Error("Form cache must be used via set/getSuccess, never get")), { @@ -141,7 +141,7 @@ export const layer = Layer.effect( deferred: yield* Deferred.make(), } yield* Cache.set(forms, id, entry) - yield* events.publish(Event.Created, { form }).pipe(Effect.onError(() => Cache.invalidate(forms, id))) + yield* bus.publish(Form.Event.Created, { form }).pipe(Effect.onError(() => Cache.invalidate(forms, id))) return form }), ), @@ -183,7 +183,7 @@ export const layer = Layer.effect( const invalid = validateAnswer(entry.form, input.answer) if (invalid) return yield* new InvalidAnswerError({ id: input.id, message: invalid }) const next: TerminalState = { status: "answered", answer: input.answer } - yield* events.publish(Event.Replied, { id: input.id, sessionID: entry.form.sessionID, answer: input.answer }) + yield* bus.publish(Form.Event.Replied, { id: input.id, sessionID: entry.form.sessionID, answer: input.answer }) yield* Cache.set(forms, input.id, { ...entry, state: next }) yield* Deferred.succeed(entry.deferred, next) }), @@ -196,7 +196,7 @@ export const layer = Layer.effect( const entry = yield* find(id) if (entry.state.status !== "pending") return yield* new AlreadySettledError({ id }) const next: TerminalState = { status: "cancelled" } - yield* events.publish(Event.Cancelled, { id, sessionID: entry.form.sessionID }) + yield* bus.publish(Form.Event.Cancelled, { id, sessionID: entry.form.sessionID }) yield* Cache.set(forms, id, { ...entry, state: next }) yield* Deferred.succeed(entry.deferred, next) }), @@ -221,7 +221,7 @@ export const layer = Layer.effect( export const locationLayer = layer -export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] }) +export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node] }) function validateAnswer(form: Info, answer: Answer) { const fields = new Map(form.fields.map((field) => [field.key, field] as const)) diff --git a/packages/core/src/generate.ts b/packages/core/src/generate.ts index 432827bce0b6..ceec4de44499 100644 --- a/packages/core/src/generate.ts +++ b/packages/core/src/generate.ts @@ -5,11 +5,11 @@ import { Context, Effect, Layer, Schema } from "effect" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { llmClient } from "./effect/app-node-platform" import { ModelResolver } from "./model-resolver" -import { ModelV2 } from "./model" +import { Model } from "./model" export interface TextInput { readonly prompt: string - readonly model?: ModelV2.Ref + readonly model?: Model.Ref } export class ModelSelectionError extends Schema.TaggedErrorClass()( @@ -28,7 +28,7 @@ export interface Interface { readonly text: (input: TextInput) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Generate") {} +export class Service extends Context.Service()("@opencode/Generate") {} export const layer = Layer.effect( Service, diff --git a/packages/core/src/git.ts b/packages/core/src/git.ts index 8802ca7317a9..c874ead415cb 100644 --- a/packages/core/src/git.ts +++ b/packages/core/src/git.ts @@ -189,7 +189,7 @@ export interface Interface { } } -export class Service extends Context.Service()("@opencode/GitV2") {} +export class Service extends Context.Service()("@opencode/Git") {} const layer = Layer.effect( Service, diff --git a/packages/core/src/github-copilot/models.ts b/packages/core/src/github-copilot/models.ts index 52f98753b5f1..34dfe2e4cc81 100644 --- a/packages/core/src/github-copilot/models.ts +++ b/packages/core/src/github-copilot/models.ts @@ -2,8 +2,8 @@ export * as CopilotModels from "./models" import { Money } from "@opencode-ai/schema/money" import { Option, Schema } from "effect" -import { ModelV2 } from "../model" -import { ProviderV2 } from "../provider" +import { Model } from "../model" +import { Provider } from "../provider" const RemoteModel = Schema.Struct({ model_picker_enabled: Schema.Boolean, @@ -70,7 +70,7 @@ type UsableModel = RemoteModel & { } } -export async function get(baseURL: string, headers: RequestInit["headers"], existing: readonly ModelV2.Info[]) { +export async function get(baseURL: string, headers: RequestInit["headers"], existing: readonly Model.Info[]) { const response = await fetch(`${baseURL}/models`, { headers, signal: AbortSignal.timeout(5_000), @@ -97,7 +97,7 @@ export async function get(baseURL: string, headers: RequestInit["headers"], exis } for (const [id, model] of remote) { - const key = ModelV2.ID.make(id) + const key = Model.ID.make(id) if (result.has(key)) continue result.set(key, build(key, model, baseURL)) } @@ -114,7 +114,7 @@ function usable(model: RemoteModel): model is UsableModel { ) } -function build(id: ModelV2.ID, remote: UsableModel, baseURL: string, previous?: ModelV2.Info) { +function build(id: Model.ID, remote: UsableModel, baseURL: string, previous?: Model.Info) { const messages = remote.supported_endpoints?.includes("/v1/messages") ?? false const endpoint = messages ? "messages" @@ -134,15 +134,15 @@ function build(id: ModelV2.ID, remote: UsableModel, baseURL: string, previous?: : remote.version const released = previous?.time.released || Date.parse(version) - return ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.githubCopilot, id), + return Model.Info.make({ + ...Model.Info.default(Provider.ID.githubCopilot, id), id, - modelID: ModelV2.ID.make(remote.id), - providerID: ProviderV2.ID.githubCopilot, - family: previous?.family ?? ModelV2.Family.make(remote.capabilities.family), + modelID: Model.ID.make(remote.id), + providerID: Provider.ID.githubCopilot, + family: previous?.family ?? Model.Family.make(remote.capabilities.family), name: previous?.name ?? remote.name, - package: ProviderV2.aisdk(messages ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot"), - settings: ProviderV2.mergeOverlay(previous?.settings, { + package: Provider.aisdk(messages ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot"), + settings: Provider.mergeOverlay(previous?.settings, { baseURL: messages ? `${baseURL}/v1` : baseURL, ...(endpoint ? { endpoint } : {}), }), @@ -175,11 +175,11 @@ function build(id: ModelV2.ID, remote: UsableModel, baseURL: string, previous?: }) } -function variants(remote: UsableModel, messages: boolean): ModelV2.Info["variants"] { +function variants(remote: UsableModel, messages: boolean): Model.Info["variants"] { const efforts = remote.capabilities.supports.reasoning_effort ?? [] if (!messages && efforts.length) { return efforts.map((effort) => ({ - id: ModelV2.VariantID.make(effort), + id: Model.VariantID.make(effort), settings: { reasoningEffort: effort, reasoningSummary: "auto", @@ -189,7 +189,7 @@ function variants(remote: UsableModel, messages: boolean): ModelV2.Info["variant } if (efforts.length && remote.capabilities.supports.adaptive_thinking) { return efforts.map((effort) => ({ - id: ModelV2.VariantID.make(effort), + id: Model.VariantID.make(effort), settings: { thinking: { type: "adaptive", @@ -203,11 +203,11 @@ function variants(remote: UsableModel, messages: boolean): ModelV2.Info["variant if (max === undefined) return [] return [ { - id: ModelV2.VariantID.make("max"), + id: Model.VariantID.make("max"), settings: { thinking: { type: "enabled", budgetTokens: max - 1 } }, }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { thinking: { type: "enabled", budgetTokens: Math.floor(max / 2) } }, }, ] diff --git a/packages/core/src/instruction-discovery.ts b/packages/core/src/instruction-discovery.ts index 910e92f8029a..834d1e0cc00f 100644 --- a/packages/core/src/instruction-discovery.ts +++ b/packages/core/src/instruction-discovery.ts @@ -26,7 +26,7 @@ export const Options = Schema.Struct({ }) export type Options = typeof Options.Type -export class Service extends Context.Service()("@opencode/v2/InstructionDiscovery") {} +export class Service extends Context.Service()("@opencode/InstructionDiscovery") {} export const layer = (options?: Options) => Layer.effect( Service, diff --git a/packages/core/src/instructions/builtins.ts b/packages/core/src/instructions/builtins.ts index 03b38d75b7b7..fdb8f87fcdf5 100644 --- a/packages/core/src/instructions/builtins.ts +++ b/packages/core/src/instructions/builtins.ts @@ -10,7 +10,7 @@ export interface Interface { readonly load: (sessionID: SessionSchema.ID) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/InstructionBuiltIns") {} +export class Service extends Context.Service()("@opencode/InstructionBuiltIns") {} const layer = Layer.effect( Service, diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 5c9548bccb03..a14635a57645 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -20,7 +20,7 @@ import { import { Integration } from "@opencode-ai/schema/integration" import { Credential } from "./credential" import { State } from "./state" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { IntegrationConnection } from "./integration/connection" import { AppProcess } from "@opencode-ai/util/process" import { ChildProcess } from "effect/unstable/process" @@ -129,7 +129,7 @@ export class AuthorizationError extends Schema.TaggedErrorClass { } } -export class Service extends Context.Service()("@opencode/v2/Integration") {} +export class Service extends Context.Service()("@opencode/Integration") {} const attemptLifetime = Duration.toMillis(Duration.minutes(10)) const terminalRetention = Duration.toMillis(Duration.minutes(1)) @@ -271,7 +271,7 @@ const layer = Layer.effect( Service, Effect.gen(function* () { const credentials = yield* Credential.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const processes = yield* AppProcess.Service const scope = yield* Scope.Scope const attempts = SynchronizedRef.makeUnsafe(new Map()) @@ -338,7 +338,7 @@ const layer = Layer.effect( }, }, }), - finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid), + finalize: () => bus.publish(Integration.Event.Updated, {}).pipe(Effect.asVoid), }) const resolveConnections = (entry: Entry | undefined, saved: readonly Credential.Info[]) => { @@ -433,8 +433,8 @@ const layer = Layer.effect( // Persisting attempts cannot be cancelled, expired, or claimed again. yield* SynchronizedRef.update(attempts, (current) => new Map(current).set(attemptID, terminal)) if (Exit.isFailure(persistence)) yield* Effect.failCause(persistence.cause) - yield* events.publish(Event.ConnectionUpdated, { integrationID: attempt.integrationID }) - yield* events.publish(Event.Updated, {}) + yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: attempt.integrationID }) + yield* bus.publish(Integration.Event.Updated, {}) }).pipe(Effect.ensuring(close(attempt.scope))) }), ) @@ -489,8 +489,8 @@ const layer = Layer.effect( yield* SynchronizedRef.update(commandAttempts, (current) => new Map(current).set(attemptID, terminal)) yield* close(attempt.scope) if (Exit.isFailure(persistence)) return - yield* events.publish(Event.ConnectionUpdated, { integrationID: attempt.integrationID }) - yield* events.publish(Event.Updated, {}) + yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: attempt.integrationID }) + yield* bus.publish(Integration.Event.Updated, {}) }), ) }) @@ -706,24 +706,24 @@ const layer = Layer.effect( label: input.label, value: Credential.Key.make({ type: "key", key: input.key }), }) - yield* events.publish(Event.ConnectionUpdated, { integrationID: input.integrationID }) - yield* events.publish(Event.Updated, {}) + yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: input.integrationID }) + yield* bus.publish(Integration.Event.Updated, {}) }), update: Effect.fn("Integration.connection.update")(function* (credentialID, updates) { const credential = yield* credentials.get(credentialID) yield* credentials.update(credentialID, updates) if (credential) { - yield* events.publish(Event.ConnectionUpdated, { integrationID: credential.integrationID }) + yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: credential.integrationID }) } - yield* events.publish(Event.Updated, {}) + yield* bus.publish(Integration.Event.Updated, {}) }), remove: Effect.fn("Integration.connection.remove")(function* (credentialID) { const credential = yield* credentials.get(credentialID) yield* credentials.remove(credentialID) if (credential) { - yield* events.publish(Event.ConnectionUpdated, { integrationID: credential.integrationID }) + yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: credential.integrationID }) } - yield* events.publish(Event.Updated, {}) + yield* bus.publish(Integration.Event.Updated, {}) }), }, oauth: { @@ -809,5 +809,5 @@ const layer = Layer.effect( export const node = makeLocationNode({ service: Service, layer, - deps: [Credential.node, EventV2.node, AppProcess.node], + deps: [Credential.node, Bus.node, AppProcess.node], }) diff --git a/packages/core/src/kv.ts b/packages/core/src/kv.ts index 92b7bd9c2c09..505beff7ec06 100644 --- a/packages/core/src/kv.ts +++ b/packages/core/src/kv.ts @@ -14,7 +14,7 @@ export interface Interface { readonly remove: (key: string) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/KV") {} +export class Service extends Context.Service()("@opencode/KV") {} const layer = Layer.effect( Service, diff --git a/packages/core/src/location-mutation.ts b/packages/core/src/location-mutation.ts index 0de177aafe4d..313b4dc79efc 100644 --- a/packages/core/src/location-mutation.ts +++ b/packages/core/src/location-mutation.ts @@ -60,7 +60,7 @@ export interface Interface { readonly resolve: (input: ResolveInput) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/LocationMutation") {} +export class Service extends Context.Service()("@opencode/LocationMutation") {} interface ResolvedPath { readonly canonical: string diff --git a/packages/core/src/location-services.ts b/packages/core/src/location-services.ts index 3cce750142c5..7a39aa278a1b 100644 --- a/packages/core/src/location-services.ts +++ b/packages/core/src/location-services.ts @@ -1,13 +1,12 @@ import { Effect, Layer, LayerMap } from "effect" -import { AgentV2 } from "./agent" +import { Agent } from "./agent" import { AISDK } from "./aisdk" import { Catalog } from "./catalog" -import { CodeMode } from "./codemode" -import { CommandV2 } from "./command" +import { Command } from "./command" import { Config } from "./config" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Node } from "@opencode-ai/util/effect/app-node" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { FileMutation } from "./file-mutation" import { FileSystem } from "./filesystem" import { FileSystemSearch } from "./filesystem/search" @@ -21,12 +20,12 @@ import { LocationMutation } from "./location-mutation" import { LocationServiceMap } from "./location-service-map" import { ModelResolver } from "./model-resolver" import { MCP } from "./mcp/index" -import { PermissionV2 } from "./permission" -import { PluginV2 } from "./plugin" +import { Permission } from "./permission" +import { Plugin } from "./plugin" import { PluginSupervisor } from "./plugin/supervisor" import { ProjectCopy } from "./project/copy" import { Pty } from "./pty" -import { QuestionV2 } from "./question" +import { Question } from "./question" import { Shell } from "./shell" import { Reference } from "./reference" import { WebSearch } from "./websearch" @@ -35,7 +34,7 @@ import { SessionRunnerLLM } from "./session/runner/llm" import { SessionRunnerModel } from "./session/runner/model" import { SessionCompaction } from "./session/compaction" import { SessionTitle } from "./session/title" -import { SkillV2 } from "./skill" +import { Skill } from "./skill" import { SkillInstructions } from "./skill/instructions" import { Snapshot } from "./snapshot" import { InstructionDiscovery } from "./instruction-discovery" @@ -45,8 +44,7 @@ import { SessionInstructions } from "./session/instructions" import { SessionGenerateNode } from "./session/generate-node" import { McpTool } from "./tool/mcp" import { ReadToolFileSystem } from "./tool/read-filesystem" -import { ToolRegistry } from "./tool/registry" -import { ToolOutputStore } from "./tool-output-store" +import { Tool } from "./tool" import { Vcs } from "./vcs" export { LocationServiceMap } from "./location-service-map" @@ -54,15 +52,15 @@ export { LocationServiceMap } from "./location-service-map" const locationServiceNodes = [ Location.node, Config.node, - AgentV2.node, - CommandV2.node, + Agent.node, + Command.node, Reference.node, WebSearch.node, Integration.node, Catalog.node, ModelResolver.node, AISDK.node, - PluginV2.node, + Plugin.node, PluginSupervisor.node, ProjectCopy.node, ProjectCopy.refreshNode, @@ -70,23 +68,20 @@ const locationServiceNodes = [ FileSystem.node, Pty.node, Shell.node, - SkillV2.node, - CodeMode.node, + Skill.node, InstructionBuiltIns.node, InstructionDiscovery.node, LocationMutation.node, FileMutation.node, MCP.node, - PermissionV2.node, - ToolOutputStore.node, - ToolRegistry.node, - ToolRegistry.toolsNode, + Permission.node, + Tool.node, Image.node, SkillInstructions.node, ReferenceInstructions.node, InstructionEntry.node, Form.node, - QuestionV2.node, + Question.node, Generate.node, SessionGenerateNode.node, ReadToolFileSystem.node, diff --git a/packages/core/src/mcp/index.ts b/packages/core/src/mcp/index.ts index 225919c27f4e..a325311ade86 100644 --- a/packages/core/src/mcp/index.ts +++ b/packages/core/src/mcp/index.ts @@ -9,7 +9,7 @@ import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Config } from "../config" import { ConfigMCP } from "../config/mcp" import { Credential } from "../credential" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { Form } from "../form" import { Integration } from "../integration" import { IntegrationConnection } from "../integration/connection" @@ -155,7 +155,7 @@ export interface Interface { }) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/MCP") {} +export class Service extends Context.Service()("@opencode/MCP") {} export const Options = Schema.Struct({ clientInfo: Schema.optional( @@ -172,7 +172,7 @@ export const layer = (options?: Options) => Layer.effect( Effect.gen(function* () { const config = yield* Config.Service const location = yield* Location.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const forms = yield* Form.Service const integration = yield* Integration.Service const credentials = yield* Credential.Service @@ -433,9 +433,9 @@ export const layer = (options?: Options) => Layer.effect( Effect.map((defs) => { entry.prompts = defs.map((def) => toPrompt(name, def)) }), - Effect.andThen(events.publish(Command.Event.Updated, {})), + Effect.andThen(bus.publish(Command.Event.Updated, {})), Effect.catch(() => - Effect.sync(() => (entry.prompts = [])).pipe(Effect.andThen(events.publish(Command.Event.Updated, {}))), + Effect.sync(() => (entry.prompts = [])).pipe(Effect.andThen(bus.publish(Command.Event.Updated, {}))), ), ) @@ -460,10 +460,10 @@ export const layer = (options?: Options) => Layer.effect( entry.tools = undefined entry.prompts = undefined entry.status = { status: "failed", error: "Connection closed" } - yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore) - yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore) - yield* events.publish(Command.Event.Updated, {}).pipe(Effect.ignore) - yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(Command.Event.Updated, {}).pipe(Effect.ignore) + yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) }), ), ) @@ -471,12 +471,12 @@ export const layer = (options?: Options) => Layer.effect( connection.onToolsChanged(() => live( refreshTools(name, entry, connection).pipe( - Effect.andThen(events.publish(McpEvent.ToolsChanged, { server: name })), + Effect.andThen(bus.publish(McpEvent.ToolsChanged, { server: name })), ), ), ) connection.onPromptsChanged(() => live(refreshPrompts(name, entry, connection))) - connection.onResourcesChanged(() => live(events.publish(McpEvent.ResourcesChanged, { server: name }))) + connection.onResourcesChanged(() => live(bus.publish(McpEvent.ResourcesChanged, { server: name }))) } const serverLog = (server: ServerName, message: MCPClient.LogMessage) => { @@ -502,7 +502,7 @@ export const layer = (options?: Options) => Layer.effect( // Announce the handshake so connect() and credential reconnects don't show a stale // disabled/failed status for the duration of the connection attempt. entry.status = { status: "pending" } - yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) const scope = yield* Scope.fork(root) entry.scope = scope const authProvider = yield* connectProvider(entry) @@ -530,9 +530,9 @@ export const layer = (options?: Options) => Layer.effect( // Announce the new tool set so the tool registry registers it. A server that finishes connecting // after the initial registration sweep and emits no list-changed notification would otherwise // stay invisible to the model. - yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore) - yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore) - yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) whenLive(name, entry, result.value.connection)(refreshPrompts(name, entry, result.value.connection)) return } @@ -544,7 +544,7 @@ export const layer = (options?: Options) => Layer.effect( ? { status: "needs_auth" } : { status: "failed", error: error instanceof Error ? error.message : String(error) } yield* Effect.logWarning("mcp connect failed", { server: name, status: entry.status }) - yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) }).pipe(Effect.ensuring(Deferred.succeed(entry.startup, undefined))) const stopServer = Effect.fnUntraced(function* (name: ServerName, entry: ServerEntry) { @@ -555,9 +555,9 @@ export const layer = (options?: Options) => Layer.effect( entry.tools = undefined entry.prompts = undefined yield* Scope.close(scope, Exit.void) - yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore) - yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore) - yield* events.publish(Command.Event.Updated, {}).pipe(Effect.ignore) + yield* bus.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(Command.Event.Updated, {}).pipe(Effect.ignore) }) // Disabled servers settle their startup immediately so queries never block on them. @@ -587,7 +587,7 @@ export const layer = (options?: Options) => Layer.effect( }).pipe(locks.withLock(name)) }) fork( - events.subscribe(Integration.Event.ConnectionUpdated).pipe( + bus.subscribe(Integration.Event.ConnectionUpdated).pipe( Stream.filter((event) => owned.has(event.data.integrationID)), Stream.runForEach((event) => Effect.sync(() => fork(reconnect(event.data.integrationID)))), Effect.ignore, @@ -632,7 +632,7 @@ export const layer = (options?: Options) => Layer.effect( yield* register(name, entry) if (config.disabled) { entry.status = { status: "disabled" } - yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) return } yield* startServer(name, entry) @@ -657,7 +657,7 @@ export const layer = (options?: Options) => Layer.effect( const target = yield* requireServer(name) yield* stopServer(name, target.entry) target.entry.status = { status: "disabled" } - yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) }).pipe(locks.withLock(name)) }), remove: Effect.fn("MCP.remove")(function* (server) { @@ -670,7 +670,7 @@ export const layer = (options?: Options) => Layer.effect( // Credentials are kept: they are keyed by name + url, so re-adding the same server // reuses them without forcing re-auth, matching add()'s replacement semantics. runtime.delete(name) - yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) }).pipe(locks.withLock(name)) }), tools: Effect.fn("MCP.tools")(function* () { @@ -793,7 +793,7 @@ export function configured(options?: Options) { return makeLocationNode({ service: Service, layer: layer(options), - deps: [Config.node, Location.node, EventV2.node, Form.node, Integration.node, Credential.node], + deps: [Config.node, Location.node, Bus.node, Form.node, Integration.node, Credential.node], }) } diff --git a/packages/core/src/mcp/instructions.ts b/packages/core/src/mcp/instructions.ts index d0cf45ec6dc0..198272f59d21 100644 --- a/packages/core/src/mcp/instructions.ts +++ b/packages/core/src/mcp/instructions.ts @@ -2,8 +2,8 @@ export * as McpInstructions from "./instructions" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Context, Effect, Layer, Schema } from "effect" -import { AgentV2 } from "../agent" -import { PermissionV2 } from "../permission" +import { Agent } from "../agent" +import { Permission } from "../permission" import { McpTool } from "../tool/mcp" import { MCP } from "./index" import { Instructions } from "../instructions/index" @@ -55,10 +55,10 @@ const update = (previous: ReadonlyArray, current: ReadonlyArray Effect.Effect + readonly load: (agent: Agent.Selection) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/McpInstructions") {} +export class Service extends Context.Service()("@opencode/McpInstructions") {} export const layer = Layer.effect( Service, @@ -83,7 +83,7 @@ export const layer = Layer.effect( const [instructions, tools] = yield* Effect.all([mcp.instructions(), mcp.tools()], { concurrency: "unbounded", }) - const canExecute = PermissionV2.evaluate("execute", "*", agent.permissions).effect !== "deny" + const canExecute = Permission.evaluate("execute", "*", agent.permissions).effect !== "deny" // Instructions are useful only when this agent can reach at least one server tool. const visible = instructions .flatMap((item) => { @@ -93,7 +93,7 @@ export const layer = Layer.effect( if ( !owned.some( (tool) => - PermissionV2.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny", + Permission.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny", ) ) return [] diff --git a/packages/core/src/model-resolver.ts b/packages/core/src/model-resolver.ts index 44faea8b12d5..024ab8b048f8 100644 --- a/packages/core/src/model-resolver.ts +++ b/packages/core/src/model-resolver.ts @@ -15,17 +15,17 @@ import { AISDK } from "./aisdk" import { Catalog } from "./catalog" import { Credential } from "./credential" import { Integration } from "./integration" -import { ModelV2 } from "./model" +import { Capabilities, ID, Info, Ref, VariantID } from "./model" import { Npm } from "@opencode-ai/util/npm" import { OpenAICodex } from "./plugin/provider/openai-codex" -import { ProviderV2 } from "./provider" +import { Provider } from "./provider" export class VariantUnavailableError extends Schema.TaggedErrorClass()( "SessionRunnerModel.VariantUnavailableError", { - providerID: ProviderV2.ID, - modelID: ModelV2.ID, - variant: ModelV2.VariantID, + providerID: Provider.ID, + modelID: ID, + variant: VariantID, }, ) { override get message() { @@ -36,8 +36,8 @@ export class VariantUnavailableError extends Schema.TaggedErrorClass()( "SessionRunnerModel.UnsupportedPackageError", { - providerID: ProviderV2.ID, - modelID: ModelV2.ID, + providerID: Provider.ID, + modelID: ID, package: Schema.String, }, ) { @@ -52,21 +52,21 @@ export interface Resolved { /** Route-level model for provider requests; its id is the provider API model id, which may differ from the catalog id. */ readonly model: Model /** Selected catalog identity. Durable records and displays must use this, never the API model id. */ - readonly ref: ModelV2.Ref + readonly ref: Ref /** Catalog capabilities used to shape requests before provider lowering. */ - readonly capabilities: ModelV2.Capabilities + readonly capabilities: Capabilities /** Catalog pricing in dollars per million tokens. */ - readonly cost: ModelV2.Info["cost"] + readonly cost: Info["cost"] } export interface Interface { - readonly resolve: (requested?: ModelV2.Ref) => Effect.Effect - readonly resolveModel: (model: ModelV2.Info, variant?: ModelV2.VariantID) => Effect.Effect + readonly resolve: (requested?: Ref) => Effect.Effect + readonly resolveModel: (model: Info, variant?: VariantID) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/ModelResolver") {} +export class Service extends Context.Service()("@opencode/ModelResolver") {} -const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => { +const apiKey = (model: Info, credential?: Credential.Value) => { if (credential?.type === "key") return Auth.value(credential.key) if (credential?.type === "oauth") return Auth.value(credential.access) const value = model.settings?.apiKey @@ -74,7 +74,7 @@ const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => { return undefined } -const withDefaults = (model: ModelV2.Info, route: AnyRoute) => +const withDefaults = (model: Info, route: AnyRoute) => route.with({ provider: model.providerID, endpoint: typeof model.settings?.baseURL === "string" ? { baseURL: model.settings.baseURL } : undefined, @@ -84,8 +84,8 @@ const withDefaults = (model: ModelV2.Info, route: AnyRoute) => limits: { context: model.limit.context, output: model.limit.output }, }) -const providerHeaders = (model: ModelV2.Info) => { - const packageName = ProviderV2.packageName(model.package) +const providerHeaders = (model: Info) => { + const packageName = Provider.packageName(model.package) const generated = new Map() if (packageName === "@ai-sdk/openai" && typeof model.settings?.organization === "string") generated.set("OpenAI-Organization", model.settings.organization) @@ -93,16 +93,16 @@ const providerHeaders = (model: ModelV2.Info) => { generated.set("OpenAI-Project", model.settings.project) if (packageName === "@ai-sdk/anthropic" && typeof model.settings?.authToken === "string") generated.set("Authorization", `Bearer ${model.settings.authToken}`) - return ProviderV2.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers) + return Provider.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers) } const providerOptions = ( - model: ModelV2.Info, + model: Info, ): { readonly [key: string]: { readonly [key: string]: unknown } } | undefined => { - if (!ProviderV2.isAISDK(model.package) || model.settings === undefined) return undefined + if (!Provider.isAISDK(model.package) || model.settings === undefined) return undefined const { apiKey: _, baseURL: _baseURL, ...settings } = model.settings if (Object.keys(settings).length === 0) return undefined - const packageName = ProviderV2.packageName(model.package) + const packageName = Provider.packageName(model.package) if (packageName === "@ai-sdk/openai") return { openai: settings } if (packageName === "@ai-sdk/anthropic") return { anthropic: settings } if (packageName === "@ai-sdk/openai-compatible") return { openai: settings } @@ -110,9 +110,9 @@ const providerOptions = ( } export const withVariant = ( - model: ModelV2.Info, - variantID: ModelV2.VariantID | undefined, -): Effect.Effect => { + model: Info, + variantID: VariantID | undefined, +): Effect.Effect => { const id = variantID === "default" ? undefined : variantID const variant = model.variants?.find((item) => item.id === id) if (!variant && variantID !== undefined && variantID !== "default") @@ -126,37 +126,37 @@ export const withVariant = ( return Effect.succeed( variant ? produce(model, (draft) => { - draft.settings = ProviderV2.mergeOverlay(draft.settings, variant.settings) - draft.headers = ProviderV2.mergeHeaders(draft.headers, variant.headers) - draft.body = ProviderV2.mergeOverlay(draft.body, variant.body) + draft.settings = Provider.mergeOverlay(draft.settings, variant.settings) + draft.headers = Provider.mergeHeaders(draft.headers, variant.headers) + draft.body = Provider.mergeOverlay(draft.body, variant.body) }) : model, ) } export interface Dependencies { - readonly loadPackage?: (specifier: string) => Effect.Effect - readonly loadAISDK?: (model: ModelV2.Info) => Effect.Effect + readonly loadPackage?: (specifier: string) => Effect.Effect + readonly loadAISDK?: (model: Info) => Effect.Effect } export const fromCatalogModel = ( - model: ModelV2.Info, + model: Info, credential?: Credential.Value, dependencies?: Dependencies, ): Effect.Effect => { const resolved = produce(model, (draft) => { if (draft.settings?.apiKey === "") delete draft.settings.apiKey if (credential?.type === "key" && credential.metadata !== undefined) - draft.body = ProviderV2.mergeOverlay(draft.body, credential.metadata) + draft.body = Provider.mergeOverlay(draft.body, credential.metadata) }) - const packageName = ProviderV2.packageName(resolved.package) + const packageName = Provider.packageName(resolved.package) const key = apiKey(resolved, credential) - if (OpenAICodex.isChatGPT(credential) && !ProviderV2.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) { + if (OpenAICodex.isChatGPT(credential) && !Provider.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) { return Effect.succeed(codexModel(resolved, credential, key)) } - if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") { + if (Provider.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") { if (OpenAICodex.isChatGPT(credential)) return Effect.succeed(codexModel(resolved, credential, key)) return Effect.succeed( withDefaults(resolved, OpenAIResponses.route) @@ -164,7 +164,7 @@ export const fromCatalogModel = ( .model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }), ) } - if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") { + if (Provider.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") { return Effect.succeed( withDefaults(resolved, AnthropicMessages.route) .with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) }) @@ -172,7 +172,7 @@ export const fromCatalogModel = ( ) } if ( - ProviderV2.isAISDK(resolved.package) && + Provider.isAISDK(resolved.package) && packageName === "@ai-sdk/openai-compatible" && typeof resolved.settings?.baseURL === "string" ) { @@ -182,10 +182,10 @@ export const fromCatalogModel = ( .model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }), ) } - if (ProviderV2.isAISDK(resolved.package)) { + if (Provider.isAISDK(resolved.package)) { if (!dependencies?.loadAISDK) return Effect.fail(unsupported(resolved)) const runtime = produce(resolved, (draft) => { - draft.settings = ProviderV2.mergeOverlay(draft.settings, { + draft.settings = Provider.mergeOverlay(draft.settings, { ...(credential?.type === "key" ? { apiKey: credential.key } : {}), ...(credential?.type === "oauth" ? { apiKey: credential.access } : {}), ...credential?.metadata, @@ -197,7 +197,7 @@ export const fromCatalogModel = ( const specifier = resolved.package return Effect.gen(function* () { - const module = yield* (dependencies?.loadPackage ?? ProviderV2.loadPackage)(specifier).pipe( + const module = yield* (dependencies?.loadPackage ?? Provider.loadPackage)(specifier).pipe( Effect.mapError(() => unsupported(resolved)), ) const configured = { ...resolved.settings, ...credential?.metadata } @@ -249,7 +249,7 @@ const withoutNativeAuthSettings = (settings: Record) => { } const codexModel = ( - model: ModelV2.Info, + model: Info, credential: Credential.Value | undefined, key: ReturnType | undefined, ) => { @@ -264,7 +264,7 @@ const codexModel = ( .model({ id: model.modelID ?? model.id, compatibility: model.compatibility }) } -const unsupported = (model: ModelV2.Info) => +const unsupported = (model: Info) => new UnsupportedPackageError({ providerID: model.providerID, modelID: model.id, @@ -272,13 +272,13 @@ const unsupported = (model: ModelV2.Info) => }) export const resolveModel = ( - model: ModelV2.Info, - variant: ModelV2.VariantID | undefined, + model: Info, + variant: VariantID | undefined, credential?: Credential.Value, dependencies?: Dependencies, ) => withVariant(model, variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential, dependencies))) -export const supported = (model: ModelV2.Info) => Boolean(model.package) +export const supported = (model: Info) => Boolean(model.package) /** Resolves catalog selections into runtime models for the current Location. */ export const layer = Layer.effect( @@ -289,8 +289,8 @@ export const layer = Layer.effect( const npm = yield* Npm.Service const aisdk = yield* AISDK.Service const load = Effect.fn("ModelResolver.resolveModel")(function* ( - selected: ModelV2.Info, - variant?: ModelV2.VariantID, + selected: Info, + variant?: VariantID, ) { const provider = yield* catalog.provider.get(selected.providerID) const connection = yield* integrations.connection.active( @@ -301,13 +301,13 @@ export const layer = Layer.effect( variant, connection ? yield* integrations.connection.resolve(connection) : undefined, { - loadPackage: (specifier) => ProviderV2.loadPackage(specifier, npm), + loadPackage: (specifier) => Provider.loadPackage(specifier, npm), loadAISDK: (model) => aisdk.model(model), }, ) return { model, - ref: ModelV2.Ref.make({ + ref: Ref.make({ id: selected.id, providerID: selected.providerID, ...(variant === undefined ? {} : { variant }), diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index e8045f11043c..36c0be160193 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -1,5 +1,5 @@ import { Model } from "@opencode-ai/schema/model" -import { ProviderV2 } from "./provider" +import { Provider } from "./provider" import type { DeepMutable } from "./schema" export const ID = Model.ID @@ -37,12 +37,12 @@ export function compatibility(input: unknown): Compatibility | undefined { return typeof input.field === "string" ? { reasoningField: input.field } : undefined } -export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } { +export function parse(input: string): { providerID: Provider.ID; modelID: ID } { const [providerID, ...modelID] = input.split("/") return { - providerID: ProviderV2.ID.make(providerID), + providerID: Provider.ID.make(providerID), modelID: ID.make(modelID.join("/")), } } -export * as ModelV2 from "./model" +export * as Model from "./model" diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts index 2db39ab124e8..252d04f53045 100644 --- a/packages/core/src/models-dev.ts +++ b/packages/core/src/models-dev.ts @@ -8,11 +8,11 @@ import { Global } from "@opencode-ai/util/global" import { Flock } from "@opencode-ai/util/flock" import { Hash } from "@opencode-ai/util/hash" import { FSUtil } from "@opencode-ai/util/fs-util" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { httpClient } from "@opencode-ai/util/effect/app-node-platform" -import { ModelV2 } from "./model" -import { ProviderV2 } from "./provider" +import { Model } from "./model" +import { Provider } from "./provider" export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"]) export type CatalogModelStatus = typeof CatalogModelStatus.Type @@ -54,7 +54,7 @@ type SourceModel = { { readonly cost?: Cost readonly provider?: { - readonly body?: ProviderV2.Settings + readonly body?: Provider.Settings readonly headers?: Readonly> } } @@ -75,29 +75,29 @@ type SourceProvider = { } export type Snapshot = { - readonly info: ProviderV2.Info - readonly models: readonly ModelV2.Info[] + readonly info: Provider.Info + readonly models: readonly Model.Info[] readonly environment: readonly string[] } function normalize(input: Record): readonly Snapshot[] { const providers: Snapshot[] = [] for (const item of Object.values(input)) { - const providerID = ProviderV2.ID.make(item.id) + const providerID = Provider.ID.make(item.id) const info = { id: providerID, name: item.name, - package: ProviderV2.aisdk(item.npm), + package: Provider.aisdk(item.npm), ...(item.api ? { settings: { baseURL: item.api } } : {}), - } satisfies ProviderV2.Info - const models: ModelV2.Info[] = [] + } satisfies Provider.Info + const models: Model.Info[] = [] for (const model of Object.values(item.models)) { const baseCost = cost(model.cost) const variants = reasoningVariants(item, model) - const id = ModelV2.ID.make(model.id) + const id = Model.ID.make(model.id) models.push(modelInfo(providerID, id, model, { cost: baseCost, variants })) for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) { - const modeID = ModelV2.ID.make(`${model.id}-${mode}`) + const modeID = Model.ID.make(`${model.id}-${mode}`) models.push( modelInfo(providerID, modeID, model, { name: modeName(model, mode), @@ -118,7 +118,7 @@ function released(date: string) { return Number.isFinite(time) ? time : 0 } -function cost(input: SourceModel["cost"]): ModelV2.Info["cost"] { +function cost(input: SourceModel["cost"]): Model.Info["cost"] { const base = { input: input?.input ?? Money.USDPerMillionTokens.zero, output: input?.output ?? Money.USDPerMillionTokens.zero, @@ -154,13 +154,13 @@ function cost(input: SourceModel["cost"]): ModelV2.Info["cost"] { ] } -function mergeCost(base: ModelV2.Info["cost"], override: SourceModel["cost"] | undefined) { +function mergeCost(base: Model.Info["cost"], override: SourceModel["cost"] | undefined) { if (!override) return base const next = cost(override) const [baseDefault, ...baseTiers] = base const [nextDefault, ...nextTiers] = next - const tierKey = (item: ModelV2.Info["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}` - const merge = (left: ModelV2.Info["cost"][number], right: ModelV2.Info["cost"][number]) => ({ + const tierKey = (item: Model.Info["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}` + const merge = (left: Model.Info["cost"][number], right: Model.Info["cost"][number]) => ({ ...left, ...right, tier: right.tier ?? left.tier, @@ -187,7 +187,7 @@ function mergeCost(base: ModelV2.Info["cost"], override: SourceModel["cost"] | u const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"] const OUTPUT_TOKEN_MAX = 32_000 -function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNullable { +function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNullable { const npm = model.provider?.npm ?? provider.npm const options = model.reasoning_options if (!options?.length) return [] @@ -203,7 +203,7 @@ function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNul if (id === undefined) return [] if (id === "none" && off.length > 0) return [] const settings = settingsForEffort(npm, model.id, id) - return settings ? [{ id: ModelV2.VariantID.make(id), settings }] : [] + return settings ? [{ id: Model.VariantID.make(id), settings }] : [] }), ] return [...new Map(variants.map((variant) => [variant.id, variant])).values()] @@ -218,7 +218,7 @@ function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNul return [] } -function settingsForEffort(npm: string, modelID: string, effort: string): ProviderV2.Settings | undefined { +function settingsForEffort(npm: string, modelID: string, effort: string): Provider.Settings | undefined { if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { effort } } if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic") { if (anthropicManualThinking(modelID)) return { effort } @@ -287,7 +287,7 @@ function budgetVariants( npm: string, model: SourceModel, option: Extract[number], { type: "budget_tokens" }>, -): NonNullable { +): NonNullable { const maximum = Math.min(option.max ?? OUTPUT_TOKEN_MAX - 1, model.limit.output - 1, OUTPUT_TOKEN_MAX - 1) if (maximum <= 0) return [] const high = Math.min(Math.max(option.min ?? 0, Math.floor((maximum + 1) / 2)), maximum) @@ -296,35 +296,35 @@ function budgetVariants( { id: "max", budget: maximum }, ].flatMap((item) => { const settings = settingsForBudget(npm, model.id, item.budget) - return settings ? [{ id: ModelV2.VariantID.make(item.id), settings }] : [] + return settings ? [{ id: Model.VariantID.make(item.id), settings }] : [] }) } -function toggleVariants(npm: string, modelID: string): NonNullable { +function toggleVariants(npm: string, modelID: string): NonNullable { if (npm === "@ai-sdk/gateway") { const upstream = gatewayPackage(modelID) if (upstream) return toggleVariants(upstream, modelID) return [ { - id: ModelV2.VariantID.make("none"), + id: Model.VariantID.make("none"), settings: { reasoning: { enabled: false } }, }, { - id: ModelV2.VariantID.make("thinking"), + id: Model.VariantID.make("thinking"), settings: { reasoning: { enabled: true } }, }, ] } if (npm === "@openrouter/ai-sdk-provider") return [ - { id: ModelV2.VariantID.make("none"), settings: { reasoning: { enabled: false } } }, - { id: ModelV2.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } }, + { id: Model.VariantID.make("none"), settings: { reasoning: { enabled: false } } }, + { id: Model.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } }, ] if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic") return [ - { id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } }, + { id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } }, { - id: ModelV2.VariantID.make("thinking"), + id: Model.VariantID.make("thinking"), settings: { thinking: { type: "adaptive", display: "summarized" }, }, @@ -333,11 +333,11 @@ function toggleVariants(npm: string, modelID: string): NonNullable["modes"]>[string]["provider"] - readonly variants?: NonNullable + readonly variants?: NonNullable } = {}, -): ModelV2.Info { +): Model.Info { return { id, - modelID: ModelV2.ID.make(model.id), + modelID: Model.ID.make(model.id), providerID, name: input.name ?? model.name, - compatibility: ModelV2.compatibility(model.interleaved), - family: model.family ? ModelV2.Family.make(model.family) : undefined, - package: model.provider?.npm ? ProviderV2.aisdk(model.provider.npm) : undefined, + compatibility: Model.compatibility(model.interleaved), + family: model.family ? Model.Family.make(model.family) : undefined, + package: model.provider?.npm ? Provider.aisdk(model.provider.npm) : undefined, settings: model.provider?.api ? { baseURL: model.provider.api } : undefined, capabilities: { tools: model.tool_call, @@ -519,7 +519,7 @@ function modelInfo( } } -export const Event = ModelsDev.Event +export { Event } from "@opencode-ai/schema/models-dev" declare const OPENCODE_MODELS_DEV: Record | undefined @@ -542,7 +542,7 @@ export const layer = (options?: Options) => Service, Effect.gen(function* () { const fs = yield* FSUtil.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const app = yield* App.Metadata const http = HttpClient.filterStatusOk( (yield* HttpClient.HttpClient).pipe( @@ -639,7 +639,7 @@ export const layer = (options?: Options) => if (!force && (yield* fresh())) return yield* fetchAndWrite() yield* invalidate - yield* events.publish(Event.Refreshed, {}) + yield* bus.publish(ModelsDev.Event.Refreshed, {}) }), ).pipe( Effect.tapCause((cause) => Effect.logError("Failed to fetch models.dev", { cause: cause })), @@ -660,7 +660,7 @@ export function configured(options?: Options) { return makeGlobalNode({ service: Service, layer: layer(options), - deps: [FSUtil.node, EventV2.node, App.node, httpClient], + deps: [FSUtil.node, Bus.node, App.node, httpClient], }) } diff --git a/packages/core/src/permission.ts b/packages/core/src/permission.ts index 8b276833dd38..b957a2e763c4 100644 --- a/packages/core/src/permission.ts +++ b/packages/core/src/permission.ts @@ -1,11 +1,11 @@ -export * as PermissionV2 from "./permission" +export * as Permission from "./permission" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Context, Deferred, Effect, Layer, Schema } from "effect" import { Permission } from "@opencode-ai/schema/permission" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { Location } from "./location" -import { AgentV2 } from "./agent" +import { Agent } from "./agent" import { SessionErrors } from "./session/error" import { SessionSchema } from "./session/schema" import { SessionStore } from "./session/store" @@ -41,32 +41,32 @@ export type Reply = typeof Reply.Type export const AssertInput = Schema.Struct({ id: ID.pipe(Schema.optional), ...RequestFields, - agent: AgentV2.ID.pipe(Schema.optional), -}).annotate({ identifier: "PermissionV2.AssertInput" }) + agent: Agent.ID.pipe(Schema.optional), +}).annotate({ identifier: "Permission.AssertInput" }) export type AssertInput = typeof AssertInput.Type export const ReplyInput = Schema.Struct({ requestID: ID, reply: Reply, message: Schema.String.pipe(Schema.optional), -}).annotate({ identifier: "PermissionV2.ReplyInput" }) +}).annotate({ identifier: "Permission.ReplyInput" }) export type ReplyInput = typeof ReplyInput.Type export const AskResult = Schema.Struct({ id: ID, effect: Permission.Effect, -}).annotate({ identifier: "PermissionV2.AskResult" }) +}).annotate({ identifier: "Permission.AskResult" }) export type AskResult = typeof AskResult.Type -export const Event = Permission.Event +export { Event } from "@opencode-ai/schema/permission" -export class DeclinedError extends Schema.TaggedErrorClass()("PermissionV2.DeclinedError", {}) {} +export class DeclinedError extends Schema.TaggedErrorClass()("Permission.DeclinedError", {}) {} -export class CorrectedError extends Schema.TaggedErrorClass()("PermissionV2.CorrectedError", { +export class CorrectedError extends Schema.TaggedErrorClass()("Permission.CorrectedError", { feedback: Schema.String, }) {} -export class BlockedError extends Schema.TaggedErrorClass()("PermissionV2.BlockedError", { +export class BlockedError extends Schema.TaggedErrorClass()("Permission.BlockedError", { rules: Permission.Ruleset, permission: Schema.String, resources: Schema.Array(Schema.String), @@ -76,7 +76,7 @@ export class BlockedError extends Schema.TaggedErrorClass()("Permi } } -export class NotFoundError extends Schema.TaggedErrorClass()("PermissionV2.NotFoundError", { +export class NotFoundError extends Schema.TaggedErrorClass()("Permission.NotFoundError", { requestID: ID, }) {} @@ -107,20 +107,20 @@ export interface Interface { readonly list: () => Effect.Effect> } -export class Service extends Context.Service()("@opencode/v2/Permission") {} +export class Service extends Context.Service()("@opencode/Permission") {} interface Pending { readonly request: Request - readonly agent?: AgentV2.ID + readonly agent?: Agent.ID readonly deferred: Deferred.Deferred } const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const location = yield* Location.Service - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const sessions = yield* SessionStore.Service const saved = yield* PermissionSaved.Service const pending = new Map() @@ -143,9 +143,9 @@ const layer = Layer.effect( ) }) - const configured = Effect.fn("PermissionV2.configured")(function* ( + const configured = Effect.fn("Permission.configured")(function* ( sessionID: SessionSchema.ID, - agentID?: AgentV2.ID, + agentID?: Agent.ID, ) { const session = yield* sessions.get(sessionID) if (!session) return yield* new SessionErrors.NotFoundError({ sessionID }) @@ -182,7 +182,7 @@ const layer = Layer.effect( } } - const create = (request: Request, agent?: AgentV2.ID) => + const create = (request: Request, agent?: Agent.ID) => Effect.uninterruptible( Effect.gen(function* () { const deferred = yield* Deferred.make() @@ -190,21 +190,21 @@ const layer = Layer.effect( if (pending.has(request.id)) return yield* Effect.die(new Error(`Duplicate pending permission ID: ${request.id}`)) pending.set(request.id, item) - yield* events - .publish(Event.Asked, request) + yield* bus + .publish(Permission.Event.Asked, request) .pipe(Effect.onError(() => Effect.sync(() => pending.delete(request.id)))) return item }), ) - const ask = Effect.fn("PermissionV2.ask")(function* (input: AssertInput) { + const ask = Effect.fn("Permission.ask")(function* (input: AssertInput) { const result = yield* evaluateInput(input) const value = request(input) if (result.effect === "ask") yield* create(value, input.agent) return { id: value.id, effect: result.effect } }) - const assert = Effect.fn("PermissionV2.assert")((input: AssertInput) => + const assert = Effect.fn("Permission.assert")((input: AssertInput) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () { const result = yield* evaluateInput(input) @@ -223,7 +223,7 @@ const layer = Layer.effect( // resurfaces as a typed failure at SessionModelRequest.executeTool. A decline // WITH feedback (CorrectedError) intentionally stays typed so the leaf can turn // it into ToolFailure and the model continues. - Effect.catchTag("PermissionV2.DeclinedError", (error) => Effect.die(error)), + Effect.catchTag("Permission.DeclinedError", (error) => Effect.die(error)), Effect.ensuring( Effect.sync(() => { pending.delete(item.request.id) @@ -234,12 +234,12 @@ const layer = Layer.effect( ), ) - const reply = Effect.fn("PermissionV2.reply")((input: ReplyInput) => + const reply = Effect.fn("Permission.reply")((input: ReplyInput) => Effect.uninterruptible( Effect.gen(function* () { const existing = pending.get(input.requestID) if (!existing) return yield* new NotFoundError({ requestID: input.requestID }) - yield* events.publish(Event.Replied, { + yield* bus.publish(Permission.Event.Replied, { sessionID: existing.request.sessionID, requestID: existing.request.id, reply: input.reply, @@ -253,7 +253,7 @@ const layer = Layer.effect( pending.delete(input.requestID) for (const [id, item] of pending) { if (item.request.sessionID !== existing.request.sessionID) continue - yield* events.publish(Event.Replied, { + yield* bus.publish(Permission.Event.Replied, { sessionID: item.request.sessionID, requestID: item.request.id, reply: "reject", @@ -290,7 +290,7 @@ const layer = Layer.effect( ) ) continue - yield* events.publish(Event.Replied, { + yield* bus.publish(Permission.Event.Replied, { sessionID: item.request.sessionID, requestID: item.request.id, reply: "always", @@ -302,15 +302,15 @@ const layer = Layer.effect( ), ) - const list = Effect.fn("PermissionV2.list")(function* () { + const list = Effect.fn("Permission.list")(function* () { return Array.from(pending.values(), (item) => item.request) }) - const get = Effect.fn("PermissionV2.get")(function* (id: ID) { + const get = Effect.fn("Permission.get")(function* (id: ID) { return pending.get(id)?.request }) - const forSession = Effect.fn("PermissionV2.forSession")(function* (sessionID: SessionSchema.ID) { + const forSession = Effect.fn("Permission.forSession")(function* (sessionID: SessionSchema.ID) { return Array.from(pending.values(), (item) => item.request).filter((request) => request.sessionID === sessionID) }) @@ -321,5 +321,5 @@ const layer = Layer.effect( export const node = makeLocationNode({ service: Service, layer, - deps: [EventV2.node, Location.node, AgentV2.node, SessionStore.node, PermissionSaved.node], + deps: [Bus.node, Location.node, Agent.node, SessionStore.node, PermissionSaved.node], }) diff --git a/packages/core/src/permission/saved.ts b/packages/core/src/permission/saved.ts index a973ae3e1ba0..0c0fe2d067b4 100644 --- a/packages/core/src/permission/saved.ts +++ b/packages/core/src/permission/saved.ts @@ -4,7 +4,7 @@ import { eq } from "drizzle-orm" import { Context, Effect, Layer, Schema } from "effect" import { Database } from "../database/database" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" -import { ProjectV2 } from "../project" +import { Project } from "../project" import { PermissionTable } from "./sql" import { PermissionSaved } from "@opencode-ai/schema/permission-saved" @@ -15,12 +15,12 @@ export const Info = PermissionSaved.Info export type Info = typeof Info.Type export const ListInput = Schema.Struct({ - projectID: ProjectV2.ID.pipe(Schema.optional), + projectID: Project.ID.pipe(Schema.optional), }).annotate({ identifier: "PermissionSaved.ListInput" }) export type ListInput = typeof ListInput.Type export const AddInput = Schema.Struct({ - projectID: ProjectV2.ID, + projectID: Project.ID, action: Schema.String, resources: Schema.Array(Schema.String), }).annotate({ identifier: "PermissionSaved.AddInput" }) @@ -32,7 +32,7 @@ export interface Interface { readonly remove: (id: ID) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/PermissionSaved") {} +export class Service extends Context.Service()("@opencode/PermissionSaved") {} const layer = Layer.effect( Service, diff --git a/packages/core/src/permission/sql.ts b/packages/core/src/permission/sql.ts index c395555d7950..d5deea9d5b0d 100644 --- a/packages/core/src/permission/sql.ts +++ b/packages/core/src/permission/sql.ts @@ -1,6 +1,6 @@ import { sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core" import { Timestamps } from "../database/schema.sql" -import { ProjectV2 } from "../project" +import { Project } from "../project" import { ProjectTable } from "../project/sql" import type { PermissionSaved } from "./saved" @@ -9,7 +9,7 @@ export const PermissionTable = sqliteTable( { id: text().$type().primaryKey(), project_id: text() - .$type() + .$type() .notNull() .references(() => ProjectTable.id, { onDelete: "cascade" }), action: text().notNull(), diff --git a/packages/core/src/plugin.ts b/packages/core/src/plugin.ts index bb093a7b97a9..009d9d060526 100644 --- a/packages/core/src/plugin.ts +++ b/packages/core/src/plugin.ts @@ -1,46 +1,43 @@ -export * as PluginV2 from "./plugin" +export * as Plugin from "./plugin" +export { Event, ID, Info } from "@opencode-ai/schema/plugin" -import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin" -import { Event, ID, type Info } from "@opencode-ai/schema/plugin" +import { Plugin } from "@opencode-ai/schema/plugin" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { App } from "./app" import { Context, Effect, Exit, Layer, Scope, Semaphore } from "effect" -import { AgentV2 } from "./agent" +import { Agent } from "./agent" import { AISDK } from "./aisdk" import { Catalog } from "./catalog" -import { CommandV2 } from "./command" -import { EventV2 } from "./event" +import { Command } from "./command" +import { Bus } from "./bus" import { Integration } from "./integration" import { Location } from "./location" import { PluginHost } from "./plugin/host" import { PluginRuntime } from "./plugin/runtime" import { WebSearch } from "./websearch" import { Reference } from "./reference" -import { SkillV2 } from "./skill" +import { Skill } from "./skill" import { State } from "./state" -import { ToolRegistry } from "./tool/registry" -import { ToolHooks } from "./tool/hooks" +import { Tool } from "./tool" import { PluginHooks } from "./plugin/hooks" export interface Interface { readonly activate: (plugins: readonly Versioned[]) => Effect.Effect - readonly list: () => Effect.Effect + readonly list: () => Effect.Effect } -export interface Versioned extends Plugin { - readonly version: string -} +export type Versioned = import("@opencode-ai/plugin/effect/plugin").Plugin & { readonly version: string } -export class Service extends Context.Service()("@opencode/v2/Plugin") {} +export class Service extends Context.Service()("@opencode/Plugin") {} const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const scope = yield* Scope.make() - const active = new Map() + const active = new Map() const lock = Semaphore.makeUnsafe(1) - let host: Parameters[0] + let host: Parameters[0] const load = Effect.fnUntraced(function* (plugin: Versioned) { const child = yield* Scope.fork(scope) @@ -49,7 +46,7 @@ const layer = Layer.effect( inherit, Effect.updateContext((_context: Context.Context) => Context.make(Scope.Scope, child)), Effect.withSpan("Plugin.load", { attributes: { "plugin.id": plugin.id } }), - Effect.andThen(events.publish(Event.Added, { id: ID.make(plugin.id) })), + Effect.andThen(bus.publish(Plugin.Event.Added, { id: Plugin.ID.make(plugin.id) })), Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)), Effect.exit, ) @@ -62,8 +59,8 @@ const layer = Layer.effect( }) const activate = Effect.fn("Plugin.activate")(function* (plugins: readonly Versioned[]) { - const definitions = plugins.map((plugin) => ({ ...plugin, id: ID.make(plugin.id) })) - const ids = new Set() + const definitions = plugins.map((plugin) => ({ ...plugin, id: Plugin.ID.make(plugin.id) })) + const ids = new Set() for (const definition of definitions) { if (ids.has(definition.id)) yield* Effect.die(new Error(`Duplicate plugin ID: ${definition.id}`)) ids.add(definition.id) @@ -118,7 +115,7 @@ const layer = Layer.effect( }) }), ) - yield* events.publish(Event.Updated, {}) + yield* bus.publish(Plugin.Event.Updated, {}) }), ) }) @@ -145,18 +142,17 @@ export const node = makeLocationNode({ service: Service, layer, deps: [ - EventV2.node, + Bus.node, App.node, - AgentV2.node, + Agent.node, AISDK.node, Catalog.node, - CommandV2.node, + Command.node, Integration.node, Location.node, Reference.node, - SkillV2.node, - ToolRegistry.toolsNode, - ToolHooks.node, + Skill.node, + Tool.node, PluginHooks.node, PluginRuntime.node, WebSearch.node, diff --git a/packages/core/src/plugin/agent.ts b/packages/core/src/plugin/agent.ts index 080cdb1ebebf..ac1adb71f125 100644 --- a/packages/core/src/plugin/agent.ts +++ b/packages/core/src/plugin/agent.ts @@ -1,12 +1,12 @@ export * as AgentPlugin from "./agent" import path from "path" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Effect } from "effect" -import { AgentV2 } from "../agent" +import { Agent } from "../agent" import { Global } from "@opencode-ai/util/global" import { Location } from "../location" -import { PermissionV2 } from "../permission" +import { Permission } from "../permission" // Combined output files written by the Shell service, e.g. `/shell//.out`. // Whitelisted so agents can read a command's full captured output without an external-directory prompt. @@ -105,13 +105,13 @@ export const Plugin = define({ const location = yield* Location.Service const worktree = location.directory const whitelistedDirs = [SHELL_OUTPUT_GLOB, path.join(Global.Path.tmp, "*")] - const readonlyExternalDirectory: PermissionV2.Ruleset = [ + const readonlyExternalDirectory: Permission.Ruleset = [ { action: "external_directory", resource: "*", effect: "ask" }, ...whitelistedDirs.map( - (resource): PermissionV2.Rule => ({ action: "external_directory", resource, effect: "allow" }), + (resource): Permission.Rule => ({ action: "external_directory", resource, effect: "allow" }), ), ] - const defaults: PermissionV2.Ruleset = [ + const defaults: Permission.Ruleset = [ { action: "*", resource: "*", effect: "allow" }, ...readonlyExternalDirectory, { action: "question", resource: "*", effect: "deny" }, @@ -124,24 +124,24 @@ export const Plugin = define({ ] yield* ctx.agent.transform((draft) => { - draft.update(AgentV2.defaultID, (item) => { - item.name = AgentV2.Name.make("Build") + draft.update(Agent.defaultID, (item) => { + item.name = Agent.Name.make("Build") item.description = "The default agent. Executes tools based on configured permissions." item.mode = "primary" item.permissions.push( - ...PermissionV2.merge(defaults, [ + ...Permission.merge(defaults, [ { action: "question", resource: "*", effect: "allow" }, { action: "plan_enter", resource: "*", effect: "allow" }, ]), ) }) - draft.update(AgentV2.ID.make("plan"), (item) => { - item.name = AgentV2.Name.make("Plan") + draft.update(Agent.ID.make("plan"), (item) => { + item.name = Agent.Name.make("Plan") item.description = "Plan mode. Disallows all edit tools." item.mode = "primary" item.permissions.push( - ...PermissionV2.merge(defaults, [ + ...Permission.merge(defaults, [ { action: "question", resource: "*", effect: "allow" }, { action: "plan_exit", resource: "*", effect: "allow" }, { action: "external_directory", resource: path.join(Global.Path.data, "plans", "*"), effect: "allow" }, @@ -156,22 +156,22 @@ export const Plugin = define({ ) }) - draft.update(AgentV2.ID.make("general"), (item) => { - item.name = AgentV2.Name.make("General") + draft.update(Agent.ID.make("general"), (item) => { + item.name = Agent.Name.make("General") item.description = "General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel." item.mode = "subagent" - item.permissions.push(...PermissionV2.merge(defaults, [{ action: "subagent", resource: "*", effect: "deny" }])) + item.permissions.push(...Permission.merge(defaults, [{ action: "subagent", resource: "*", effect: "deny" }])) }) - draft.update(AgentV2.ID.make("explore"), (item) => { - item.name = AgentV2.Name.make("Explore") + draft.update(Agent.ID.make("explore"), (item) => { + item.name = Agent.Name.make("Explore") item.description = 'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.' item.system = PROMPT_EXPLORE item.mode = "subagent" item.permissions.push( - ...PermissionV2.merge( + ...Permission.merge( defaults, [ { action: "*", resource: "*", effect: "deny" }, @@ -187,28 +187,28 @@ export const Plugin = define({ ) }) - draft.update(AgentV2.ID.make("compaction"), (item) => { - item.name = AgentV2.Name.make("Compaction") + draft.update(Agent.ID.make("compaction"), (item) => { + item.name = Agent.Name.make("Compaction") item.mode = "primary" item.hidden = true item.system = PROMPT_COMPACTION - item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) + item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) }) - draft.update(AgentV2.ID.make("title"), (item) => { - item.name = AgentV2.Name.make("Title") + draft.update(Agent.ID.make("title"), (item) => { + item.name = Agent.Name.make("Title") item.mode = "primary" item.hidden = true item.system = PROMPT_TITLE - item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) + item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) }) - draft.update(AgentV2.ID.make("summary"), (item) => { - item.name = AgentV2.Name.make("Summary") + draft.update(Agent.ID.make("summary"), (item) => { + item.name = Agent.Name.make("Summary") item.mode = "primary" item.hidden = true item.system = PROMPT_SUMMARY - item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) + item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) }) }) }), diff --git a/packages/core/src/plugin/command.ts b/packages/core/src/plugin/command.ts index 7bb14dad319f..676d88c0c441 100644 --- a/packages/core/src/plugin/command.ts +++ b/packages/core/src/plugin/command.ts @@ -1,6 +1,6 @@ export * as CommandPlugin from "./command" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Effect } from "effect" import { Location } from "../location" import PROMPT_INITIALIZE from "./command/initialize.txt" diff --git a/packages/core/src/plugin/hooks.ts b/packages/core/src/plugin/hooks.ts index 5538601c915c..50c019e6fb49 100644 --- a/packages/core/src/plugin/hooks.ts +++ b/packages/core/src/plugin/hooks.ts @@ -1,8 +1,8 @@ export * as PluginHooks from "./hooks" -import type { AISDKHooks } from "@opencode-ai/plugin/v2/effect/aisdk" -import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session" -import type { ToolHooks } from "@opencode-ai/plugin/v2/effect/tool" +import type { AISDKHooks } from "@opencode-ai/plugin/effect/aisdk" +import type { SessionHooks } from "@opencode-ai/plugin/effect/session" +import type { ToolHooks } from "@opencode-ai/plugin/effect/tool" import { Context, Effect, Layer, Scope } from "effect" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { State } from "../state" @@ -28,7 +28,7 @@ export interface Interface { ) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/PluginHooks") {} +export class Service extends Context.Service()("@opencode/PluginHooks") {} const layer = Layer.effect( Service, diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index 730ddafecc55..b0b11d2169b4 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -1,48 +1,44 @@ export * as PluginHost from "./host" -import { Plugin } from "@opencode-ai/plugin/v2/effect" -import type { IntegrationMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration" +import { Plugin } from "@opencode-ai/plugin/effect" +import type { IntegrationMethodRegistration } from "@opencode-ai/plugin/effect/integration" import type { CredentialOAuth } from "@opencode-ai/sdk/v2/types" import { EventManifest } from "@opencode-ai/schema/event-manifest" import { App } from "../app" import { Effect, Schema, Stream } from "effect" -import { AgentV2 } from "../agent" +import { Agent } from "../agent" import { AISDK } from "../aisdk" import { Catalog } from "../catalog" -import { CommandV2 } from "../command" +import { Command } from "../command" import { Credential } from "../credential" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { Integration } from "../integration" import { Location } from "../location" -import { ModelV2 } from "../model" -import type { PluginV2 } from "../plugin" +import { Model } from "../model" import { PluginRuntime } from "./runtime" -import { ProviderV2 } from "../provider" +import { Provider } from "../provider" import { Reference } from "../reference" import { AbsolutePath, type DeepMutable } from "../schema" -import { SkillV2 } from "../skill" -import { Tool } from "../tool/tool" -import { Tools } from "../tool/tools" -import { ToolHooks } from "../tool/hooks" -import { WorkspaceV2 } from "../workspace" +import { Skill } from "../skill" +import { Tool } from "../tool" +import { Workspace } from "../workspace" import { WebSearch } from "../websearch" import { PluginHooks } from "./hooks" const mutable = (value: T) => value as DeepMutable -export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) { +export const make = Effect.fn("PluginHost.make")(function* (plugin: import("../plugin").Interface) { const app = yield* App.Metadata - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const aisdk = yield* AISDK.Service const catalog = yield* Catalog.Service - const commands = yield* CommandV2.Service - const events = yield* EventV2.Service + const commands = yield* Command.Service + const bus = yield* Bus.Service const integration = yield* Integration.Service const location = yield* Location.Service const reference = yield* Reference.Service - const skill = yield* SkillV2.Service - const tools = yield* Tools.Service + const skill = yield* Skill.Service + const tools = yield* Tool.Service const websearch = yield* WebSearch.Service - const toolHooks = yield* ToolHooks.Service const hooks = yield* PluginHooks.Service const runtime = yield* PluginRuntime.Service const locationInfo = () => @@ -51,7 +47,9 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int workspaceID: location.workspaceID, project: location.project, }) - const locationRef = (input?: Parameters[0]) => + const locationRef = (input?: { + readonly location?: { readonly directory?: string; readonly workspace?: string } + }) => input?.location === undefined ? undefined : Location.Ref.make({ @@ -59,7 +57,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int workspaceID: input.location.workspace === undefined ? location.workspaceID - : WorkspaceV2.ID.make(input.location.workspace), + : Workspace.ID.make(input.location.workspace), }) const isCurrentLocation = (ref: Location.Ref) => ref.directory === location.directory && ref.workspaceID === location.workspaceID @@ -70,7 +68,22 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int app, options: {}, agent: { - get: (id) => agents.get(AgentV2.ID.make(id)), + get: (input) => { + const ref = locationRef(input) + const output = + ref && !isCurrentLocation(ref) + ? runtime.location.agent + .list(ref) + .pipe(Effect.map((result) => ({ ...result, data: result.data.find((agent) => agent.id === input.agentID) }))) + : response(agents.get(input.agentID)) + return output.pipe( + Effect.flatMap((result) => + result.data + ? Effect.succeed({ ...result, data: result.data }) + : Effect.fail(new Error(`Agent not found: ${input.agentID}`)), + ), + ) + }, list: (input) => { const ref = locationRef(input) if (ref && !isCurrentLocation(ref)) return runtime.location.agent.list(ref) @@ -81,10 +94,10 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int agents.transform((draft) => { callback({ list: () => mutable(draft.list()), - get: (id) => mutable(draft.get(AgentV2.ID.make(id))), - default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)), - update: (id, update) => draft.update(AgentV2.ID.make(id), update), - remove: (id) => draft.remove(AgentV2.ID.make(id)), + get: (id) => mutable(draft.get(Agent.ID.make(id))), + default: (id) => draft.default(id === undefined ? undefined : Agent.ID.make(id)), + update: (id, update) => draft.update(Agent.ID.make(id), update), + remove: (id) => draft.remove(Agent.ID.make(id)), }) }), }, @@ -121,7 +134,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int list: () => response(catalog.provider.available()), get: (input) => catalog.provider - .get(ProviderV2.ID.make(input.providerID)) + .get(Provider.ID.make(input.providerID)) .pipe( Effect.flatMap((provider) => provider === undefined @@ -131,7 +144,6 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int ), }, model: { - get: (providerID, modelID) => catalog.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)), list: () => response(catalog.model.available()), default: () => response(catalog.model.default()), }, @@ -141,21 +153,21 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int callback({ provider: { list: () => mutable(draft.provider.list()), - get: (id) => mutable(draft.provider.get(ProviderV2.ID.make(id))), - update: (id, update) => draft.provider.update(ProviderV2.ID.make(id), update), - remove: (id) => draft.provider.remove(ProviderV2.ID.make(id)), + get: (id) => mutable(draft.provider.get(Provider.ID.make(id))), + update: (id, update) => draft.provider.update(Provider.ID.make(id), update), + remove: (id) => draft.provider.remove(Provider.ID.make(id)), }, model: { get: (providerID, modelID) => - mutable(draft.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID))), + mutable(draft.model.get(Provider.ID.make(providerID), Model.ID.make(modelID))), update: (providerID, modelID, update) => - draft.model.update(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID), update), + draft.model.update(Provider.ID.make(providerID), Model.ID.make(modelID), update), remove: (providerID, modelID) => - draft.model.remove(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)), + draft.model.remove(Provider.ID.make(providerID), Model.ID.make(modelID)), default: { get: draft.model.default.get, set: (providerID, modelID) => - draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)), + draft.model.default.set(Provider.ID.make(providerID), Model.ID.make(modelID)), }, }, }) @@ -170,7 +182,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int }), }, event: { - subscribe: () => events.subscribe().pipe(Stream.filter(EventManifest.isServer)), + subscribe: () => bus.subscribe().pipe(Stream.filter(EventManifest.isServer)), }, integration: { list: () => response(integration.list()), @@ -279,88 +291,21 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int transform: (callback) => skill.transform((draft) => { callback({ - source: (source) => draft.source(Schema.decodeUnknownSync(SkillV2.Source)(source)), + source: (source) => draft.source(Schema.decodeUnknownSync(Skill.Source)(source)), list: draft.list, }) }), }, tool: { transform: (callback) => - Effect.gen(function* () { - const registrations: Array<{ - readonly name: string - readonly tool: Tool.Any - readonly options?: Tool.RegisterOptions - }> = [] - yield* Effect.sync(() => + tools + .transform((draft) => callback({ - add: (name, tool, options) => { - registrations.push({ name, tool, ...(options ? { options } : {}) }) - }, - }), - ) - yield* tools - .registerBatch( - registrations.map((registration) => ({ - tools: { [registration.name]: registration.tool }, - ...(registration.options === undefined ? {} : { options: registration.options }), - })), - ) - .pipe(Effect.orDie) - return { dispose: Effect.void } - }), - hook: (name, callback) => { - if (name === "execute.before") { - return toolHooks.hook.before((event) => { - const output = { - tool: event.tool, - sessionID: event.sessionID, - agent: event.agent, - messageID: event.messageID, - callID: event.callID, - input: event.input, - } - return Reflect.apply(callback, undefined, [output]).pipe( - Effect.tap(() => Effect.sync(() => (event.input = output.input))), - ) - }) - } - return toolHooks.hook.after((event) => { - // Decode first so plugin mutations cannot alias the canonical outcome. - const output = { - tool: event.tool, - sessionID: event.sessionID, - agent: event.agent, - messageID: event.messageID, - callID: event.callID, - input: event.input, - ...Schema.decodeUnknownSync(Tool.ExecuteAfterOutcome)(event), - } - return Reflect.apply(callback, undefined, [output]).pipe( - Effect.tap(() => { - const decoded = Schema.decodeUnknownOption(Tool.ExecuteAfterOutcome)(output) - if (decoded._tag === "None") - return Effect.logWarning("ignoring invalid execute.after tool outcome", { tool: event.tool }) - if (decoded.value.status !== event.status) - return Effect.logWarning("ignoring execute.after tool status change", { tool: event.tool }) - return Effect.sync(() => { - if (event.status === "completed" && decoded.value.status === "completed") { - event.content = decoded.value.content - event.metadata = decoded.value.metadata - event.outputPaths = decoded.value.outputPaths - return - } - if (event.status === "error" && decoded.value.status === "error") { - event.error = decoded.value.error - event.content = decoded.value.content - event.metadata = decoded.value.metadata - event.outputPaths = decoded.value.outputPaths - } - }) + add: (tool) => draft.add(tool), }), ) - }) - }, + .pipe(Effect.orDie, Effect.as({ dispose: Effect.void })), + hook: (name, callback) => hooks.register("tool", name, callback), }, websearch: { providers: () => response(websearch.providers()), diff --git a/packages/core/src/plugin/internal.ts b/packages/core/src/plugin/internal.ts index 815dd4339d10..b46a6ae74609 100644 --- a/packages/core/src/plugin/internal.ts +++ b/packages/core/src/plugin/internal.ts @@ -1,11 +1,11 @@ export * as PluginInternal from "./internal" -import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Plugin } from "@opencode-ai/plugin/effect/plugin" import { Context, Effect, Scope } from "effect" import { HttpClient } from "effect/unstable/http" -import { AgentV2 } from "../agent" +import { Agent } from "../agent" import { Catalog } from "../catalog" -import { CommandV2 } from "../command" +import { Command } from "../command" import { Config } from "../config" import { ConfigAgentPlugin } from "../config/plugin/agent" import { ConfigCommandPlugin } from "../config/plugin/command" @@ -14,7 +14,7 @@ import { ConfigPolicyPlugin } from "../config/plugin/policy" import { ConfigReferencePlugin } from "../config/plugin/reference" import { ConfigSkillPlugin } from "../config/plugin/skill" import { ConfigWebSearchPlugin } from "../config/plugin/websearch" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { FileMutation } from "../file-mutation" import { Form } from "../form" import { FileSystem } from "../filesystem" @@ -27,28 +27,28 @@ import { Location } from "../location" import { LocationMutation } from "../location-mutation" import { ModelsDev } from "../models-dev" import { Npm } from "@opencode-ai/util/npm" -import { PermissionV2 } from "../permission" +import { Permission } from "../permission" import { Reference } from "../reference" import { WebSearch } from "../websearch" import { Ripgrep } from "../ripgrep" import { SessionInstructions } from "../session/instructions" import { Shell } from "../shell" -import { SkillV2 } from "../skill" -import { PatchTool } from "../tool/patch" -import { EditTool } from "../tool/edit" -import { GlobTool } from "../tool/glob" -import { GrepTool } from "../tool/grep" -import { QuestionTool } from "../tool/question" +import { Skill } from "../skill" +import { PatchTool } from "../tool/plugin/patch" +import { EditTool } from "../tool/plugin/edit" +import { GlobTool } from "../tool/plugin/glob" +import { GrepTool } from "../tool/plugin/grep" +import { QuestionTool } from "../tool/plugin/question" import { ReadToolFileSystem } from "../tool/read-filesystem" -import { ReadTool } from "../tool/read" -import { ShellTool } from "../tool/shell" -import { SkillTool } from "../tool/skill" -import { SubagentTool } from "../tool/subagent" -import { Tools } from "../tool/tools" -import { WebFetchTool } from "../tool/webfetch" -import { WebSearchTool } from "../tool/websearch" +import { ReadTool } from "../tool/plugin/read" +import { ShellTool } from "../tool/plugin/shell" +import { SkillTool } from "../tool/plugin/skill" +import { SubagentTool } from "../tool/plugin/subagent" +import { Tool } from "../tool" +import { WebFetchTool } from "../tool/plugin/webfetch" +import { WebSearchTool } from "../tool/plugin/websearch" import { WellKnown } from "../wellknown" -import { WriteTool } from "../tool/write" +import { WriteTool } from "../tool/plugin/write" import { AgentPlugin } from "./agent" import { CommandPlugin } from "./command" import { ModelsDevPlugin } from "./models-dev" @@ -62,11 +62,11 @@ import { WarmingPlugin } from "./warming" import { WellKnownPlugin } from "../wellknown/plugin" const services = Effect.fn("PluginInternal.services")(function* () { - const agent = yield* AgentV2.Service + const agent = yield* Agent.Service const catalog = yield* Catalog.Service - const command = yield* CommandV2.Service + const command = yield* Command.Service const config = yield* Config.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const mutation = yield* FileMutation.Service const filesystem = yield* FileSystem.Service const fs = yield* FSUtil.Service @@ -79,7 +79,7 @@ const services = Effect.fn("PluginInternal.services")(function* () { const locationMutation = yield* LocationMutation.Service const models = yield* ModelsDev.Service const npm = yield* Npm.Service - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service const runtime = yield* PluginRuntime.Service const form = yield* Form.Service const read = yield* ReadToolFileSystem.Service @@ -88,15 +88,15 @@ const services = Effect.fn("PluginInternal.services")(function* () { const ripgrep = yield* Ripgrep.Service const instructions = yield* SessionInstructions.Service const shell = yield* Shell.Service - const skill = yield* SkillV2.Service - const tools = yield* Tools.Service + const skill = yield* Skill.Service + const tools = yield* Tool.Service const wellknown = yield* WellKnown.Service return Context.mergeAll( - Context.make(AgentV2.Service, agent), + Context.make(Agent.Service, agent), Context.make(Catalog.Service, catalog), - Context.make(CommandV2.Service, command), + Context.make(Command.Service, command), Context.make(Config.Service, config), - Context.make(EventV2.Service, events), + Context.make(Bus.Service, bus), Context.make(FileMutation.Service, mutation), Context.make(FileSystem.Service, filesystem), Context.make(FSUtil.Service, fs), @@ -109,7 +109,7 @@ const services = Effect.fn("PluginInternal.services")(function* () { Context.make(LocationMutation.Service, locationMutation), Context.make(ModelsDev.Service, models), Context.make(Npm.Service, npm), - Context.make(PermissionV2.Service, permission), + Context.make(Permission.Service, permission), Context.make(PluginRuntime.Service, runtime), Context.make(Form.Service, form), Context.make(ReadToolFileSystem.Service, read), @@ -118,8 +118,8 @@ const services = Effect.fn("PluginInternal.services")(function* () { Context.make(Ripgrep.Service, ripgrep), Context.make(SessionInstructions.Service, instructions), Context.make(Shell.Service, shell), - Context.make(SkillV2.Service, skill), - Context.make(Tools.Service, tools), + Context.make(Skill.Service, skill), + Context.make(Tool.Service, tools), Context.make(WellKnown.Service, wellknown), ) }) diff --git a/packages/core/src/plugin/models-dev.ts b/packages/core/src/plugin/models-dev.ts index ec23dfd36b33..b643cfd75587 100644 --- a/packages/core/src/plugin/models-dev.ts +++ b/packages/core/src/plugin/models-dev.ts @@ -1,13 +1,14 @@ -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Integration } from "@opencode-ai/schema/integration" import { Effect, Stream } from "effect" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { ModelsDev } from "../models-dev" export const ModelsDevPlugin = define({ id: "opencode.models-dev", effect: Effect.fn(function* (ctx) { const modelsDev = yield* ModelsDev.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const loaded = { data: structuredClone(yield* modelsDev.get()) } yield* ctx.integration.transform((integrations) => { for (const provider of loaded.data) { @@ -28,14 +29,14 @@ export const ModelsDevPlugin = define({ for (const provider of loaded.data) { catalog.provider.update(provider.info.id, (draft) => { Object.assign(draft, provider.info) - draft.integrationID = provider.info.id + draft.integrationID = Integration.ID.make(provider.info.id) }) for (const model of provider.models) { catalog.model.update(provider.info.id, model.id, (draft) => Object.assign(draft, model)) } } }) - yield* events.subscribe(ModelsDev.Event.Refreshed).pipe( + yield* bus.subscribe(ModelsDev.Event.Refreshed).pipe( Stream.runForEach(() => modelsDev.get().pipe( Effect.tap((data) => Effect.sync(() => (loaded.data = structuredClone(data)))), diff --git a/packages/core/src/plugin/promise.ts b/packages/core/src/plugin/promise.ts index a5172b0c98ac..21380800af48 100644 --- a/packages/core/src/plugin/promise.ts +++ b/packages/core/src/plugin/promise.ts @@ -1,8 +1,8 @@ export * as PluginPromise from "./promise" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import type { Context, Plugin } from "@opencode-ai/plugin/v2/plugin" -import type { Any, RegisterOptions } from "@opencode-ai/plugin/v2/tool" +import { define } from "@opencode-ai/plugin/effect/plugin" +import type { Context, Plugin } from "@opencode-ai/plugin/promise/plugin" +import type { Info } from "@opencode-ai/plugin/promise/tool" import { Agent } from "@opencode-ai/schema/agent" import { Integration } from "@opencode-ai/schema/integration" import { Location } from "@opencode-ai/schema/location" @@ -14,7 +14,7 @@ import { SessionMessage } from "@opencode-ai/schema/session-message" import { Workspace } from "@opencode-ai/schema/workspace" import { WebSearch } from "@opencode-ai/schema/websearch" import { DateTime, Effect, Scope, Stream } from "effect" -import { Tool } from "../tool/tool" +import { Tool } from "../tool" type HostRegistration = { readonly dispose: Effect.Effect } type Registration = { readonly dispose: () => Promise } @@ -23,7 +23,7 @@ type JsonValue = null | boolean | number | string | Array | { [key: s /** * Adapts a Promise plugin into an Effect plugin so the existing Effect-only - * loader (`PluginV2` / `PluginSupervisor`) can run it unchanged. + * loader (`Plugin` / `PluginSupervisor`) can run it unchanged. * * Hook registrations created during the async `setup` attach to the plugin's * scope, so unloading the plugin disposes them. The captured fiber context @@ -61,7 +61,7 @@ export function fromPromise(plugin: Plugin) { app: host.app, options: host.options, agent: { - get: (id) => run(host.agent.get(id)), + get: (input) => run(host.agent.get({ ...input, agentID: Agent.ID.make(input.agentID) })), list: (input) => run(host.agent.list(input)), transform: transform(host.agent), reload: () => run(host.agent.reload()), @@ -77,7 +77,6 @@ export function fromPromise(plugin: Plugin) { run(host.catalog.provider.get({ ...input, providerID: Provider.ID.make(input.providerID) })), }, model: { - get: (providerID, modelID) => run(host.catalog.model.get(providerID, modelID)), list: (input) => run(host.catalog.model.list(input)), default: (input) => run(host.catalog.model.default(input)).then((result) => ({ ...result, data: result.data ?? null })), @@ -165,7 +164,44 @@ export function fromPromise(plugin: Plugin) { }), ), }, - transform: transform(host.integration), + transform: (callback) => + register( + host.integration.transform((draft) => + callback({ + list: draft.list, + get: draft.get, + update: draft.update, + remove: draft.remove, + method: { + list: draft.method.list, + update: (input) => { + if (!("authorize" in input)) return draft.method.update(input) + const refresh = input.refresh + draft.method.update({ + ...input, + authorize: (inputs) => + Effect.promise(() => input.authorize(inputs)).pipe( + Effect.map((authorization) => + authorization.mode === "auto" + ? { + ...authorization, + callback: Effect.promise(() => authorization.callback), + } + : { + ...authorization, + callback: (code) => Effect.promise(() => authorization.callback(code)), + }, + ), + ), + refresh: + refresh === undefined ? undefined : (credential) => Effect.promise(() => refresh(credential)), + }) + }, + remove: draft.method.remove, + }, + }), + ), + ), reload: () => run(host.integration.reload()), connection: { active: (id) => Effect.runPromiseWith(context)(host.integration.connection.active(id)), @@ -190,8 +226,11 @@ export function fromPromise(plugin: Plugin) { register( host.tool.transform((draft) => callback({ - add: (name: string, tool: Any, options?: RegisterOptions) => - draft.add(name, fromPromiseTool(tool), options), + add: (tool: Info) => + draft.add({ + ...tool, + execute: (input, context) => executePromiseTool(tool, input, context), + }), }), ), ), @@ -333,15 +372,10 @@ function wireEvent(value: unknown): unknown { return wire(value) } -function fromPromiseTool(tool: Any): Tool.Any { - return { - ...tool, - execute: (input, context) => - Effect.promise(() => - tool.execute(input, { - ...context, - progress: (update) => Effect.runPromise(context.progress(update)), - }), - ), - } -} +const executePromiseTool = (tool: Info, input: any, context: Tool.Context) => + Effect.promise(() => + tool.execute(input, { + ...context, + progress: (update) => Effect.runPromise(context.progress(update)), + }), + ) diff --git a/packages/core/src/plugin/provider/alibaba.ts b/packages/core/src/plugin/provider/alibaba.ts index ea37b004531d..47c7e6b25c38 100644 --- a/packages/core/src/plugin/provider/alibaba.ts +++ b/packages/core/src/plugin/provider/alibaba.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const AlibabaPlugin = define({ id: "opencode.provider.alibaba", diff --git a/packages/core/src/plugin/provider/amazon-bedrock.ts b/packages/core/src/plugin/provider/amazon-bedrock.ts index ecc137bbde3b..322f5ba45c29 100644 --- a/packages/core/src/plugin/provider/amazon-bedrock.ts +++ b/packages/core/src/plugin/provider/amazon-bedrock.ts @@ -1,7 +1,7 @@ import { Effect } from "effect" import type { LanguageModelV3 } from "@ai-sdk/provider" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" type MantleSDK = { languageModel: (modelID: string) => LanguageModelV3 @@ -64,8 +64,8 @@ export const AmazonBedrockPlugin = define({ effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/amazon-bedrock") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/amazon-bedrock") continue evt.provider.update(item.provider.id, (provider) => { if (typeof provider.settings?.endpoint !== "string") return // The AI SDK expects a base URL, but users configure Bedrock private/VPC @@ -112,10 +112,10 @@ export const AmazonBedrockPlugin = define({ yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return + if (evt.model.providerID !== Provider.ID.amazonBedrock) return if ( - ProviderV2.isAISDK(evt.model.package) && - ProviderV2.packageName(evt.model.package) === "@ai-sdk/amazon-bedrock/mantle" + Provider.isAISDK(evt.model.package) && + Provider.packageName(evt.model.package) === "@ai-sdk/amazon-bedrock/mantle" ) { evt.language = selectMantleModel(evt.sdk, evt.model.modelID ?? evt.model.id) return diff --git a/packages/core/src/plugin/provider/anthropic.ts b/packages/core/src/plugin/provider/anthropic.ts index bbf0d56dfbe2..06ba5042014c 100644 --- a/packages/core/src/plugin/provider/anthropic.ts +++ b/packages/core/src/plugin/provider/anthropic.ts @@ -1,14 +1,14 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" export const AnthropicPlugin = define({ id: "opencode.provider.anthropic", effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/anthropic") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/anthropic") continue evt.provider.update(item.provider.id, (provider) => { provider.headers = { ...provider.headers, diff --git a/packages/core/src/plugin/provider/azure.ts b/packages/core/src/plugin/provider/azure.ts index f50bbdb1ba22..ccc12eef7f43 100644 --- a/packages/core/src/plugin/provider/azure.ts +++ b/packages/core/src/plugin/provider/azure.ts @@ -1,6 +1,6 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" function selectLanguage(sdk: any, modelID: string, useChat: boolean) { if (useChat && sdk.chat) return sdk.chat(modelID) @@ -15,8 +15,8 @@ export const AzurePlugin = define({ effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/azure") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/azure") continue const configured = item.provider.settings?.resourceName const resourceName = typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME @@ -30,11 +30,11 @@ export const AzurePlugin = define({ "sdk", Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/azure") return - if (evt.model.providerID === ProviderV2.ID.azure) { + if (evt.model.providerID === Provider.ID.azure) { if ( !evt.options.resourceName && !evt.options.baseURL && - (!ProviderV2.isAISDK(evt.model.package) || typeof evt.model.settings?.baseURL !== "string") + (!Provider.isAISDK(evt.model.package) || typeof evt.model.settings?.baseURL !== "string") ) { throw new Error( "AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it", @@ -48,7 +48,7 @@ export const AzurePlugin = define({ yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.azure) return + if (evt.model.providerID !== Provider.ID.azure) return evt.language = selectLanguage( evt.sdk, evt.model.modelID ?? evt.model.id, @@ -66,8 +66,8 @@ export const AzureCognitiveServicesPlugin = define({ const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME if (!resourceName) return for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue if (!item.provider.id.includes("azure-cognitive-services")) continue evt.provider.update(item.provider.id, (provider) => { provider.settings = { @@ -80,7 +80,7 @@ export const AzureCognitiveServicesPlugin = define({ yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return + if (evt.model.providerID !== Provider.ID.make("azure-cognitive-services")) return evt.language = selectLanguage( evt.sdk, evt.model.modelID ?? evt.model.id, diff --git a/packages/core/src/plugin/provider/cerebras.ts b/packages/core/src/plugin/provider/cerebras.ts index 13614d6be081..cb071e71fb42 100644 --- a/packages/core/src/plugin/provider/cerebras.ts +++ b/packages/core/src/plugin/provider/cerebras.ts @@ -1,14 +1,14 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" export const CerebrasPlugin = define({ id: "opencode.provider.cerebras", effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/cerebras") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/cerebras") continue evt.provider.update(item.provider.id, (provider) => { provider.headers = { ...provider.headers, "X-Cerebras-3rd-Party-Integration": "opencode" } }) diff --git a/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts b/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts index 6787e9169799..6e4234711527 100644 --- a/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts +++ b/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts @@ -1,7 +1,7 @@ import os from "os" import { App } from "../../app" import { Effect, Option, Schema } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const CloudflareAIGatewayPlugin = define({ id: "opencode.provider.cloudflare-ai-gateway", diff --git a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts index 99f94320baff..1ac31d77f993 100644 --- a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts +++ b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts @@ -1,10 +1,10 @@ import os from "os" import { App } from "../../app" import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" -const providerID = ProviderV2.ID.make("cloudflare-workers-ai") +const providerID = Provider.ID.make("cloudflare-workers-ai") export const CloudflareWorkersAIPlugin = define({ id: "opencode.provider.cloudflare-workers-ai", @@ -13,7 +13,7 @@ export const CloudflareWorkersAIPlugin = define({ const item = evt.provider.get(providerID) if (!item) return evt.provider.update(item.provider.id, (provider) => { - if (!ProviderV2.isAISDK(provider.package)) return + if (!Provider.isAISDK(provider.package)) return if (typeof provider.settings?.baseURL === "string") return const accountId = resolveAccountId(provider.settings ?? {}) if (accountId) provider.settings = { ...provider.settings, baseURL: workersEndpoint(accountId) } @@ -61,7 +61,7 @@ function hasWorkersEndpoint(model: { readonly package?: string readonly settings?: Readonly> }) { - return ProviderV2.isAISDK(model.package) && typeof model.settings?.baseURL === "string" + return Provider.isAISDK(model.package) && typeof model.settings?.baseURL === "string" } function sdkOptions(options: Record, app: App.Info) { diff --git a/packages/core/src/plugin/provider/cohere.ts b/packages/core/src/plugin/provider/cohere.ts index 9284defcb14a..7fffb9db4b76 100644 --- a/packages/core/src/plugin/provider/cohere.ts +++ b/packages/core/src/plugin/provider/cohere.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const CoherePlugin = define({ id: "opencode.provider.cohere", diff --git a/packages/core/src/plugin/provider/deepinfra.ts b/packages/core/src/plugin/provider/deepinfra.ts index f4ddf97859a0..6f76bf6c37ff 100644 --- a/packages/core/src/plugin/provider/deepinfra.ts +++ b/packages/core/src/plugin/provider/deepinfra.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const DeepInfraPlugin = define({ id: "opencode.provider.deepinfra", diff --git a/packages/core/src/plugin/provider/dynamic.ts b/packages/core/src/plugin/provider/dynamic.ts index 4e8eedb81ef9..aea911363cb3 100644 --- a/packages/core/src/plugin/provider/dynamic.ts +++ b/packages/core/src/plugin/provider/dynamic.ts @@ -1,6 +1,6 @@ import { Effect } from "effect" import { pathToFileURL } from "url" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Npm } from "@opencode-ai/util/npm" import { importModule } from "@opencode-ai/util/runtime-import" diff --git a/packages/core/src/plugin/provider/gateway.ts b/packages/core/src/plugin/provider/gateway.ts index b67c0ef1798b..8ee5a6ccf319 100644 --- a/packages/core/src/plugin/provider/gateway.ts +++ b/packages/core/src/plugin/provider/gateway.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const GatewayPlugin = define({ id: "opencode.provider.gateway", diff --git a/packages/core/src/plugin/provider/github-copilot.ts b/packages/core/src/plugin/provider/github-copilot.ts index 94414eda3d75..c8aff68697cb 100644 --- a/packages/core/src/plugin/provider/github-copilot.ts +++ b/packages/core/src/plugin/provider/github-copilot.ts @@ -1,14 +1,14 @@ -import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration" +import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration" import { Effect, Option, Schema, Semaphore, Stream } from "effect" import { Catalog } from "../../catalog" import { Credential } from "../../credential" -import { EventV2 } from "../../event" +import { Bus } from "../../bus" import { CopilotModels } from "../../github-copilot/models" import { App } from "../../app" import { Integration } from "../../integration" -import { ModelV2 } from "../../model" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { Model } from "../../model" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" import type { PluginInternal } from "../internal" const clientID = "Ov23li8tweQw6odWQebz" @@ -152,11 +152,11 @@ export const GithubCopilotPlugin = define({ id: "opencode.provider.github-copilot", effect: Effect.fn(function* (ctx) { const catalog = yield* Catalog.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const loading = Semaphore.makeUnsafe(1) const loaded: { baseURL?: string - models?: Map + models?: Map } = {} const load = Effect.fn("GithubCopilotPlugin.load")(function* () { @@ -171,8 +171,8 @@ export const GithubCopilotPlugin = define({ } loaded.baseURL = copilotBaseURL(credential.metadata) - const provider = yield* catalog.provider.get(ProviderV2.ID.githubCopilot) - const existing = (yield* catalog.model.all()).filter((model) => model.providerID === ProviderV2.ID.githubCopilot) + const provider = yield* catalog.provider.get(Provider.ID.githubCopilot) + const existing = (yield* catalog.model.all()).filter((model) => model.providerID === Provider.ID.githubCopilot) loaded.models = yield* Effect.tryPromise({ try: () => CopilotModels.get( @@ -197,11 +197,11 @@ export const GithubCopilotPlugin = define({ draft.method.update(oauth(ctx.app)) }) yield* ctx.catalog.transform((evt) => { - const item = evt.provider.get(ProviderV2.ID.githubCopilot) + const item = evt.provider.get(Provider.ID.githubCopilot) if (!item) return if (loaded.models) { for (const id of item.models.keys()) { - if (!loaded.models.has(ModelV2.ID.make(id))) evt.model.remove(item.provider.id, id) + if (!loaded.models.has(Model.ID.make(id))) evt.model.remove(item.provider.id, id) } for (const [id, model] of loaded.models) { evt.model.update(item.provider.id, id, (draft) => Object.assign(draft, structuredClone(model))) @@ -209,12 +209,12 @@ export const GithubCopilotPlugin = define({ } else if (loaded.baseURL) { for (const id of item.models.keys()) { evt.model.update(item.provider.id, id, (model) => { - model.settings = ProviderV2.mergeOverlay(model.settings, { baseURL: loaded.baseURL }) + model.settings = Provider.mergeOverlay(model.settings, { baseURL: loaded.baseURL }) }) } } - if (item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) { - evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => { + if (item.models.has(Model.ID.make("gpt-5-chat-latest"))) { + evt.model.update(item.provider.id, Model.ID.make("gpt-5-chat-latest"), (model) => { // This chat-only alias conflicts with the Copilot GPT-5 Responses route, // so hide it only for Copilot rather than for every provider catalog. model.enabled = false @@ -222,7 +222,7 @@ export const GithubCopilotPlugin = define({ } }) const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload()))) - yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe( + yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe( Stream.filter((event) => event.data.integrationID === Integration.ID.make("github-copilot")), Stream.runForEach(refresh), Effect.forkScoped({ startImmediately: true }), @@ -231,7 +231,7 @@ export const GithubCopilotPlugin = define({ yield* ctx.aisdk.hook( "sdk", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return + if (evt.model.providerID !== Provider.ID.githubCopilot) return if (evt.package !== "@ai-sdk/github-copilot" && evt.package !== "@ai-sdk/anthropic") return evt.options.fetch = copilotFetch( typeof evt.options.apiKey === "string" ? evt.options.apiKey : undefined, @@ -255,7 +255,7 @@ export const GithubCopilotPlugin = define({ yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return + if (evt.model.providerID !== Provider.ID.githubCopilot) return if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) { evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id) return diff --git a/packages/core/src/plugin/provider/gitlab.ts b/packages/core/src/plugin/provider/gitlab.ts index 9772a38b3a63..273541179062 100644 --- a/packages/core/src/plugin/provider/gitlab.ts +++ b/packages/core/src/plugin/provider/gitlab.ts @@ -1,8 +1,8 @@ import os from "os" import { App } from "../../app" import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" export const GitLabPlugin = define({ id: "opencode.provider.gitlab", @@ -35,7 +35,7 @@ export const GitLabPlugin = define({ yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.gitlab) return + if (evt.model.providerID !== Provider.ID.gitlab) return const featureFlags = typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {} const id = evt.model.modelID ?? evt.model.id diff --git a/packages/core/src/plugin/provider/google-vertex.ts b/packages/core/src/plugin/provider/google-vertex.ts index ffc29f4bf354..8014f2662c06 100644 --- a/packages/core/src/plugin/provider/google-vertex.ts +++ b/packages/core/src/plugin/provider/google-vertex.ts @@ -1,6 +1,6 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" function resolveProject(options: Record) { // models.dev advertises GOOGLE_VERTEX_PROJECT for Vertex, while Google SDKs @@ -59,12 +59,12 @@ export const GoogleVertexPlugin = define({ effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue + if (!Provider.isAISDK(item.provider.package)) continue if ( - ProviderV2.packageName(item.provider.package) !== "@ai-sdk/google-vertex" && + Provider.packageName(item.provider.package) !== "@ai-sdk/google-vertex" && !( - item.provider.id === ProviderV2.ID.googleVertex && - ProviderV2.packageName(item.provider.package)?.includes("@ai-sdk/openai-compatible") + item.provider.id === Provider.ID.googleVertex && + Provider.packageName(item.provider.package)?.includes("@ai-sdk/openai-compatible") ) ) continue @@ -78,7 +78,7 @@ export const GoogleVertexPlugin = define({ ...(typeof provider.settings?.baseURL === "string" ? { baseURL: replaceVertexVars(provider.settings.baseURL, project, location) } : {}), - ...(ProviderV2.packageName(provider.package)?.includes("@ai-sdk/openai-compatible") + ...(Provider.packageName(provider.package)?.includes("@ai-sdk/openai-compatible") ? { fetch: authFetch(provider.settings?.fetch) } : {}), } @@ -88,7 +88,7 @@ export const GoogleVertexPlugin = define({ yield* ctx.aisdk.hook( "sdk", Effect.fn(function* (evt) { - if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) { + if (evt.model.providerID === Provider.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) { evt.options.fetch = authFetch(evt.options.fetch) return } @@ -108,7 +108,7 @@ export const GoogleVertexPlugin = define({ yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.googleVertex) return + if (evt.model.providerID !== Provider.ID.googleVertex) return evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim()) }), ) @@ -120,8 +120,8 @@ export const GoogleVertexAnthropicPlugin = define({ effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/google-vertex/anthropic") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/google-vertex/anthropic") continue const project = item.provider.settings?.project ?? process.env.GOOGLE_CLOUD_PROJECT ?? @@ -167,7 +167,7 @@ export const GoogleVertexAnthropicPlugin = define({ yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return + if (evt.model.providerID !== Provider.ID.make("google-vertex-anthropic")) return evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim()) }), ) diff --git a/packages/core/src/plugin/provider/google.ts b/packages/core/src/plugin/provider/google.ts index c62d962eb590..f30be656f7ff 100644 --- a/packages/core/src/plugin/provider/google.ts +++ b/packages/core/src/plugin/provider/google.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const GooglePlugin = define({ id: "opencode.provider.google", diff --git a/packages/core/src/plugin/provider/groq.ts b/packages/core/src/plugin/provider/groq.ts index 51b8e4c42f7c..f2f078979eaf 100644 --- a/packages/core/src/plugin/provider/groq.ts +++ b/packages/core/src/plugin/provider/groq.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const GroqPlugin = define({ id: "opencode.provider.groq", diff --git a/packages/core/src/plugin/provider/kilo.ts b/packages/core/src/plugin/provider/kilo.ts index b1c53fca9083..0d69c92d5c5e 100644 --- a/packages/core/src/plugin/provider/kilo.ts +++ b/packages/core/src/plugin/provider/kilo.ts @@ -1,14 +1,14 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" export const KiloPlugin = define({ id: "opencode.provider.kilo", effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue if (item.provider.settings?.baseURL !== "https://api.kilo.ai/api/gateway") continue evt.provider.update(item.provider.id, (provider) => { provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" } diff --git a/packages/core/src/plugin/provider/llmgateway.ts b/packages/core/src/plugin/provider/llmgateway.ts index 88a597164b4a..2f545478c9dc 100644 --- a/packages/core/src/plugin/provider/llmgateway.ts +++ b/packages/core/src/plugin/provider/llmgateway.ts @@ -1,7 +1,7 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Integration } from "../../integration" -import { ProviderV2 } from "../../provider" +import { Provider } from "../../provider" export const LLMGatewayPlugin = define({ id: "opencode.provider.llmgateway", @@ -11,8 +11,8 @@ export const LLMGatewayPlugin = define({ yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { if (item.provider.disabled) continue - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue if (item.provider.settings?.baseURL !== "https://api.llmgateway.io/v1") continue if (!configured.has(Integration.ID.make(item.provider.id))) continue evt.provider.update(item.provider.id, (provider) => { diff --git a/packages/core/src/plugin/provider/mistral.ts b/packages/core/src/plugin/provider/mistral.ts index f5de664432eb..f7a42deb40b1 100644 --- a/packages/core/src/plugin/provider/mistral.ts +++ b/packages/core/src/plugin/provider/mistral.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const MistralPlugin = define({ id: "opencode.provider.mistral", diff --git a/packages/core/src/plugin/provider/nvidia.ts b/packages/core/src/plugin/provider/nvidia.ts index 7363e88f52a7..41581366d8a0 100644 --- a/packages/core/src/plugin/provider/nvidia.ts +++ b/packages/core/src/plugin/provider/nvidia.ts @@ -1,14 +1,14 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" export const NvidiaPlugin = define({ id: "opencode.provider.nvidia", effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue if (item.provider.settings?.baseURL !== "https://integrate.api.nvidia.com/v1") continue evt.provider.update(item.provider.id, (provider) => { provider.headers = { diff --git a/packages/core/src/plugin/provider/openai-compatible.ts b/packages/core/src/plugin/provider/openai-compatible.ts index 76646b4e453c..fb523a3a2424 100644 --- a/packages/core/src/plugin/provider/openai-compatible.ts +++ b/packages/core/src/plugin/provider/openai-compatible.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const OpenAICompatiblePlugin = define({ id: "opencode.provider.openai-compatible", diff --git a/packages/core/src/plugin/provider/openai.ts b/packages/core/src/plugin/provider/openai.ts index b6ff9e581064..e133d9edc22a 100644 --- a/packages/core/src/plugin/provider/openai.ts +++ b/packages/core/src/plugin/provider/openai.ts @@ -1,14 +1,14 @@ import { createServer } from "node:http" -import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Deferred, Effect, Option, Schema, Semaphore, Stream } from "effect" import { App } from "../../app" import { Credential } from "../../credential" -import { EventV2 } from "../../event" +import { Bus } from "../../bus" import { Integration } from "../../integration" -import { ModelV2 } from "../../model" +import { Model } from "../../model" import { OauthCallbackPage } from "../../oauth/page" -import { ProviderV2 } from "../../provider" +import { Provider } from "../../provider" import type { PluginInternal } from "../internal" import { OpenAICodex } from "./openai-codex" @@ -162,7 +162,7 @@ const headless = (app: App.Info) => ({ export const OpenAIPlugin = define({ id: "opencode.provider.openai", effect: Effect.fn(function* (ctx) { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const loading = Semaphore.makeUnsafe(1) let chatgpt = false @@ -181,17 +181,17 @@ export const OpenAIPlugin = define({ yield* load() yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai") continue - if (!item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) continue - evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => { + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai") continue + if (!item.models.has(Model.ID.make("gpt-5-chat-latest"))) continue + evt.model.update(item.provider.id, Model.ID.make("gpt-5-chat-latest"), (model) => { // OpenAIPlugin sends OpenAI models through Responses; this alias is a // chat-completions-only model, so hide it only from OpenAI's catalog. model.enabled = false }) } if (!chatgpt) return - const item = evt.provider.get(ProviderV2.ID.openai) + const item = evt.provider.get(Provider.ID.openai) if (!item) return for (const model of item.models.values()) { // ChatGPT-plan tokens only authorize codex-eligible models, and the @@ -211,7 +211,7 @@ export const OpenAIPlugin = define({ }) const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload()))) - yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe( + yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe( Stream.filter((event) => event.data.integrationID === Integration.ID.make("openai")), Stream.runForEach(refresh), Effect.forkScoped({ startImmediately: true }), @@ -227,7 +227,7 @@ export const OpenAIPlugin = define({ yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.openai) return + if (evt.model.providerID !== Provider.ID.openai) return evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id) }), ) diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts index e70be4281429..75704a1c7b89 100644 --- a/packages/core/src/plugin/provider/opencode.ts +++ b/packages/core/src/plugin/provider/opencode.ts @@ -1,14 +1,14 @@ import { Duration, Effect, Schema, Semaphore, Stream } from "effect" import type { Scope } from "effect" -import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration" +import { define } from "@opencode-ai/plugin/effect/plugin" import type { CredentialValue } from "@opencode-ai/sdk/v2/types" import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" -import { EventV2 } from "../../event" +import { Bus } from "../../bus" import { Credential } from "../../credential" import { Integration } from "../../integration" -import { ModelV2 } from "../../model" -import { ProviderV2 } from "../../provider" +import { Model } from "../../model" +import { Provider } from "../../provider" import { ConfigProviderV1 } from "../../v1/config/provider" import { Money } from "@opencode-ai/schema/money" import { ConfigProviderOptionsV1 } from "../../v1/config/provider-options" @@ -82,10 +82,10 @@ function oauth(http: HttpClient.HttpClient) { } satisfies IntegrationOAuthMethodRegistration } -export const OpencodePlugin = define({ +export const OpencodePlugin = define({ id: "opencode.provider.opencode", effect: Effect.fn(function* (ctx) { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const http = yield* HttpClient.HttpClient const loading = Semaphore.makeUnsafe(1) let connected = false @@ -120,7 +120,7 @@ export const OpencodePlugin = define { provider.integrationID = Integration.ID.make("opencode") if (item.name !== undefined) provider.name = item.name - provider.package = item.npm ? ProviderV2.aisdk(item.npm) : "" + provider.package = item.npm ? Provider.aisdk(item.npm) : "" provider.settings = { ...provider.settings, ...withoutCredentials(item.options), @@ -131,12 +131,12 @@ export const OpencodePlugin = define { - if (config.family !== undefined) model.family = config.family + if (config.family !== undefined) model.family = Model.Family.make(config.family) if (config.name !== undefined) model.name = config.name - if (config.id !== undefined) model.modelID = config.id - model.compatibility = ModelV2.compatibility(config.interleaved) ?? model.compatibility + if (config.id !== undefined) model.modelID = Model.ID.make(config.id) + model.compatibility = Model.compatibility(config.interleaved) ?? model.compatibility if (config.provider !== undefined) { - model.package = config.provider.npm ? ProviderV2.aisdk(config.provider.npm) : undefined + model.package = config.provider.npm ? Provider.aisdk(config.provider.npm) : undefined if (config.provider.api) model.settings = { ...model.settings, baseURL: config.provider.api } } if (config.tool_call !== undefined) model.capabilities.tools = config.tool_call @@ -147,7 +147,7 @@ export const OpencodePlugin = define item.id === variantID) if (!existing) { existing = { id: variantID } @@ -174,7 +174,7 @@ export const OpencodePlugin = define { @@ -190,7 +190,7 @@ export const OpencodePlugin = define loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload()))) - yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe( + yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe( Stream.filter((event) => event.data.integrationID === Integration.ID.make("opencode")), Stream.runForEach(refresh), Effect.forkScoped({ startImmediately: true }), diff --git a/packages/core/src/plugin/provider/openrouter.ts b/packages/core/src/plugin/provider/openrouter.ts index f00386a94a1e..a9ace9f91ad2 100644 --- a/packages/core/src/plugin/provider/openrouter.ts +++ b/packages/core/src/plugin/provider/openrouter.ts @@ -1,19 +1,19 @@ import { Effect } from "effect" -import { ModelV2 } from "../../model" -import { ProviderV2 } from "../../provider" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { Model } from "../../model" +import { Provider } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" export const OpenRouterPlugin = define({ id: "opencode.provider.openrouter", effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@openrouter/ai-sdk-provider") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@openrouter/ai-sdk-provider") continue evt.provider.update(item.provider.id, (provider) => { provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" } }) - for (const modelID of [ModelV2.ID.make("gpt-5-chat-latest"), ModelV2.ID.make("openai/gpt-5-chat")]) { + for (const modelID of [Model.ID.make("gpt-5-chat-latest"), Model.ID.make("openai/gpt-5-chat")]) { if (!item.models.has(modelID)) continue evt.model.update(item.provider.id, modelID, (model) => { // These are OpenRouter-specific OpenAI chat aliases that do not work diff --git a/packages/core/src/plugin/provider/perplexity.ts b/packages/core/src/plugin/provider/perplexity.ts index 36cafdb2d1cb..37a390948aea 100644 --- a/packages/core/src/plugin/provider/perplexity.ts +++ b/packages/core/src/plugin/provider/perplexity.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const PerplexityPlugin = define({ id: "opencode.provider.perplexity", diff --git a/packages/core/src/plugin/provider/sap-ai-core.ts b/packages/core/src/plugin/provider/sap-ai-core.ts index 4015a77c147d..9a8a28c5bcbb 100644 --- a/packages/core/src/plugin/provider/sap-ai-core.ts +++ b/packages/core/src/plugin/provider/sap-ai-core.ts @@ -1,8 +1,8 @@ import { Effect } from "effect" import { pathToFileURL } from "url" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Npm } from "@opencode-ai/util/npm" -import { ProviderV2 } from "../../provider" +import { Provider } from "../../provider" import { importModule } from "@opencode-ai/util/runtime-import" export const SapAICorePlugin = define({ @@ -12,7 +12,7 @@ export const SapAICorePlugin = define({ yield* ctx.aisdk.hook( "sdk", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return + if (evt.model.providerID !== Provider.ID.make("sap-ai-core")) return const serviceKey = process.env.AICORE_SERVICE_KEY ?? (typeof evt.options.serviceKey === "string" ? evt.options.serviceKey : undefined) @@ -42,7 +42,7 @@ export const SapAICorePlugin = define({ yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return + if (evt.model.providerID !== Provider.ID.make("sap-ai-core")) return evt.language = evt.sdk(evt.model.modelID ?? evt.model.id) }), ) diff --git a/packages/core/src/plugin/provider/snowflake-cortex.ts b/packages/core/src/plugin/provider/snowflake-cortex.ts index 45c5f095ee17..8e4a6f39f731 100644 --- a/packages/core/src/plugin/provider/snowflake-cortex.ts +++ b/packages/core/src/plugin/provider/snowflake-cortex.ts @@ -1,6 +1,6 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise @@ -70,7 +70,7 @@ export const SnowflakeCortexPlugin = define({ yield* ctx.aisdk.hook( "sdk", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return + if (evt.model.providerID !== Provider.ID.make("snowflake-cortex")) return const token = process.env.SNOWFLAKE_CORTEX_TOKEN ?? process.env.SNOWFLAKE_CORTEX_PAT ?? diff --git a/packages/core/src/plugin/provider/togetherai.ts b/packages/core/src/plugin/provider/togetherai.ts index ea43db9e4a52..eb8a8f764208 100644 --- a/packages/core/src/plugin/provider/togetherai.ts +++ b/packages/core/src/plugin/provider/togetherai.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const TogetherAIPlugin = define({ id: "opencode.provider.togetherai", diff --git a/packages/core/src/plugin/provider/venice.ts b/packages/core/src/plugin/provider/venice.ts index 9930ce831d93..1c140db5b23b 100644 --- a/packages/core/src/plugin/provider/venice.ts +++ b/packages/core/src/plugin/provider/venice.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const VenicePlugin = define({ id: "opencode.provider.venice", diff --git a/packages/core/src/plugin/provider/vercel.ts b/packages/core/src/plugin/provider/vercel.ts index fc392fe5e177..5ac2bb296668 100644 --- a/packages/core/src/plugin/provider/vercel.ts +++ b/packages/core/src/plugin/provider/vercel.ts @@ -1,14 +1,14 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" export const VercelPlugin = define({ id: "opencode.provider.vercel", effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/vercel") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/vercel") continue evt.provider.update(item.provider.id, (provider) => { provider.headers = { ...provider.headers, "http-referer": "https://opencode.ai/", "x-title": "opencode" } }) diff --git a/packages/core/src/plugin/provider/xai.ts b/packages/core/src/plugin/provider/xai.ts index c97f8d68f464..f4d6307ddc97 100644 --- a/packages/core/src/plugin/provider/xai.ts +++ b/packages/core/src/plugin/provider/xai.ts @@ -1,12 +1,12 @@ import { createServer } from "node:http" -import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Clock, Deferred, Effect, Option, Schema } from "effect" import { App } from "../../app" import { Credential } from "../../credential" import { Integration } from "../../integration" import { OauthCallbackPage } from "../../oauth/page" -import { ProviderV2 } from "../../provider" +import { Provider } from "../../provider" const clientID = "b1a00492-073a-47ea-816f-4c329264a828" const issuer = "https://auth.x.ai/oauth2" @@ -173,7 +173,7 @@ export const XAIPlugin = define({ yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.make("xai")) return + if (evt.model.providerID !== Provider.ID.make("xai")) return evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id) }), ) diff --git a/packages/core/src/plugin/provider/zenmux.ts b/packages/core/src/plugin/provider/zenmux.ts index 44619a1ac5b4..13684f5bac58 100644 --- a/packages/core/src/plugin/provider/zenmux.ts +++ b/packages/core/src/plugin/provider/zenmux.ts @@ -1,14 +1,14 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" export const ZenmuxPlugin = define({ id: "opencode.provider.zenmux", effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue if (item.provider.settings?.baseURL !== "https://zenmux.ai/api/v1") continue evt.provider.update(item.provider.id, (provider) => { provider.headers = { diff --git a/packages/core/src/plugin/runtime.ts b/packages/core/src/plugin/runtime.ts index 4bb85f059dc8..cafa5d443e51 100644 --- a/packages/core/src/plugin/runtime.ts +++ b/packages/core/src/plugin/runtime.ts @@ -1,16 +1,16 @@ export * as PluginRuntime from "./runtime" import { Context, Effect, Layer } from "effect" -import { AgentV2 } from "../agent" +import { Agent } from "../agent" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { Job } from "../job" import { Location } from "../location" import { LocationServiceMap } from "../location-service-map" -import { SessionV2 } from "../session" +import { Session } from "../session" export interface Interface { readonly session: Pick< - SessionV2.Interface, + Session.Interface, "get" | "create" | "messages" | "prompt" | "generate" | "command" | "resume" | "interrupt" | "synthetic" > readonly job: Pick @@ -18,7 +18,7 @@ export interface Interface { readonly agent: { readonly list: ( ref: Location.Ref, - ) => Effect.Effect<{ readonly location: Location.Info; readonly data: AgentV2.Info[] }> + ) => Effect.Effect<{ readonly location: Location.Info; readonly data: Agent.Info[] }> } } } @@ -74,7 +74,7 @@ export const layerWithCell = (cell: Cell) => export const providerLayerWithCell = (cell: Cell) => Layer.effectDiscard( Effect.gen(function* () { - const sessions = yield* SessionV2.Service + const sessions = yield* Session.Service const jobs = yield* Job.Service const locations = yield* LocationServiceMap.Service const runtime: Interface = { @@ -85,7 +85,7 @@ export const providerLayerWithCell = (cell: Cell) => list: (ref) => Effect.gen(function* () { const location = yield* Location.Service - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service return { location: new Location.Info({ directory: location.directory, @@ -118,7 +118,7 @@ export const providerNodeWithCell = (cell: Cell) => makeGlobalNode({ name: "plugin-runtime-provider", layer: providerLayerWithCell(cell), - deps: [node, SessionV2.node, Job.node, LocationServiceMap.node], + deps: [node, Session.node, Job.node, LocationServiceMap.node], }) export const providerNode = providerNodeWithCell(defaultCell) diff --git a/packages/core/src/plugin/sdk.ts b/packages/core/src/plugin/sdk.ts index 99861d90d5a2..4a432c06c81f 100644 --- a/packages/core/src/plugin/sdk.ts +++ b/packages/core/src/plugin/sdk.ts @@ -1,12 +1,12 @@ export * as SdkPlugins from "./sdk" -import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Plugin } from "@opencode-ai/plugin/effect/plugin" import { Context, Effect, Layer } from "effect" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" -import { EventV2 } from "../event" -import type { PluginV2 } from "../plugin" +import { Bus } from "../bus" +import type { Versioned } from "../plugin" -export const Updated = EventV2.ephemeral({ type: "sdk.plugin.updated", schema: {} }) +export const Updated = Bus.ephemeral({ type: "sdk.plugin.updated", schema: {} }) /** * Holds the plugins an embedder (the `@opencode-ai/sdk-next` host) contributes, @@ -21,7 +21,7 @@ export const Updated = EventV2.ephemeral({ type: "sdk.plugin.updated", schema: { */ export interface Interface { readonly register: (plugin: Plugin) => Effect.Effect - readonly all: () => readonly PluginV2.Versioned[] + readonly all: () => readonly Versioned[] } export class Service extends Context.Service()("@opencode/SdkPlugins") {} @@ -29,17 +29,17 @@ export class Service extends Context.Service()("@opencode/Sd export const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service - const plugins = new Map() + const bus = yield* Bus.Service + const plugins = new Map() let revision = 0 return Service.of({ register: (plugin) => Effect.sync(() => { plugins.set(plugin.id, { ...plugin, version: String(++revision) }) - }).pipe(Effect.andThen(events.publish(Updated, {})), Effect.asVoid), + }).pipe(Effect.andThen(bus.publish(Updated, {})), Effect.asVoid), all: () => [...plugins.values()], }) }), ) -export const node = makeGlobalNode({ service: Service, layer, deps: [EventV2.node] }) +export const node = makeGlobalNode({ service: Service, layer, deps: [Bus.node] }) diff --git a/packages/core/src/plugin/skill.ts b/packages/core/src/plugin/skill.ts index c0edc4a43120..e10ce3e848fe 100644 --- a/packages/core/src/plugin/skill.ts +++ b/packages/core/src/plugin/skill.ts @@ -2,10 +2,10 @@ export * as SkillPlugin from "./skill" -import { define, type Context } from "@opencode-ai/plugin/v2/effect/plugin" +import { define, type Context } from "@opencode-ai/plugin/effect/plugin" import { Effect } from "effect" import { AbsolutePath } from "../schema" -import { SkillV2 } from "../skill" +import { Skill } from "../skill" import { Config } from "../config" import { Location } from "../location" import { FSUtil } from "@opencode-ai/util/fs-util" @@ -29,11 +29,11 @@ export const Plugin = define({ const reportContent = yield* reportContentWithDiagnostics(ctx.app) yield* ctx.skill.transform((draft) => { draft.source( - SkillV2.EmbeddedSource.make({ + Skill.EmbeddedSource.make({ type: "embedded", - skill: SkillV2.Info.make({ - id: SkillV2.ID.make("opencode"), - name: SkillV2.Name.make("OpenCode"), + skill: Skill.Info.make({ + id: Skill.ID.make("opencode"), + name: Skill.Name.make("OpenCode"), description: OpencodeDescription, location: AbsolutePath.make("/builtin/opencode.md"), content: OpencodeContent, @@ -41,11 +41,11 @@ export const Plugin = define({ }), ) draft.source( - SkillV2.EmbeddedSource.make({ + Skill.EmbeddedSource.make({ type: "embedded", - skill: SkillV2.Info.make({ - id: SkillV2.ID.make("report"), - name: SkillV2.Name.make("Report"), + skill: Skill.Info.make({ + id: Skill.ID.make("report"), + name: Skill.Name.make("Report"), description: REPORT_DESCRIPTION, slash: true, location: AbsolutePath.make("/builtin/report.md"), diff --git a/packages/core/src/plugin/skill/opencode.md b/packages/core/src/plugin/skill/opencode.md index 913082d3f1cf..7566a7e588a7 100644 --- a/packages/core/src/plugin/skill/opencode.md +++ b/packages/core/src/plugin/skill/opencode.md @@ -4,11 +4,11 @@ Use this guide as the starting point for work involving OpenCode itself. It covers the core concepts needed to configure and customize OpenCode, extend it with plugins, and build integrations with the OpenCode SDK, clients, and API. -Full documentation is available at . This overview is +Full documentation is available at . This overview is only an index of core concepts. Before answering a question about a topic below, fetch the URL named in that section and use the full page as the source of truth. Follow links from that page when the question needs more detail. Fetch - first when you need to discover the relevant + first when you need to discover the relevant documentation page. ## Version policy @@ -16,10 +16,10 @@ documentation page. Always answer for OpenCode V2 unless the user explicitly asks about V1, legacy OpenCode, or migrating from V1. -Use only documentation as the source of truth for V2. +Use only documentation as the source of truth for V2. Do not use , which documents V1, and do not use general web search to resolve a V2 documentation question when the V2 docs or -their `llms.txt` index cover it. The schema served from +linked pages cover it. The schema served from may describe V1 even though V2 configuration files include that URL for editor integration. Never use it to infer V2 field names or shapes. If V2 documentation is missing or contradictory, state the @@ -29,7 +29,7 @@ V1 documentation and syntax may be consulted only when the user explicitly asks about V1 or when needed as migration input. Outputs and recommendations must still use V2 unless the user specifically requests a V1 result. -## [Configuration](https://v2.opencode.ai/docs/config) +## [Configuration](https://opencode.ai/v2/docs/config) OpenCode configuration uses JSON or JSONC. Include the published schema so the user's editor can validate fields and provide autocomplete: @@ -60,14 +60,14 @@ linked topic guide as the source of truth, and preserve unrelated settings when editing an existing file. Keep the published `$schema` URL in configuration examples, but do not fetch it to determine the V2 configuration shape. -See the [full configuration guide](https://v2.opencode.ai/docs/config) for +See the [full configuration guide](https://opencode.ai/v2/docs/config) for every field, examples, config locations, and links to dedicated feature guides. -## [V1 to V2 migration](https://v2.opencode.ai/docs/migrate-v1) +## [V1 to V2 migration](https://opencode.ai/v2/docs/migrate-v1) For any request to migrate OpenCode configuration, agents, commands, skills, plugins, integrations, or other behavior from V1 to V2, read the full -[migration guide](https://v2.opencode.ai/docs/migrate-v1) before acting. In +[migration guide](https://opencode.ai/v2/docs/migrate-v1) before acting. In the repository, its source is `packages/www/content/docs/(Get started)/migrate-v1.mdx`. V1 config files and `.opencode/` definitions are intended to remain compatible. @@ -76,18 +76,18 @@ V2 config uses more ergonomic shapes, but conversion is optional. When the user requests conversion, inspect the complete configuration, preserve behavior and unrelated settings, and apply only the relevant migrations from the guide. For plugin migrations, fetch and follow both the migration guide and the full -[plugins guide](https://v2.opencode.ai/docs/build/plugins). If non-API V1 +[plugins guide](https://opencode.ai/v2/docs/build/plugins). If non-API V1 functionality fails in V2, use the `report` skill to file it as a compatibility bug. -## [Plugins](https://v2.opencode.ai/docs/build/plugins) +## [Plugins](https://opencode.ai/v2/docs/build/plugins) For questions about creating, configuring, loading, publishing, or migrating -plugins, fetch the full [plugins guide](https://v2.opencode.ai/docs/build/plugins) +plugins, fetch the full [plugins guide](https://opencode.ai/v2/docs/build/plugins) before answering. This includes questions about the Effect plugin API, hooks, transforms, tools, plugin context capabilities, and package entrypoints. -## [Service](https://v2.opencode.ai/docs/troubleshooting#check-the-background-service) +## [Service](https://opencode.ai/v2/docs/troubleshooting#check-the-background-service) OpenCode uses a client-server architecture. Interfaces such as the TUI connect to a background OpenCode service, which owns sessions, configuration, plugins, @@ -106,7 +106,7 @@ Check its status after restarting: opencode2 service status ``` -## [API](https://v2.opencode.ai/docs/api) +## [API](https://opencode.ai/v2/docs/api) OpenCode exposes an HTTP API from its server. The API is described by an OpenAPI document available from the running server at `/openapi.json`. @@ -135,15 +135,15 @@ connected to an explicit server instead of its managed background service, use the same configured server and authentication context rather than constructing an unauthenticated request separately. -See the [full API reference](https://v2.opencode.ai/docs/api) for available +See the [full API reference](https://opencode.ai/v2/docs/api) for available endpoints, parameters, request bodies, and response schemas. The -raw [OpenAPI specification](https://v2.opencode.ai/openapi.json) is also +raw [OpenAPI specification](https://opencode.ai/v2/openapi.json) is also available for code generation and other tooling. -## [Client](https://v2.opencode.ai/docs/build/client) +## [Client](https://opencode.ai/v2/docs/build/client) For questions about connecting an application to OpenCode over the network, -fetch the full [client guide](https://v2.opencode.ai/docs/build/client) before +fetch the full [client guide](https://opencode.ai/v2/docs/build/client) before answering. `@opencode-ai/client` is the generated TypeScript client for the OpenCode HTTP @@ -154,7 +154,7 @@ exposes typed Effects, Streams, and decoded OpenCode schema values. Its `Service` API can discover, start, stop, and authenticate with the local background service from a Node application. -## [Troubleshooting](https://v2.opencode.ai/docs/troubleshooting) +## [Troubleshooting](https://opencode.ai/v2/docs/troubleshooting) OpenCode runs a client and a background server. Start by determining whether a problem belongs to the client, the shared server, or one project. @@ -174,6 +174,6 @@ problem belongs to the client, the shared server, or one project. - Redact API keys, authorization headers, prompts, file contents, and other sensitive data before sharing diagnostics. -See the [full troubleshooting guide](https://v2.opencode.ai/docs/troubleshooting) +See the [full troubleshooting guide](https://opencode.ai/v2/docs/troubleshooting) for service lifecycle commands, API inspection, log locations, explicit server connections, issue-reporting details, and local development paths. diff --git a/packages/core/src/plugin/supervisor.ts b/packages/core/src/plugin/supervisor.ts index c6b977d5a760..2b14c42db196 100644 --- a/packages/core/src/plugin/supervisor.ts +++ b/packages/core/src/plugin/supervisor.ts @@ -1,18 +1,17 @@ export * as PluginSupervisor from "./supervisor" -import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin" import { Event } from "@opencode-ai/schema/config" import { Context, Deferred, Effect, Layer, Option, Schema, Semaphore, Stream } from "effect" import path from "path" import { fileURLToPath, pathToFileURL } from "url" -import { AgentV2 } from "../agent" +import { Agent } from "../agent" import { Catalog } from "../catalog" -import { CommandV2 } from "../command" +import { Command } from "../command" import { Config } from "../config" import { ConfigPlugin } from "../config/plugin" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { httpClient } from "@opencode-ai/util/effect/app-node-platform" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { FileMutation } from "../file-mutation" import { FileSystem } from "../filesystem" import { Form } from "../form" @@ -25,16 +24,16 @@ import { Location } from "../location" import { LocationMutation } from "../location-mutation" import { ModelsDev } from "../models-dev" import { Npm } from "@opencode-ai/util/npm" -import { PermissionV2 } from "../permission" -import { PluginV2 } from "../plugin" +import { Permission } from "../permission" +import { Plugin } from "../plugin" import { PluginPromise } from "../plugin/promise" import { Reference } from "../reference" import { Ripgrep } from "../ripgrep" import { SessionInstructions } from "../session/instructions" import { Shell } from "../shell" -import { SkillV2 } from "../skill" +import { Skill } from "../skill" import { ReadToolFileSystem } from "../tool/read-filesystem" -import { ToolRegistry } from "../tool/registry" +import { Tool } from "../tool" import { WebSearch } from "../websearch" import { WellKnown } from "../wellknown" import { PluginInternal } from "./internal" @@ -46,7 +45,9 @@ const PluginModule = Schema.Struct({ default: Schema.Union([ Schema.Struct({ id: Schema.String, - effect: Schema.declare((input): input is Plugin["effect"] => typeof input === "function"), + effect: Schema.declare( + (input): input is import("@opencode-ai/plugin/effect/plugin").Plugin["effect"] => typeof input === "function", + ), }), Schema.Struct({ id: Schema.String, @@ -113,15 +114,15 @@ const scan = Effect.fn("PluginSupervisor.scan")(function* (entries: readonly Con }) const resolve = Effect.fn("PluginSupervisor.resolve")(function* ( - pre: readonly PluginV2.Versioned[], - post: readonly PluginV2.Versioned[], + pre: readonly Plugin.Versioned[], + post: readonly Plugin.Versioned[], operations: readonly Operation[], ) { const matches = (selector: string, target: string) => selector === "*" || (selector.endsWith(".*") ? target.startsWith(selector.slice(0, -1)) : selector === target) const definitions = [...pre, ...post] const enabled = new Set(definitions.map((plugin) => plugin.id)) - const packages = new Map() + const packages = new Map() const plugins = () => [...definitions, ...packages.values()] for (const operation of operations) { @@ -183,7 +184,7 @@ const load = Effect.fn("PluginSupervisor.load")(function* (operation: Extract plugin.effect({ ...host, options: operation.options }), - } satisfies PluginV2.Versioned + } satisfies Plugin.Versioned }) function discoverDirectory(fs: FSUtil.Interface, directory: string) { @@ -211,10 +212,10 @@ export class Service extends Context.Service()("@opencode/Pl const layer = Layer.effect( Service, Effect.gen(function* () { - const registry = yield* PluginV2.Service + const registry = yield* Plugin.Service const sdk = yield* SdkPlugins.Service const config = yield* Config.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const lock = Semaphore.makeUnsafe(1) const ready = yield* Deferred.make() let observed = 0 @@ -238,7 +239,7 @@ const layer = Layer.effect( }), ) }) - const updates = yield* events + const updates = yield* bus .subscribe([Event.Updated, SdkPlugins.Updated]) .pipe(Stream.toQueue({ capacity: 1, strategy: "sliding" })) const signals = yield* Stream.concat( @@ -276,13 +277,13 @@ export const node = makeLocationNode({ service: Service, layer: nodeLayer, deps: [ - PluginV2.node, + Plugin.node, SdkPlugins.node, - AgentV2.node, + Agent.node, Catalog.node, - CommandV2.node, + Command.node, Config.node, - EventV2.node, + Bus.node, FileMutation.node, FileSystem.node, FSUtil.node, @@ -295,7 +296,7 @@ export const node = makeLocationNode({ LocationMutation.node, ModelsDev.node, Npm.node, - PermissionV2.node, + Permission.node, PluginRuntime.node, Form.node, ReadToolFileSystem.node, @@ -303,8 +304,8 @@ export const node = makeLocationNode({ Ripgrep.node, SessionInstructions.node, Shell.node, - SkillV2.node, - ToolRegistry.toolsNode, + Skill.node, + Tool.node, WebSearch.node, WellKnown.node, ], diff --git a/packages/core/src/plugin/system-prompt.ts b/packages/core/src/plugin/system-prompt.ts index 9d5d55787177..6bb78c864b3d 100644 --- a/packages/core/src/plugin/system-prompt.ts +++ b/packages/core/src/plugin/system-prompt.ts @@ -1,6 +1,6 @@ export * as SystemPromptPlugin from "./system-prompt" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Effect } from "effect" import PROMPT_ANTHROPIC from "./system-prompt/anthropic.txt" @@ -33,14 +33,16 @@ function make(id: string, select: (modelID: string) => string | undefined) { effect: Effect.fn(`SystemPromptPlugin.${id}`)(function* (ctx) { yield* ctx.session.hook("context", (event) => Effect.gen(function* () { - if ((yield* ctx.agent.get(event.agent))?.system) return + if ((yield* ctx.agent.get({ agentID: event.agent })).data.system) return const system = event.system[0] if (!system) return - const model = yield* ctx.catalog.model.get(event.model.providerID, event.model.id) + const model = (yield* ctx.catalog.model.list()).data.find( + (model) => model.providerID === event.model.providerID && model.id === event.model.id, + ) const prompt = select(`${model?.modelID ?? event.model.id} ${model?.family ?? ""}`.toLowerCase()) if (!prompt) return event.system[0] = { ...system, text: prompt } - }), + }).pipe(Effect.catch(() => Effect.void)), ) }), }) diff --git a/packages/core/src/plugin/variant.ts b/packages/core/src/plugin/variant.ts index 55c9667f9e89..b7e9e567d2b9 100644 --- a/packages/core/src/plugin/variant.ts +++ b/packages/core/src/plugin/variant.ts @@ -1,9 +1,9 @@ export * as VariantPlugin from "./variant" import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ModelV2 } from "../model" -import { ProviderV2 } from "../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Model } from "../model" +import { Provider } from "../provider" export const Plugin = define({ id: "opencode.variant", @@ -32,13 +32,13 @@ export const Plugin = define({ export function generate( model: { readonly id: string; readonly modelID?: string; readonly package?: string }, provider?: { readonly package: string }, -): NonNullable { +): NonNullable { const packageName = model.package ?? provider?.package - if (!ProviderV2.isAISDK(packageName) || ProviderV2.packageName(packageName) !== "@ai-sdk/openai-compatible") return [] + if (!Provider.isAISDK(packageName) || Provider.packageName(packageName) !== "@ai-sdk/openai-compatible") return [] const ids = `${model.id} ${model.modelID ?? ""}`.toLowerCase() if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return [] return ["high", "max"].map((id) => ({ - id: ModelV2.VariantID.make(id), + id: Model.VariantID.make(id), settings: { reasoningEffort: id }, })) } diff --git a/packages/core/src/plugin/warming.ts b/packages/core/src/plugin/warming.ts index d7bb9d0fd84d..828773de7336 100644 --- a/packages/core/src/plugin/warming.ts +++ b/packages/core/src/plugin/warming.ts @@ -1,6 +1,6 @@ export * as WarmingPlugin from "./warming" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Clock, Duration, Effect, Scope } from "effect" import { Config } from "../config" import { SessionSchema } from "../session/schema" diff --git a/packages/core/src/plugin/websearch/exa.ts b/packages/core/src/plugin/websearch/exa.ts index ba7596040782..af99fd49c203 100644 --- a/packages/core/src/plugin/websearch/exa.ts +++ b/packages/core/src/plugin/websearch/exa.ts @@ -1,6 +1,6 @@ export * as WebSearchExa from "./exa" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Effect, Schema, Scope } from "effect" import { HttpClient } from "effect/unstable/http" import { WebSearchMcp } from "./mcp" diff --git a/packages/core/src/plugin/websearch/parallel.ts b/packages/core/src/plugin/websearch/parallel.ts index e55302d8af70..05379652de01 100644 --- a/packages/core/src/plugin/websearch/parallel.ts +++ b/packages/core/src/plugin/websearch/parallel.ts @@ -1,6 +1,6 @@ export * as WebSearchParallel from "./parallel" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Effect, Schema, Scope } from "effect" import { HttpClient } from "effect/unstable/http" import { App } from "../../app" diff --git a/packages/core/src/project.ts b/packages/core/src/project.ts index 30ea46540730..2fb0aabc18d8 100644 --- a/packages/core/src/project.ts +++ b/packages/core/src/project.ts @@ -1,4 +1,3 @@ -export * as ProjectV2 from "./project" export * as Project from "./project" import { Context, Effect, Layer, Schema } from "effect" @@ -71,7 +70,7 @@ export interface Interface { readonly commit: (input: { store: AbsolutePath; id: ID }) => Effect.Effect } -export class Service extends Context.Service()("@opencode/ProjectV2") {} +export class Service extends Context.Service()("@opencode/Project") {} function fromRow(row: typeof ProjectTable.$inferSelect): Info { const icon = diff --git a/packages/core/src/project/copy.ts b/packages/core/src/project/copy.ts index 8b09eb78864e..7a2b156df176 100644 --- a/packages/core/src/project/copy.ts +++ b/packages/core/src/project/copy.ts @@ -10,7 +10,7 @@ import { Project } from "../project" import { ProjectDirectories } from "./directories" import { makeGitWorktreeStrategy } from "./copy-strategies" import { Slug } from "../util/slug" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { Database } from "../database/database" import { Location } from "../location" import { Event } from "@opencode-ai/schema/project-directories" @@ -132,10 +132,10 @@ const layer = Layer.effect( const git = yield* Git.Service const directories = yield* ProjectDirectories.Service const db = (yield* Database.Service).db - const events = yield* EventV2.Service + const bus = yield* Bus.Service const changed = Effect.fnUntraced(function* (projectID: Project.ID, update: boolean) { - if (update) yield* events.publish(Event.Updated, { projectID }) + if (update) yield* bus.publish(Event.Updated, { projectID }) }) const canonical = Effect.fnUntraced(function* (input: AbsolutePath) { @@ -282,7 +282,7 @@ const layer = Layer.effect( export const node = makeLocationNode({ service: Service, layer: layer, - deps: [FSUtil.node, Git.node, ProjectDirectories.node, EventV2.node, Database.node], + deps: [FSUtil.node, Git.node, ProjectDirectories.node, Bus.node, Database.node], }) export const refreshNode = makeLocationNode({ diff --git a/packages/core/src/provider.ts b/packages/core/src/provider.ts index e378882d79e9..629fe4cb66e2 100644 --- a/packages/core/src/provider.ts +++ b/packages/core/src/provider.ts @@ -1,4 +1,4 @@ -export * as ProviderV2 from "./provider" +export * as Provider from "./provider" import { Effect, Schema } from "effect" import { Provider } from "@opencode-ai/schema/provider" @@ -25,7 +25,7 @@ type Json = Schema.Schema.Type const JsonRecord = Schema.Record(Schema.String, Schema.Json) const decodeJsonRecord = Schema.decodeUnknownSync(JsonRecord) -export class LoadError extends Schema.TaggedErrorClass()("ProviderV2.LoadError", { +export class LoadError extends Schema.TaggedErrorClass()("Provider.LoadError", { package: Schema.String, cause: Schema.Defect(), }) {} @@ -47,7 +47,7 @@ const builtins = new Map Promise>([ ["@opencode-ai/ai/providers/xai", () => import("@opencode-ai/ai/providers/xai")], ]) -export const loadPackage = Effect.fn("ProviderV2.loadPackage")(function* (specifier: string, npm?: Npm.Interface) { +export const loadPackage = Effect.fn("Provider.loadPackage")(function* (specifier: string, npm?: Npm.Interface) { const builtin = builtins.get(specifier) if (builtin) return yield* importPackage(specifier, specifier, builtin) const resolved = yield* Effect.sync(() => { @@ -133,7 +133,7 @@ export type Info = Provider.Info export type MutableInfo = DeepMutable -const importPackage = Effect.fn("ProviderV2.importPackage")(function* ( +const importPackage = Effect.fn("Provider.importPackage")(function* ( specifier: string, entrypoint: string, load = () => importModule(entrypoint), diff --git a/packages/core/src/pty.ts b/packages/core/src/pty.ts index 427444d91a29..ad00debe8c05 100644 --- a/packages/core/src/pty.ts +++ b/packages/core/src/pty.ts @@ -5,7 +5,7 @@ import type { Disp, Proc } from "#pty" import { Context, Effect, Layer, Schema, Types } from "effect" import { Pty } from "@opencode-ai/schema/pty" import { Config } from "./config" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { Location } from "./location" import { PtyID } from "./pty/schema" import { ShellSelect } from "./shell/select" @@ -47,7 +47,7 @@ export const UpdateInput = Pty.UpdateInput export type UpdateInput = Types.DeepMutable -export const Event = Pty.Event +export { Event } from "@opencode-ai/schema/pty" export type AttachInput = { // Absolute output cursor to replay from. -1 tails from the current end; omitted replays the full retained buffer. @@ -87,12 +87,12 @@ export interface Interface { readonly attach: (id: PtyID, input: AttachInput) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Pty") {} +export class Service extends Context.Service()("@opencode/Pty") {} export const layer = (options?: ShellSelect.Options) => Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const location = yield* Location.Service const config = yield* Config.Service const context = yield* Effect.context() @@ -146,7 +146,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( if (index !== -1) exitOrder.splice(index, 1) yield* Effect.logInfo("removing session", { id }) teardown(session) - yield* events.publish(Event.Deleted, { id: session.info.id }) + yield* bus.publish(Pty.Event.Deleted, { id: session.info.id }) }) const remove = Effect.fn("Pty.remove")(function* (id: PtyID) { @@ -229,7 +229,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( runFork( Effect.gen(function* () { yield* Effect.logInfo("session exited", { id, exitCode }) - yield* events.publish(Event.Exited, { id, exitCode }) + yield* bus.publish(Pty.Event.Exited, { id, exitCode }) while (exitOrder.length > EXITED_LIMIT) { const oldest = exitOrder[0] if (!oldest) break @@ -239,7 +239,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( ) }), ) - yield* events.publish(Event.Created, { info }) + yield* bus.publish(Pty.Event.Created, { info }) return info }) @@ -247,7 +247,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( const session = yield* requireSession(id) if (input.title) session.info.title = input.title if (input.size && session.info.status === "running") session.process.resize(input.size.cols, input.size.rows) - yield* events.publish(Event.Updated, { info: session.info }) + yield* bus.publish(Pty.Event.Updated, { info: session.info }) return session.info }) @@ -314,7 +314,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( ) export function configured(options?: ShellSelect.Options) { - return makeLocationNode({ service: Service, layer: layer(options), deps: [EventV2.node, Location.node, Config.node] }) + return makeLocationNode({ service: Service, layer: layer(options), deps: [Bus.node, Location.node, Config.node] }) } export const node = configured() diff --git a/packages/core/src/pty/ticket.ts b/packages/core/src/pty/ticket.ts index f434c367914e..033460818f4f 100644 --- a/packages/core/src/pty/ticket.ts +++ b/packages/core/src/pty/ticket.ts @@ -1,6 +1,6 @@ export * as PtyTicket from "./ticket" -import { WorkspaceV2 } from "../workspace" +import { Workspace } from "../workspace" import { PtyTicket } from "@opencode-ai/schema/pty-ticket" import { PtyID } from "./schema" import { Cache, Context, Duration, Effect, Layer } from "effect" @@ -14,7 +14,7 @@ export const ConnectToken = PtyTicket.ConnectToken export type Scope = { readonly ptyID: PtyID readonly directory?: string - readonly workspaceID?: WorkspaceV2.ID + readonly workspaceID?: Workspace.ID } export interface Interface { diff --git a/packages/core/src/question.ts b/packages/core/src/question.ts index 3961ad5b1fb5..7164d1e4bf05 100644 --- a/packages/core/src/question.ts +++ b/packages/core/src/question.ts @@ -1,9 +1,9 @@ -export * as QuestionV2 from "./question" +export * as Question from "./question" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Context, Deferred, Effect, Layer, Schema } from "effect" import { Question } from "@opencode-ai/schema/question" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { SessionSchema } from "./session/schema" export const ID = Question.ID @@ -30,15 +30,15 @@ export type Answer = typeof Answer.Type export const Reply = Question.Reply export type Reply = typeof Reply.Type -export const Event = Question.Event +export { Event } from "@opencode-ai/schema/question" -export class RejectedError extends Schema.TaggedErrorClass()("QuestionV2.RejectedError", {}) { +export class RejectedError extends Schema.TaggedErrorClass()("Question.RejectedError", {}) { override get message() { return "The user dismissed this question" } } -export class NotFoundError extends Schema.TaggedErrorClass()("QuestionV2.NotFoundError", { +export class NotFoundError extends Schema.TaggedErrorClass()("Question.NotFoundError", { requestID: ID, }) {} @@ -60,7 +60,7 @@ export interface Interface { readonly list: () => Effect.Effect> } -export class Service extends Context.Service()("@opencode/v2/Question") {} +export class Service extends Context.Service()("@opencode/Question") {} interface Pending { readonly request: Request @@ -75,7 +75,7 @@ interface Pending { const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const pending = new Map() yield* Effect.addFinalizer(() => @@ -90,14 +90,14 @@ const layer = Layer.effect( ), ) - const ask = Effect.fn("QuestionV2.ask")((input: AskInput) => + const ask = Effect.fn("Question.ask")((input: AskInput) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () { const id = ID.ascending() const deferred = yield* Deferred.make, RejectedError>() const request: Request = { id, ...input } pending.set(id, { request, deferred }) - return yield* events.publish(Event.Asked, request).pipe( + return yield* bus.publish(Question.Event.Asked, request).pipe( Effect.andThen(restore(Deferred.await(deferred))), Effect.ensuring( Effect.sync(() => { @@ -109,12 +109,12 @@ const layer = Layer.effect( ), ) - const reply = Effect.fn("QuestionV2.reply")((input: ReplyInput) => + const reply = Effect.fn("Question.reply")((input: ReplyInput) => Effect.uninterruptible( Effect.gen(function* () { const existing = pending.get(input.requestID) if (!existing) return yield* new NotFoundError({ requestID: input.requestID }) - yield* events.publish(Event.Replied, { + yield* bus.publish(Question.Event.Replied, { sessionID: existing.request.sessionID, requestID: existing.request.id, answers: input.answers.map((answer) => [...answer]), @@ -125,12 +125,12 @@ const layer = Layer.effect( ), ) - const reject = Effect.fn("QuestionV2.reject")((requestID: ID) => + const reject = Effect.fn("Question.reject")((requestID: ID) => Effect.uninterruptible( Effect.gen(function* () { const existing = pending.get(requestID) if (!existing) return yield* new NotFoundError({ requestID }) - yield* events.publish(Event.Rejected, { + yield* bus.publish(Question.Event.Rejected, { sessionID: existing.request.sessionID, requestID: existing.request.id, }) @@ -140,7 +140,7 @@ const layer = Layer.effect( ), ) - const list = Effect.fn("QuestionV2.list")(function* () { + const list = Effect.fn("Question.list")(function* () { return Array.from(pending.values(), (item) => item.request) }) @@ -148,4 +148,4 @@ const layer = Layer.effect( }), ) -export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] }) +export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node] }) diff --git a/packages/core/src/reference.ts b/packages/core/src/reference.ts index 414af3ba32ce..d6905051b99e 100644 --- a/packages/core/src/reference.ts +++ b/packages/core/src/reference.ts @@ -4,7 +4,7 @@ import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Context, Effect, Layer, Scope, Types } from "effect" import { Reference } from "@opencode-ai/schema/reference" import { Global } from "@opencode-ai/util/global" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { Repository } from "./repository" import { RepositoryCache } from "./repository-cache" import { AbsolutePath } from "./schema" @@ -19,7 +19,7 @@ export type GitSource = Reference.GitSource export const Source = Reference.Source export type Source = Reference.Source -export const Event = Reference.Event +export { Event } from "@opencode-ai/schema/reference" export const Info = Reference.Info export type Info = Reference.Info @@ -38,13 +38,13 @@ export interface Interface extends State.Transformable { readonly list: () => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Reference") {} +export class Service extends Context.Service()("@opencode/Reference") {} const layer = Layer.effect( Service, Effect.gen(function* () { const global = yield* Global.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const cache = yield* RepositoryCache.Service const scope = yield* Scope.Scope const materialized = new Map() @@ -107,7 +107,7 @@ const layer = Layer.effect( Effect.forkIn(scope), ) } - yield* events.publish(Event.Updated, {}) + yield* bus.publish(Reference.Event.Updated, {}) }), }) @@ -124,5 +124,5 @@ const layer = Layer.effect( export const node = makeLocationNode({ service: Service, layer, - deps: [Global.node, EventV2.node, RepositoryCache.node], + deps: [Global.node, Bus.node, RepositoryCache.node], }) diff --git a/packages/core/src/reference/instructions.ts b/packages/core/src/reference/instructions.ts index 83632ba58506..34801bde10b1 100644 --- a/packages/core/src/reference/instructions.ts +++ b/packages/core/src/reference/instructions.ts @@ -57,7 +57,7 @@ export interface Interface { readonly load: () => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/ReferenceInstructions") {} +export class Service extends Context.Service()("@opencode/ReferenceInstructions") {} const layer = Layer.effect( Service, diff --git a/packages/core/src/ripgrep.ts b/packages/core/src/ripgrep.ts index 5673c9ab4ce8..a6aa9ed426cb 100644 --- a/packages/core/src/ripgrep.ts +++ b/packages/core/src/ripgrep.ts @@ -83,7 +83,7 @@ export interface Interface { readonly grep: (input: GrepInput) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Ripgrep") {} +export class Service extends Context.Service()("@opencode/Ripgrep") {} const failure = (message: string, cause?: unknown) => new Error({ message, cause }) diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 53a64866c2e5..c2cdbb3ffc87 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -1,23 +1,23 @@ -export * as SessionV2 from "./session" +export * as Session from "./session" export * from "./session/schema" import { Effect, Layer, Schema, Context, Stream, Scope } from "effect" import { ListAnchor } from "@opencode-ai/schema/session" import { and, asc, desc, eq, gt, isNull, like, lt, or, type SQL } from "drizzle-orm" -import { ProjectV2 } from "./project" -import { WorkspaceV2 } from "./workspace" -import { ModelV2 } from "./model" +import { Project } from "./project" +import { Workspace } from "./workspace" +import { Model } from "./model" import { Location } from "./location" import { SessionMessage } from "./session/message" import { Base64, FileAttachment, Prompt } from "@opencode-ai/schema/prompt" import { PromptInput } from "@opencode-ai/schema/prompt-input" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { Database } from "./database/database" import { SessionProjector } from "./session/projector" import { SessionMessageTable, SessionTable } from "./session/sql" import { SessionSchema } from "./session/schema" import { AbsolutePath, PositiveInt, RelativePath } from "./schema" -import { AgentV2 } from "./agent" +import { Agent } from "./agent" import { SessionV1 } from "./v1/session" import { Money } from "@opencode-ai/schema/money" import { App } from "./app" @@ -41,9 +41,10 @@ import { FSUtil } from "@opencode-ai/util/fs-util" import { Image } from "./image" import { Mime } from "./mime" import type { EventLog } from "@opencode-ai/schema/event-log" -import { SkillV2 } from "./skill" +import { Event } from "@opencode-ai/schema/event" +import { Skill } from "./skill" import { Job } from "./job" -import { CommandV2 } from "./command" +import { Command } from "./command" import { Shell } from "./shell" import { Global } from "@opencode-ai/util/global" import { Shell as ShellSchema } from "@opencode-ai/schema/shell" @@ -65,7 +66,7 @@ export type RevertState = Session.Revert export { ListAnchor } const ListInputBase = { - workspaceID: WorkspaceV2.ID.pipe(Schema.optional), + workspaceID: Workspace.ID.pipe(Schema.optional), search: Schema.String.pipe(Schema.optional), limit: PositiveInt.pipe(Schema.optional), order: Schema.Literals(["asc", "desc"]).pipe(Schema.optional), @@ -80,7 +81,7 @@ const ListDirectoryInput = Schema.Struct({ const ListProjectInput = Schema.Struct({ ...ListInputBase, - project: ProjectV2.ID, + project: Project.ID, subpath: RelativePath.pipe(Schema.optional), }) @@ -92,8 +93,8 @@ export type ListInput = typeof ListInput.Type type CreateBaseInput = { id?: SessionSchema.ID title?: string - agent?: AgentV2.ID - model?: ModelV2.Ref + agent?: Agent.ID + model?: Model.Ref } type CreateInput = CreateBaseInput & ({ location: Location.Ref; parentID?: never } | { parentID: SessionSchema.ID; location?: never }) @@ -143,7 +144,7 @@ export class BusyError extends Schema.TaggedErrorClass()("Session.Bus sessionID: SessionSchema.ID, }) {} export class SkillNotFoundError extends Schema.TaggedErrorClass()("Session.SkillNotFoundError", { - skill: SkillV2.ID, + skill: Skill.ID, }) {} export class DestinationNotFoundError extends Schema.TaggedErrorClass()( @@ -170,8 +171,8 @@ export type Error = | SkillNotFoundError | DestinationNotFoundError | DestinationNotDirectoryError - | CommandV2.NotFoundError - | CommandV2.EvaluationError + | Command.NotFoundError + | Command.EvaluationError | MessageNotFoundError | SessionGenerate.Error @@ -206,11 +207,11 @@ export interface Interface { */ readonly pending: (sessionID: SessionSchema.ID) => Effect.Effect /** - * Durable, ordered session log read. Replays durable session events after + * Durable, ordered session log read. Replays durable session bus after * the exclusive `after` cursor, emits a `Synced` marker at the captured * replay watermark, then continues live when `follow` is set. * The marker's seq may exceed the last emitted event because other durable - * events share the aggregate's sequence space. + * bus share the aggregate's sequence space. */ readonly log: (input: { sessionID: SessionSchema.ID @@ -219,11 +220,11 @@ export interface Interface { }) => Stream.Stream readonly switchAgent: (input: { sessionID: SessionSchema.ID - agent: AgentV2.ID + agent: Agent.ID }) => Effect.Effect readonly switchModel: (input: { sessionID: SessionSchema.ID - model: ModelV2.Ref + model: Model.Ref }) => Effect.Effect readonly rename: (input: { sessionID: SessionSchema.ID; title: string }) => Effect.Effect readonly move: (input: { @@ -251,25 +252,25 @@ export interface Interface { sessionID: SessionSchema.ID command: string arguments?: string - agent?: AgentV2.ID - model?: ModelV2.Ref + agent?: Agent.ID + model?: Model.Ref files?: PromptInput.Prompt["files"] agents?: PromptInput.Prompt["agents"] delivery?: SessionPending.Delivery resume?: boolean }) => Effect.Effect< SessionPending.User, - NotFoundError | PromptConflictError | AttachmentError | CommandV2.NotFoundError | CommandV2.EvaluationError + NotFoundError | PromptConflictError | AttachmentError | Command.NotFoundError | Command.EvaluationError > readonly shell: (input: { - id?: EventV2.ID + id?: Event.ID sessionID: SessionSchema.ID command: string }) => Effect.Effect readonly skill: (input: { id?: SessionMessage.ID sessionID: SessionSchema.ID - skill: SkillV2.ID + skill: Skill.ID resume?: boolean }) => Effect.Effect readonly compact: ( @@ -300,7 +301,7 @@ export interface Interface { } } -export class Service extends Context.Service()("@opencode/v2/Session") {} +export class Service extends Context.Service()("@opencode/Session") {} const layer = Layer.effect( Service, @@ -308,8 +309,8 @@ const layer = Layer.effect( const app = yield* App.Metadata const database = yield* Database.Service const db = database.db - const events = yield* EventV2.Service - const projects = yield* ProjectV2.Service + const bus = yield* Bus.Service + const projects = yield* Project.Service const global = yield* Global.Service const execution = yield* SessionExecution.Service const store = yield* SessionStore.Service @@ -333,7 +334,7 @@ const layer = Layer.effect( ) const result = Service.of({ - create: Effect.fn("V2Session.create")(function* (input) { + create: Effect.fn("Session.create")(function* (input) { const sessionID = input.id ?? SessionSchema.ID.create() const recorded = yield* store.get(sessionID) if (recorded) return recorded @@ -341,7 +342,7 @@ const layer = Layer.effect( if (input.parentID && parent === undefined) return yield* new NotFoundError({ sessionID: input.parentID }) const location = parent?.location ?? input.location if (location === undefined) - return yield* Effect.die(new Error("V2Session.create requires either location or an existing parentID")) + return yield* Effect.die(new Error("Session.create requires either location or an existing parentID")) const project = yield* projects.resolve(location.directory) yield* db .insert(ProjectTable) @@ -358,12 +359,12 @@ const layer = Layer.effect( parentID: input.parentID, directory: location.directory, path: path.relative(project.directory, location.directory).replaceAll("\\", "/"), - workspaceID: location.workspaceID ? WorkspaceV2.ID.make(location.workspaceID) : undefined, + workspaceID: location.workspaceID ? Workspace.ID.make(location.workspaceID) : undefined, title: input.title ?? `New session - ${new Date(now).toISOString()}`, agent: input.agent, model: input.model ? { - id: ModelV2.ID.make(input.model.id), + id: Model.ID.make(input.model.id), providerID: input.model.providerID, variant: input.model.variant, } @@ -372,7 +373,7 @@ const layer = Layer.effect( tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, time: { created: now, updated: now }, }) - const projected = yield* events.publish(SessionV1.Event.Created, { sessionID, info }, { location }).pipe( + const projected = yield* bus.publish(SessionV1.Event.Created, { sessionID, info }, { location }).pipe( Effect.as({ type: "created" } as const), Effect.catchDefect((defect) => { if (!(defect instanceof SessionProjector.SessionAlreadyProjected)) { @@ -392,7 +393,7 @@ const layer = Layer.effect( // TODO: Restore recorded sessions onto replacement synchronized workspaces in a future API slice. return yield* result.get(sessionID).pipe(Effect.orDie) }), - fork: Effect.fn("V2Session.fork")(function* (input) { + fork: Effect.fn("Session.fork")(function* (input) { const parent = yield* result.get(input.sessionID) const boundary = input.messageID ? yield* db @@ -407,8 +408,8 @@ const layer = Layer.effect( if (input.messageID && !boundary) return yield* new MessageNotFoundError({ sessionID: input.sessionID, messageID: input.messageID }) const sessionID = SessionSchema.ID.create() - const parentSeq = boundary ? boundary.seq - 1 : yield* EventV2.latestSequence(db, parent.id) - yield* events.publish(SessionEvent.Forked, { + const parentSeq = boundary ? boundary.seq - 1 : yield* Bus.latestSequence(db, parent.id) + yield* bus.publish(SessionEvent.Forked, { sessionID, parentID: parent.id, parentSeq, @@ -416,21 +417,21 @@ const layer = Layer.effect( }) return yield* result.get(sessionID).pipe(Effect.orDie) }), - get: Effect.fn("V2Session.get")(function* (sessionID) { + get: Effect.fn("Session.get")(function* (sessionID) { const session = yield* store.get(sessionID) if (!session) return yield* new NotFoundError({ sessionID }) return session }), - remove: Effect.fn("V2Session.remove")(function* (sessionID) { + remove: Effect.fn("Session.remove")(function* (sessionID) { yield* result.get(sessionID) yield* execution.interrupt(sessionID) yield* execution.awaitIdle(sessionID) const children = yield* result.list({ parentID: sessionID }) yield* Effect.forEach(children.data, (child) => result.remove(child.id), { concurrency: 1, discard: true }) - yield* events.publish(SessionEvent.Deleted, { sessionID }) - yield* events.remove(sessionID) + yield* bus.publish(SessionEvent.Deleted, { sessionID }) + yield* bus.remove(sessionID) }), - list: Effect.fn("V2Session.list")(function* (input = {}) { + list: Effect.fn("Session.list")(function* (input = {}) { const direction = input.anchor?.direction ?? "next" const requestedOrder = input.order ?? "desc" const order = direction === "previous" ? (requestedOrder === "asc" ? "desc" : "asc") : requestedOrder @@ -470,7 +471,7 @@ const layer = Layer.effect( ) return { data: (direction === "previous" ? rows.toReversed() : rows).map((row) => fromRow(row)) } }), - messages: Effect.fn("V2Session.messages")(function* (input) { + messages: Effect.fn("Session.messages")(function* (input) { yield* result.get(input.sessionID) const direction = input.cursor?.direction ?? "next" const requestedOrder = input.order ?? "desc" @@ -504,15 +505,15 @@ const layer = Layer.effect( ) return yield* Effect.forEach(direction === "previous" ? rows.toReversed() : rows, decode) }), - message: Effect.fn("V2Session.message")(function* (input) { + message: Effect.fn("Session.message")(function* (input) { const stored = yield* store.message(input.messageID) return stored?.sessionID === input.sessionID ? stored.message : undefined }), - context: Effect.fn("V2Session.context")(function* (sessionID) { + context: Effect.fn("Session.context")(function* (sessionID) { yield* result.get(sessionID) return yield* store.context(sessionID) }), - pending: Effect.fn("V2Session.pending")(function* (sessionID) { + pending: Effect.fn("Session.pending")(function* (sessionID) { yield* result.get(sessionID) return yield* SessionPending.list(db, sessionID) }), @@ -520,21 +521,21 @@ const layer = Layer.effect( Stream.unwrap( result .get(input.sessionID) - .pipe(Effect.as(events.log({ aggregateID: input.sessionID, after: input.after, follow: input.follow }))), + .pipe(Effect.as(bus.log({ aggregateID: input.sessionID, after: input.after, follow: input.follow }))), ).pipe( Stream.filter( (item): item is SessionEvent.DurableEvent | EventLog.Synced => - EventV2.isSynced(item) || isDurableSessionEvent(item), + Bus.isSynced(item) || isDurableSessionEvent(item), ), ), - prompt: Effect.fn("V2Session.prompt")((input) => + prompt: Effect.fn("Session.prompt")((input) => Effect.uninterruptible( Effect.gen(function* () { const session = yield* result.get(input.sessionID) // A staged revert must be committed before admitting new input so the prompt // continues from the reverted boundary rather than stale post-boundary history. if (session.revert) - yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events)) + yield* SessionRevert.commit(session).pipe(Effect.provideService(Bus.Service, bus)) // Resolved lazily so prompt admission only boots location services when an // image attachment actually needs the resizer. const image = Image.Service.pipe(Effect.provide(locations.get(session.location))) @@ -548,7 +549,7 @@ const layer = Layer.effect( data: { ...prompt, metadata: input.metadata }, delivery: input.delivery ?? "steer", }) - const admitted = yield* SessionPending.admit(db, events, { + const admitted = yield* SessionPending.admit(db, bus, { id: messageID, sessionID: input.sessionID, input: admittedInput, @@ -572,17 +573,17 @@ const layer = Layer.effect( }), ), ), - generate: Effect.fn("V2Session.generate")(function* (input) { + generate: Effect.fn("Session.generate")(function* (input) { const session = yield* result.get(input.sessionID) const generate = yield* SessionGenerate.Service.pipe(Effect.provide(locations.get(session.location))) return yield* generate.generate(input) }), - command: Effect.fn("V2Session.command")(function* (input) { + command: Effect.fn("Session.command")(function* (input) { const session = yield* result.get(input.sessionID) - const commands = yield* CommandV2.Service.pipe(Effect.provide(locations.get(session.location))) + const commands = yield* Command.Service.pipe(Effect.provide(locations.get(session.location))) const command = yield* commands.get(input.command) if (!command) - return yield* new CommandV2.NotFoundError({ + return yield* new Command.NotFoundError({ command: input.command, message: `Command not found: ${input.command}`, }) @@ -592,12 +593,12 @@ const layer = Layer.effect( const agent = command.agent ?? input.agent const commandAgent = yield* Effect.gen(function* () { if (!command.agent) return undefined - const agents = yield* AgentV2.Service.pipe(Effect.provide(locations.get(session.location))) - return yield* agents.get(AgentV2.ID.make(command.agent)) + const agents = yield* Agent.Service.pipe(Effect.provide(locations.get(session.location))) + return yield* agents.get(Agent.ID.make(command.agent)) }) const model = command.model ?? commandAgent?.model ?? input.model - if (agent !== undefined && session.agent !== AgentV2.ID.make(agent)) - yield* result.switchAgent({ sessionID: input.sessionID, agent: AgentV2.ID.make(agent) }) + if (agent !== undefined && session.agent !== Agent.ID.make(agent)) + yield* result.switchAgent({ sessionID: input.sessionID, agent: Agent.ID.make(agent) }) if (model !== undefined) yield* result.switchModel({ sessionID: input.sessionID, model }) return yield* result.prompt({ @@ -610,7 +611,7 @@ const layer = Layer.effect( resume: input.resume, }) }), - shell: Effect.fn("V2Session.shell")(function* (input) { + shell: Effect.fn("Session.shell")(function* (input) { const session = yield* result.get(input.sessionID) yield* shellLocks.withLock(input.sessionID)( Effect.gen(function* () { @@ -620,7 +621,7 @@ const layer = Layer.effect( const shell = yield* Shell.Service return yield* shell.create({ command: input.command, cwd: session.location.directory, timeout: 0 }) }).pipe(Effect.provide(locations.get(session.location))) - yield* events.publish( + yield* bus.publish( SessionEvent.Shell.Started, { sessionID: input.sessionID, @@ -643,7 +644,7 @@ const layer = Layer.effect( : missingShellOutput() return { shell: terminal.info, output } }).pipe(Effect.provide(locations.get(session.location))) - yield* events.publish(SessionEvent.Shell.Ended, { + yield* bus.publish(SessionEvent.Shell.Ended, { sessionID: input.sessionID, shell: completed.shell, output: completed.output, @@ -658,12 +659,12 @@ const layer = Layer.effect( ), ) }), - skill: Effect.fn("V2Session.skill")(function* (input) { + skill: Effect.fn("Session.skill")(function* (input) { const session = yield* result.get(input.sessionID) - const skills = yield* SkillV2.Service.pipe(Effect.provide(locations.get(session.location))) + const skills = yield* Skill.Service.pipe(Effect.provide(locations.get(session.location))) const skill = (yield* skills.list()).find((item) => item.id === input.skill) if (!skill) return yield* new SkillNotFoundError({ skill: input.skill }) - yield* events.publish( + yield* bus.publish( SessionEvent.Skill.Activated, { sessionID: input.sessionID, @@ -671,21 +672,21 @@ const layer = Layer.effect( name: skill.name, text: skill.content, }, - { id: input.id ? EventV2.ID.make(input.id.replace(/^msg_/, "evt_")) : undefined }, + { id: input.id ? Event.ID.make(input.id.replace(/^msg_/, "evt_")) : undefined }, ) if (input.resume !== false) yield* execution .resume(input.sessionID) .pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid) }), - switchAgent: Effect.fn("V2Session.switchAgent")(function* (input) { + switchAgent: Effect.fn("Session.switchAgent")(function* (input) { yield* result.get(input.sessionID) - yield* events.publish(SessionEvent.AgentSelected, { + yield* bus.publish(SessionEvent.AgentSelected, { sessionID: input.sessionID, agent: input.agent, }) }), - switchModel: Effect.fn("V2Session.switchModel")(function* (input) { + switchModel: Effect.fn("Session.switchModel")(function* (input) { const session = yield* result.get(input.sessionID) if ( session.model?.providerID === input.model.providerID && @@ -693,19 +694,19 @@ const layer = Layer.effect( (session.model.variant ?? "default") === (input.model.variant ?? "default") ) return - yield* events.publish(SessionEvent.ModelSelected, { + yield* bus.publish(SessionEvent.ModelSelected, { sessionID: input.sessionID, model: input.model, }) }), - rename: Effect.fn("V2Session.rename")(function* (input) { + rename: Effect.fn("Session.rename")(function* (input) { yield* result.get(input.sessionID) - yield* events.publish(SessionEvent.Renamed, { + yield* bus.publish(SessionEvent.Renamed, { sessionID: input.sessionID, title: input.title, }) }), - move: Effect.fn("V2Session.move")(function* (input) { + move: Effect.fn("Session.move")(function* (input) { const current = yield* result.get(input.sessionID) const value = input.directory.trim() const expanded = @@ -730,17 +731,17 @@ const layer = Layer.effect( yield* execution.interrupt(input.sessionID) yield* execution.awaitIdle(input.sessionID) } - yield* events.publish(SessionEvent.Moved, { + yield* bus.publish(SessionEvent.Moved, { sessionID: input.sessionID, location: Location.Ref.make({ directory, workspaceID: input.workspaceID }), projectID: project.id, subpath: RelativePath.make(path.relative(project.directory, directory).replaceAll("\\", "/")), }) }), - compact: Effect.fn("V2Session.compact")(function* (input) { + compact: Effect.fn("Session.compact")(function* (input) { yield* result.get(input.sessionID) const inputID = input.id ?? SessionMessage.ID.create() - const admitted = yield* SessionPending.admitCompaction(db, events, { + const admitted = yield* SessionPending.admitCompaction(db, bus, { id: inputID, sessionID: input.sessionID, }).pipe( @@ -753,12 +754,12 @@ const layer = Layer.effect( yield* execution.wake(input.sessionID) return admitted }), - wait: Effect.fn("V2Session.wait")(function* (sessionID) { + wait: Effect.fn("Session.wait")(function* (sessionID) { yield* result.get(sessionID) yield* execution.awaitIdle(sessionID) }), active: execution.active, - background: Effect.fn("V2Session.background")(function* (sessionID) { + background: Effect.fn("Session.background")(function* (sessionID) { yield* result.get(sessionID) const backgrounded = yield* jobs.backgroundAll({ sessionID }) if (backgrounded.length === 0) return @@ -776,11 +777,11 @@ const layer = Layer.effect( }) .pipe(Effect.catchTag("Session.SyntheticConflictError", Effect.die)) }), - resume: Effect.fn("V2Session.resume")(function* (sessionID) { + resume: Effect.fn("Session.resume")(function* (sessionID) { yield* result.get(sessionID) yield* execution.resume(sessionID) }), - synthetic: Effect.fn("V2Session.synthetic")((input) => + synthetic: Effect.fn("Session.synthetic")((input) => Effect.uninterruptible( Effect.gen(function* () { yield* result.get(input.sessionID) @@ -794,7 +795,7 @@ const layer = Layer.effect( }, delivery: input.delivery ?? "steer", }) - const admitted = yield* SessionPending.admit(db, events, { + const admitted = yield* SessionPending.admit(db, bus, { id: inputID, sessionID: input.sessionID, input: admittedInput, @@ -816,34 +817,34 @@ const layer = Layer.effect( }), ), ), - interrupt: Effect.fn("V2Session.interrupt")((sessionID) => + interrupt: Effect.fn("Session.interrupt")((sessionID) => Effect.uninterruptible(execution.interrupt(sessionID)), ), revert: { - stage: Effect.fn("V2Session.revert.stage")(function* (input) { + stage: Effect.fn("Session.revert.stage")(function* (input) { const session = yield* result.get(input.sessionID) if ((yield* execution.active).has(input.sessionID)) return yield* new BusyError({ sessionID: input.sessionID }) return yield* SessionRevert.stage({ session, messageID: input.messageID, files: input.files }).pipe( Effect.provideService(Database.Service, database), - Effect.provideService(EventV2.Service, events), + Effect.provideService(Bus.Service, bus), Effect.provide(locations.get(session.location)), ) }), - clear: Effect.fn("V2Session.revert.clear")(function* (sessionID) { + clear: Effect.fn("Session.revert.clear")(function* (sessionID) { const session = yield* result.get(sessionID) if ((yield* execution.active).has(sessionID)) return yield* new BusyError({ sessionID }) const revert = yield* SessionRevert.clear(session).pipe( - Effect.provideService(EventV2.Service, events), + Effect.provideService(Bus.Service, bus), Effect.provide(locations.get(session.location)), ) yield* execution.wake(sessionID) return revert }), - commit: Effect.fn("V2Session.revert.commit")(function* (sessionID) { + commit: Effect.fn("Session.revert.commit")(function* (sessionID) { const session = yield* result.get(sessionID) if ((yield* execution.active).has(sessionID)) return yield* new BusyError({ sessionID }) - return yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events)) + return yield* SessionRevert.commit(session).pipe(Effect.provideService(Bus.Service, bus)) }), }, }) @@ -872,7 +873,7 @@ function synthesizeTerminalShellInfo(started: ShellSchema.Info): ShellSchema.Inf } } -const resolvePrompt = Effect.fn("V2Session.resolvePrompt")(function* ( +const resolvePrompt = Effect.fn("Session.resolvePrompt")(function* ( input: PromptInput.Prompt, image: Effect.Effect, ) { @@ -885,7 +886,7 @@ const resolvePrompt = Effect.fn("V2Session.resolvePrompt")(function* ( const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024 -const materializeAttachment = Effect.fn("V2Session.materializeAttachment")(function* ( +const materializeAttachment = Effect.fn("Session.materializeAttachment")(function* ( fs: FSUtil.Interface, input: PromptInput.FileAttachment, image: Effect.Effect, @@ -933,7 +934,7 @@ const materializeAttachment = Effect.fn("V2Session.materializeAttachment")(funct }) }) -const normalizeImageAttachment = Effect.fn("V2Session.normalizeImageAttachment")(function* ( +const normalizeImageAttachment = Effect.fn("Session.normalizeImageAttachment")(function* ( input: PromptInput.FileAttachment, data: Base64, mime: string, @@ -950,7 +951,7 @@ const normalizeImageAttachment = Effect.fn("V2Session.normalizeImageAttachment") return { data: Base64.make(normalized.content), mime: normalized.mime } }) -const readFileAttachment = Effect.fn("V2Session.readFileAttachment")(function* (fs: FSUtil.Interface, uri: string) { +const readFileAttachment = Effect.fn("Session.readFileAttachment")(function* (fs: FSUtil.Interface, uri: string) { const url = yield* Effect.try({ try: () => new URL(uri), catch: () => new AttachmentError({ uri, message: `Invalid attachment URI: ${uri}` }), @@ -1033,8 +1034,8 @@ export const node = makeGlobalNode({ deps: [ Job.node, Database.node, - EventV2.node, - ProjectV2.node, + Bus.node, + Project.node, SessionExecution.node, SessionStore.node, LocationServiceMap.node, diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index a5b2c626b622..569675cb459b 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -4,7 +4,7 @@ import { LLM, LLMClient, LLMError, LLMEvent, Message, type LLMRequest, type Mode import { SessionError } from "@opencode-ai/schema/session-error" import { Context, Effect, Layer, Stream } from "effect" import { Config } from "../config" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { llmClient } from "../effect/app-node-platform" import { SessionEvent } from "./event" @@ -15,7 +15,7 @@ import { SessionRunnerModel } from "./runner/model" import { SessionSchema } from "./schema" import { toSessionError } from "./to-session-error" import { Token } from "../util/token" -import type { ModelV2 } from "../model" +import type { Info } from "../model" import { SessionUsage } from "./usage" const DEFAULT_BUFFER = 20_000 @@ -62,7 +62,7 @@ type Settings = { type Dependencies = { readonly app: App.Info - readonly events: EventV2.Interface + readonly bus: Bus.Interface readonly llm: { readonly stream: (request: LLMRequest) => Stream.Stream } @@ -74,7 +74,7 @@ export type AutoInput = { readonly session: SessionSchema.Info readonly messages: readonly SessionMessage.Info[] readonly model: Model - readonly cost: ModelV2.Info["cost"] + readonly cost: Info["cost"] } export type ManualInput = { @@ -86,7 +86,7 @@ export type ManualInput = { type Plan = { readonly session: SessionSchema.Info readonly model: Model - readonly cost: ModelV2.Info["cost"] + readonly cost: Info["cost"] readonly reason: SessionMessage.Compaction["reason"] readonly prompt: string readonly recent: string @@ -103,7 +103,7 @@ export interface Interface { readonly compactManual: (input: ManualInput) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/SessionCompaction") {} +export class Service extends Context.Service()("@opencode/SessionCompaction") {} const truncate = (value: string) => value.length <= TOOL_OUTPUT_MAX_CHARS ? value : `${value.slice(0, TOOL_OUTPUT_MAX_CHARS)}\n[truncated]` @@ -233,11 +233,11 @@ const make = (dependencies: Dependencies) => { readonly error: SessionError.Error readonly inputID?: SessionMessage.ID }) { - yield* dependencies.events.publish(SessionEvent.Compaction.Failed, input) + yield* dependencies.bus.publish(SessionEvent.Compaction.Failed, input) return { status: "failed" as const, error: input.error } }) const execute = Effect.fn("SessionCompaction.execute")(function* (plan: Plan) { - yield* dependencies.events.publish(SessionEvent.Compaction.Started, { + yield* dependencies.bus.publish(SessionEvent.Compaction.Started, { sessionID: plan.session.id, reason: plan.reason, recent: plan.recent, @@ -249,7 +249,7 @@ const make = (dependencies: Dependencies) => { let usage: SessionUsage.Recorded | undefined const recordUsage = Effect.suspend(() => usage - ? dependencies.events.publish(SessionEvent.UsageRecorded, { + ? dependencies.bus.publish(SessionEvent.UsageRecorded, { sessionID: plan.session.id, source: "compaction", ...usage, @@ -274,7 +274,7 @@ const make = (dependencies: Dependencies) => { } if (LLMEvent.is.textDelta(event)) { chunks.push(event.text) - return dependencies.events.publish(SessionEvent.Compaction.Delta, { + return dependencies.bus.publish(SessionEvent.Compaction.Delta, { sessionID: plan.session.id, text: event.text, }) @@ -316,7 +316,7 @@ const make = (dependencies: Dependencies) => { inputID: plan.inputID, }) } - yield* dependencies.events.publish(SessionEvent.Compaction.Ended, { + yield* dependencies.bus.publish(SessionEvent.Compaction.Ended, { sessionID: plan.session.id, reason: plan.reason, text: summary, @@ -395,17 +395,17 @@ const make = (dependencies: Dependencies) => { export const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const llm = yield* LLMClient.Service const config = yield* Config.Service const models = yield* SessionRunnerModel.Service const app = yield* App.Metadata - return make({ events, llm, models, config: settings(yield* config.entries()), app }) + return make({ bus, llm, models, config: settings(yield* config.entries()), app }) }), ) export const node = makeLocationNode({ service: Service, layer, - deps: [EventV2.node, llmClient, Config.node, SessionRunnerModel.node, App.node], + deps: [Bus.node, llmClient, Config.node, SessionRunnerModel.node, App.node], }) diff --git a/packages/core/src/session/context.ts b/packages/core/src/session/context.ts index c2c41e329984..d4936bd2c5b7 100644 --- a/packages/core/src/session/context.ts +++ b/packages/core/src/session/context.ts @@ -1,7 +1,7 @@ export * as SessionContext from "./context" import { Context, Effect, Layer } from "effect" -import { AgentV2 } from "../agent" +import { Agent } from "../agent" import { CodeModeInstructions } from "../codemode/instructions" import { Database } from "../database/database" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" @@ -13,7 +13,7 @@ import { McpInstructions } from "../mcp/instructions" import { PluginSupervisor } from "../plugin/supervisor" import { ReferenceInstructions } from "../reference/instructions" import { SkillInstructions } from "../skill/instructions" -import { ToolRegistry } from "../tool/registry" +import { Tool } from "../tool" import { AgentNotFoundError } from "./error" import { SessionHistory } from "./history" import { InstructionEntry } from "./instruction-entry" @@ -24,18 +24,18 @@ import { SessionStore } from "./store" export interface Selection { readonly session: SessionSchema.Info - readonly agent: AgentV2.Selection & { readonly info: AgentV2.Info } + readonly agent: Agent.Selection & { readonly info: Agent.Info } readonly instructions: Instructions.Instructions - readonly toolSet: ToolRegistry.ToolSet + readonly tools: Tool.Snapshot } export interface Loaded { readonly session: SessionSchema.Info - readonly agent: AgentV2.Selection & { readonly info: AgentV2.Info } + readonly agent: Agent.Selection & { readonly info: Agent.Info } readonly model: SessionRunnerModel.Resolved readonly initial: string readonly messages: ReadonlyArray - readonly toolSet: ToolRegistry.ToolSet + readonly tools: Tool.Snapshot } /** @@ -52,12 +52,12 @@ export interface Interface { } /** Location-scoped model-context loader for durable Session Steps. */ -export class Service extends Context.Service()("@opencode/v2/SessionContext") {} +export class Service extends Context.Service()("@opencode/SessionContext") {} const layer = Layer.effect( Service, Effect.gen(function* () { - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const builtins = yield* InstructionBuiltIns.Service const db = (yield* Database.Service).db const discovery = yield* InstructionDiscovery.Service @@ -69,7 +69,7 @@ const layer = Layer.effect( const referenceInstructions = yield* ReferenceInstructions.Service const skillInstructions = yield* SkillInstructions.Service const store = yield* SessionStore.Service - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const select = Effect.fn("SessionContext.select")(function* (sessionID: SessionSchema.ID) { const session = yield* store.get(sessionID) @@ -82,7 +82,7 @@ const layer = Layer.effect( if (!agent.info) return yield* new AgentNotFoundError({ sessionID: session.id, agent: session.agent ?? agent.id }) const loaded = yield* Effect.all( { - toolSet: registry.snapshot(agent.info.permissions), + tools: registry.snapshot(agent.info.permissions), builtins: builtins.load(sessionID), discovery: discovery.load(), skills: skillInstructions.load(agent), @@ -97,14 +97,14 @@ const layer = Layer.effect( agent: { ...agent, info: agent.info }, instructions: Instructions.combine([ loaded.builtins, - CodeModeInstructions.make(loaded.toolSet.codeModeCatalog), + CodeModeInstructions.make(loaded.tools.codeModeCatalog), loaded.discovery, loaded.skills, loaded.references, loaded.mcp, loaded.entries, ]), - toolSet: loaded.toolSet, + tools: loaded.tools, } }) @@ -117,7 +117,7 @@ const layer = Layer.effect( model, initial: history.initial, messages: history.entries.map((entry) => entry.message), - toolSet: selection.toolSet, + tools: selection.tools, } }) @@ -129,7 +129,7 @@ export const node = makeLocationNode({ service: Service, layer, deps: [ - AgentV2.node, + Agent.node, Database.node, InstructionBuiltIns.node, InstructionDiscovery.node, @@ -141,6 +141,6 @@ export const node = makeLocationNode({ SessionRunnerModel.node, SessionStore.node, SkillInstructions.node, - ToolRegistry.node, + Tool.node, ], }) diff --git a/packages/core/src/session/execution.ts b/packages/core/src/session/execution.ts index 9cef14725293..44a90a8492de 100644 --- a/packages/core/src/session/execution.ts +++ b/packages/core/src/session/execution.ts @@ -1,7 +1,7 @@ export * as SessionExecution from "./execution" import { Cause, Context, Effect, Exit, Layer } from "effect" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { LocationServiceMap } from "../location-service-map" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { SessionEvent } from "./event" @@ -26,7 +26,7 @@ export interface Interface { } /** Routes execution from a Session ID to the runner owned by that Session's Location. */ -export class Service extends Context.Service()("@opencode/v2/SessionExecution") {} +export class Service extends Context.Service()("@opencode/SessionExecution") {} type InterruptReason = "user" | "shutdown" | "superseded" @@ -44,7 +44,7 @@ export const layer = Layer.effect( Effect.gen(function* () { const store = yield* SessionStore.Service const locations = yield* LocationServiceMap.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const reportLifecycle = (sessionID: SessionSchema.ID, effect: Effect.Effect) => effect.pipe( Effect.tapCause((cause) => @@ -65,7 +65,7 @@ export const layer = Layer.effect( started: (sessionID) => reportLifecycle( sessionID, - events.publish(SessionEvent.Execution.Started, { sessionID }, clearSuspensionOnCommit(sessionID)), + bus.publish(SessionEvent.Execution.Started, { sessionID }, clearSuspensionOnCommit(sessionID)), ), drain: Effect.fnUntraced(function* (sessionID: SessionSchema.ID, force) { const session = yield* store.get(sessionID) @@ -86,14 +86,14 @@ export const layer = Layer.effect( Effect.gen(function* () { const outcome = terminal(exit, reason) if (outcome.type === "succeeded") { - yield* events.publish(SessionEvent.Execution.Succeeded, { sessionID }, clearSuspensionOnCommit(sessionID)) + yield* bus.publish(SessionEvent.Execution.Succeeded, { sessionID }, clearSuspensionOnCommit(sessionID)) return } if (outcome.type === "interrupted") { - yield* events.publish(SessionEvent.Execution.Interrupted, { sessionID, reason: outcome.reason }) + yield* bus.publish(SessionEvent.Execution.Interrupted, { sessionID, reason: outcome.reason }) return } - yield* events.publish( + yield* bus.publish( SessionEvent.Execution.Failed, { sessionID, @@ -118,7 +118,7 @@ export const layer = Layer.effect( export const node = makeGlobalNode({ service: Service, layer, - deps: [SessionStore.node, LocationServiceMap.node, EventV2.node], + deps: [SessionStore.node, LocationServiceMap.node, Bus.node], }) /** Low-level compatibility layer for callers that only need durable Session recording. */ diff --git a/packages/core/src/session/execution/restart.ts b/packages/core/src/session/execution/restart.ts index d7133bb7d25f..08d8bf401195 100644 --- a/packages/core/src/session/execution/restart.ts +++ b/packages/core/src/session/execution/restart.ts @@ -19,7 +19,7 @@ export interface Interface { * Restart continuity actions for the managed server. The service is inert until called: only the * managed server invokes it, so default, embedded, and stdio servers never suspend or auto-resume. */ -export class Service extends Context.Service()("@opencode/v2/SessionRestart") {} +export class Service extends Context.Service()("@opencode/SessionRestart") {} export const layer = Layer.effect( Service, diff --git a/packages/core/src/session/generate-node.ts b/packages/core/src/session/generate-node.ts index e1b81c22b914..9a5c1031fd85 100644 --- a/packages/core/src/session/generate-node.ts +++ b/packages/core/src/session/generate-node.ts @@ -34,8 +34,8 @@ export const layer = Layer.effect( const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(selection.session.id) ? selection.session.id.slice(4) : selection.session.id - const toolSet = selection.toolSet - const toolDefinitions = toolSet.definitions + const tools = selection.tools + const toolDefinitions = tools.definitions const toolsByName = new Map(toolDefinitions.map((tool) => [tool.name, tool])) const contextEvent = yield* hooks.trigger("session", "context", { sessionID: selection.session.id, diff --git a/packages/core/src/session/generate.ts b/packages/core/src/session/generate.ts index 381c3345185a..a8f38b10c827 100644 --- a/packages/core/src/session/generate.ts +++ b/packages/core/src/session/generate.ts @@ -18,4 +18,4 @@ export interface Interface { } /** Location-scoped transient generation from Session context. */ -export class Service extends Context.Service()("@opencode/v2/SessionGenerate") {} +export class Service extends Context.Service()("@opencode/SessionGenerate") {} diff --git a/packages/core/src/session/info.ts b/packages/core/src/session/info.ts index 2c507d005898..83d0297bd241 100644 --- a/packages/core/src/session/info.ts +++ b/packages/core/src/session/info.ts @@ -1,11 +1,11 @@ import { DateTime, Schema } from "effect" -import { AgentV2 } from "../agent" +import { Agent } from "../agent" import { Location } from "../location" -import { ModelV2 } from "../model" -import { ProjectV2 } from "../project" -import { ProviderV2 } from "../provider" +import { Model } from "../model" +import { Project } from "../project" +import { Provider } from "../provider" import { AbsolutePath, RelativePath } from "../schema" -import { WorkspaceV2 } from "../workspace" +import { Workspace } from "../workspace" import { SessionSchema } from "./schema" import { SessionTable } from "./sql" import { SessionMessage } from "./message" @@ -17,7 +17,7 @@ const decodeRevert = Schema.decodeUnknownSync(PersistedRevert) export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.Info { return SessionSchema.Info.make({ id: SessionSchema.ID.make(row.id), - projectID: ProjectV2.ID.make(row.project_id), + projectID: Project.ID.make(row.project_id), title: row.title, parentID: row.parent_id ? SessionSchema.ID.make(row.parent_id) : undefined, fork: row.fork_session_id @@ -26,12 +26,12 @@ export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.In messageID: row.fork_message_id ? SessionMessage.ID.make(row.fork_message_id) : undefined, } : undefined, - agent: row.agent ? AgentV2.ID.make(row.agent) : undefined, + agent: row.agent ? Agent.ID.make(row.agent) : undefined, model: row.model ? { - id: ModelV2.ID.make(row.model.id), - providerID: ProviderV2.ID.make(row.model.providerID), - variant: ModelV2.VariantID.make(row.model.variant ?? "default"), + id: Model.ID.make(row.model.id), + providerID: Provider.ID.make(row.model.providerID), + variant: Model.VariantID.make(row.model.variant ?? "default"), } : undefined, cost: Money.USD.make(row.cost), @@ -46,7 +46,7 @@ export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.In }, location: Location.Ref.make({ directory: AbsolutePath.make(row.directory), - workspaceID: row.workspace_id ? WorkspaceV2.ID.make(row.workspace_id) : undefined, + workspaceID: row.workspace_id ? Workspace.ID.make(row.workspace_id) : undefined, }), subpath: row.path ? RelativePath.make(row.path) : undefined, revert: row.revert ? decodeRevert(row.revert) : undefined, diff --git a/packages/core/src/session/instruction-entry.ts b/packages/core/src/session/instruction-entry.ts index 371a7337e764..5fca262da008 100644 --- a/packages/core/src/session/instruction-entry.ts +++ b/packages/core/src/session/instruction-entry.ts @@ -28,7 +28,7 @@ export interface Interface { readonly load: (sessionID: SessionSchema.ID) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/InstructionEntry") {} +export class Service extends Context.Service()("@opencode/InstructionEntry") {} const renderValue = (value: Schema.Json) => (typeof value === "string" ? value : JSON.stringify(value, null, 2)) diff --git a/packages/core/src/session/instruction-state.ts b/packages/core/src/session/instruction-state.ts index 1d6127b6deea..364dfba8a5e9 100644 --- a/packages/core/src/session/instruction-state.ts +++ b/packages/core/src/session/instruction-state.ts @@ -3,11 +3,12 @@ export * as InstructionState from "./instruction-state" import { and, asc, desc, eq, gt, inArray, lte, sql } from "drizzle-orm" import { DateTime, Effect, Option, Schema } from "effect" import type { Database } from "../database/database" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { EventTable } from "../event/sql" import { Instructions } from "../instructions/index" import { SessionEvent } from "./event" import { SessionMessage } from "./message" +import { Event } from "@opencode-ai/schema/event" import { SessionSchema } from "./schema" import { InstructionBlobTable, InstructionStateTable, SessionTable } from "./sql" @@ -39,11 +40,11 @@ export const observe = Effect.fn("InstructionState.observe")(function* ( export const commit = Effect.fn("InstructionState.commit")(function* ( db: DatabaseService, - events: EventV2.Interface, + bus: Bus.Interface, observation: Observation, ) { if (!observation.initial && Object.keys(observation.delta).length === 0) return - yield* events.publish( + yield* bus.publish( SessionEvent.InstructionsUpdated, { sessionID: observation.sessionID, delta: observation.delta }, { @@ -56,11 +57,11 @@ export const commit = Effect.fn("InstructionState.commit")(function* ( export const prepare = Effect.fn("InstructionState.prepare")(function* ( db: DatabaseService, - events: EventV2.Interface, + bus: Bus.Interface, instructions: Instructions.Instructions, sessionID: SessionSchema.ID, ) { - yield* commit(db, events, yield* observe(db, instructions, sessionID)) + yield* commit(db, bus, yield* observe(db, instructions, sessionID)) }) export const apply = Effect.fn("InstructionState.apply")(function* ( @@ -171,7 +172,7 @@ const assembleState = Effect.fnUntraced(function* ( result.push({ seq: update.row.seq, message: SessionMessage.System.make({ - id: SessionMessage.ID.fromEvent(EventV2.ID.make(update.row.id)), + id: SessionMessage.ID.fromEvent(Event.ID.make(update.row.id)), type: "system", text, time: { created: DateTime.makeUnsafe(update.row.created) }, @@ -310,16 +311,16 @@ function requireBlob(blobs: ReadonlyMap, hash: I return value } -const instructionEventType = EventV2.versionedType( +const instructionEventType = Bus.versionedType( SessionEvent.InstructionsUpdated.type, SessionEvent.InstructionsUpdated.durable.version, ) -const compactionEventType = EventV2.versionedType( +const compactionEventType = Bus.versionedType( SessionEvent.Compaction.Ended.type, SessionEvent.Compaction.Ended.durable.version, ) -const movedEventType = EventV2.versionedType(SessionEvent.Moved.type, SessionEvent.Moved.durable.version) -const revertedEventType = EventV2.versionedType( +const movedEventType = Bus.versionedType(SessionEvent.Moved.type, SessionEvent.Moved.durable.version) +const revertedEventType = Bus.versionedType( SessionEvent.RevertEvent.Committed.type, SessionEvent.RevertEvent.Committed.durable.version, ) diff --git a/packages/core/src/session/instructions.ts b/packages/core/src/session/instructions.ts index d3ad3fcbaf60..4688c79bb3f3 100644 --- a/packages/core/src/session/instructions.ts +++ b/packages/core/src/session/instructions.ts @@ -3,7 +3,7 @@ export * as SessionInstructions from "./instructions" import { relative } from "path" import { Context, DateTime, Effect, Layer, Option, Ref, Schema } from "effect" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "../location" import { SessionEvent } from "./event" @@ -23,12 +23,12 @@ export interface Interface { }) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/SessionInstructions") {} +export class Service extends Context.Service()("@opencode/SessionInstructions") {} const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const fs = yield* FSUtil.Service const store = yield* SessionStore.Service const location = yield* Location.Service @@ -67,12 +67,12 @@ const layer = Layer.effect( ) const readable = files.filter((file): file is { path: string; content: string } => file !== undefined) if (readable.length === 0) return - // Publish directly rather than through SessionV2.synthetic: a Location-scoped layer - // cannot depend on SessionV2 (it routes through LocationServiceMap, forming a type + // Publish directly rather than through Session.synthetic: a Location-scoped layer + // cannot depend on Session (it routes through LocationServiceMap, forming a type // cycle with this node). The durable publish is what makes the synthetic visible on // the next projected history reload. The dedup ledger lives on the synthetic message // metadata so it survives across Location layer restarts. - yield* events.publish(SessionEvent.Synthetic, { + yield* bus.publish(SessionEvent.Synthetic, { sessionID: input.sessionID, text: readable.map((file) => `Instructions from: ${file.path}\n${file.content}`).join("\n\n"), description: `Loaded ${readable.map((file) => describePath(root, file.path)).join(", ")}`, @@ -109,5 +109,5 @@ function describePath(root: string, path: string) { export const node = makeLocationNode({ name: "session-instructions", layer, - deps: [EventV2.node, FSUtil.node, Location.node, SessionStore.node], + deps: [Bus.node, FSUtil.node, Location.node, SessionStore.node], }) diff --git a/packages/core/src/session/model-request.ts b/packages/core/src/session/model-request.ts index b62fdc8f3f70..4cf3e4f8de79 100644 --- a/packages/core/src/session/model-request.ts +++ b/packages/core/src/session/model-request.ts @@ -1,16 +1,16 @@ export * as SessionModelRequest from "./model-request" -import { LLM, Message, SystemPart, type LLMRequest, type ToolContent } from "@opencode-ai/ai" +import { LLM, Message, SystemPart, type LLMRequest } from "@opencode-ai/ai" +import type { Content } from "@opencode-ai/schema/tool" import { SessionError } from "@opencode-ai/schema/session-error" import { Cause, Context, Effect, Layer, Result } from "effect" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { App } from "../app" -import { ModelV2 } from "../model" -import { PermissionV2 } from "../permission" +import { Model } from "../model" +import { Permission } from "../permission" import { PluginHooks } from "../plugin/hooks" -import { QuestionTool } from "../tool/question" -import { ToolOutputStore } from "../tool-output-store" -import { ToolRegistry } from "../tool/registry" +import { QuestionTool } from "../tool/plugin/question" +import { Tool } from "../tool" import { SessionContext } from "./context" import { SessionModelHeaders } from "./model-headers" import { MAX_STEPS_PROMPT } from "./runner/max-steps" @@ -18,16 +18,16 @@ import PROMPT_DEFAULT from "./runner/prompt/base.txt" import { toLLMMessages } from "./runner/to-llm-message" /** Failures a prepared execution can surface: infrastructure errors plus user declines resurfaced from the defect tunnel. */ -export type ExecuteError = ToolOutputStore.Error | PermissionV2.DeclinedError | QuestionTool.CancelledError +export type ExecuteError = Tool.Error | Permission.DeclinedError | QuestionTool.CancelledError // User declines dive under the leaves' blanket `mapError` as defects (the deliberate -// tunnel entered in PermissionV2.assert and the question tool), so a user's "no" can +// tunnel entered in Permission.assert and the question tool), so a user's "no" can // never become model-facing tool output. They resurface as typed failures exactly once, // here at the seam the runner executes through. -const declineDefect = (cause: Cause.Cause) => { +const declineDefect = (cause: Cause.Cause) => { const decline = cause.reasons.flatMap((reason) => Cause.isDieReason(reason) && - (reason.defect instanceof PermissionV2.DeclinedError || reason.defect instanceof QuestionTool.CancelledError) + (reason.defect instanceof Permission.DeclinedError || reason.defect instanceof QuestionTool.CancelledError) ? [reason.defect] : [], )[0] @@ -41,8 +41,8 @@ interface Prepared { * step-limit-violating calls fail individually through the same seam. */ readonly executeTool: ( - input: ToolRegistry.ExecuteInput, - ) => Effect.Effect + input: Parameters[0], + ) => Effect.Effect /** True when this request is the final Step; violating calls are rejected and no continuation follows. */ readonly stepLimitReached: boolean } @@ -59,7 +59,7 @@ const mimeToModality = (mime: string) => { if (mime === "application/pdf") return "pdf" } -const unsupportedMedia = (mime: string, name: string | undefined, capabilities: ModelV2.Capabilities) => { +const unsupportedMedia = (mime: string, name: string | undefined, capabilities: Model.Capabilities) => { const modality = mimeToModality(mime) if (!modality || capabilities.input.some((item) => item.startsWith(modality))) return return { @@ -68,7 +68,7 @@ const unsupportedMedia = (mime: string, name: string | undefined, capabilities: } } -export const unsupportedParts = (messages: LLMRequest["messages"], capabilities: ModelV2.Capabilities) => +export const unsupportedParts = (messages: LLMRequest["messages"], capabilities: Model.Capabilities) => messages.map((message) => Message.make({ ...message, @@ -81,7 +81,7 @@ export const unsupportedParts = (messages: LLMRequest["messages"], capabilities: ...part, result: { ...part.result, - value: part.result.value.map((item: ToolContent) => { + value: part.result.value.map((item: Content) => { if (item.type !== "file") return item return unsupportedMedia(item.mime, item.name, capabilities) ?? item }), @@ -102,7 +102,7 @@ export interface Interface { } /** Location-scoped outbound model-request preparation. */ -export class Service extends Context.Service()("@opencode/v2/SessionModelRequest") {} +export class Service extends Context.Service()("@opencode/SessionModelRequest") {} export const layer = Layer.effect( Service, @@ -119,14 +119,14 @@ export const layer = Layer.effect( const stepLimitReached = agent.info.steps !== undefined && input.step >= agent.info.steps // The final Step keeps definitions available to protocols with native "none", // preserving their prompt cache prefix. Calls are still rejected at execution. - const toolSet = input.context.toolSet + const tools = input.context.tools const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id const system = [agent.info.system ? agent.info.system : PROMPT_DEFAULT, input.context.initial] .filter((part) => part.length > 0) .map(SystemPart.make) const history = toLLMMessages(input.context.messages, resolved.ref, providerMetadataKey) const messages = stepLimitReached ? [...history, Message.assistant(MAX_STEPS_PROMPT)] : history - const toolDefinitions = toolSet.definitions + const toolDefinitions = tools.definitions const toolsByName = new Map(toolDefinitions.map((tool) => [tool.name, tool])) // Hooks may reshape available definitions but cannot advertise tools omitted by permissions or the Step limit. const contextEvent = yield* hooks.trigger("session", "context", { @@ -142,7 +142,7 @@ export const layer = Layer.effect( const hookedTools = Object.entries(contextEvent.tools).flatMap(([name, tool]) => { const registered = toolsByName.get(name) return registered - ? [Object.assign({}, registered, { description: tool.description, inputSchema: tool.input })] + ? [{ ...registered, description: tool.description, inputSchema: tool.input }] : [] }) const request = LLM.request({ @@ -158,16 +158,10 @@ export const layer = Layer.effect( }) const executeTool: Prepared["executeTool"] = (executeInput) => { if (stepLimitReached) - return Effect.succeed({ - status: "error", - error: { type: "tool.execution", message: "Tools are disabled after the maximum agent steps" }, - }) + return new Tool.Error({ message: "Tools are disabled after the maximum agent steps" }) if (toolsByName.has(executeInput.call.name) && !Object.hasOwn(contextEvent.tools, executeInput.call.name)) - return Effect.succeed({ - status: "error", - error: { type: "tool.unknown", message: `Tool is not available for this request: ${executeInput.call.name}` }, - }) - return toolSet + return new Tool.Error({ message: `Tool is not available for this request: ${executeInput.call.name}` }) + return tools .execute(executeInput) .pipe(Effect.catchCauseFilter(declineDefect, (decline) => Effect.fail(decline))) } diff --git a/packages/core/src/session/pending.ts b/packages/core/src/session/pending.ts index 6753903be9b2..47d74d0abfff 100644 --- a/packages/core/src/session/pending.ts +++ b/packages/core/src/session/pending.ts @@ -14,7 +14,7 @@ import { } from "@opencode-ai/schema/session-pending" import { Event } from "@opencode-ai/schema/event" import type { Database } from "../database/database" -import type { EventV2 } from "../event" +import { Bus } from "../bus" import { EventTable } from "../event/sql" import { KeyedMutex } from "../effect/keyed-mutex" import { SessionEvent } from "./event" @@ -38,7 +38,7 @@ const encodeUser = Schema.encodeSync(UserData) const decodeSynthetic = Schema.decodeUnknownSync(SyntheticData) const encodeSynthetic = Schema.encodeSync(SyntheticData) const decodeAdmittedEvent = Schema.decodeUnknownOption(SessionEvent.InputAdmitted.data) -const admittedEventType = Event.versionedType( +const admittedEventType = Bus.versionedType( SessionEvent.InputAdmitted.type, SessionEvent.InputAdmitted.durable.version, ) @@ -150,7 +150,7 @@ const promotedFromHistory = Effect.fn("SessionPending.promotedFromHistory")(func export const admit = Effect.fn("SessionPending.admit")(function* ( db: DatabaseService, - events: EventV2.Interface, + bus: Bus.Interface, request: { readonly id: SessionMessage.ID readonly sessionID: SessionSchema.ID @@ -164,7 +164,7 @@ export const admit = Effect.fn("SessionPending.admit")(function* ( } const promoted = yield* promotedFromHistory(db, request.sessionID, request.id) if (promoted !== undefined) return promoted - return yield* events + return yield* bus .publish(SessionEvent.InputAdmitted, { inputID: request.id, sessionID: request.sessionID, @@ -198,7 +198,7 @@ export const admit = Effect.fn("SessionPending.admit")(function* ( export const admitCompaction = Effect.fn("SessionPending.admitCompaction")(function* ( db: DatabaseService, - events: EventV2.Interface, + bus: Bus.Interface, input: { readonly id: SessionMessage.ID; readonly sessionID: SessionSchema.ID }, ) { return yield* inboxLocks.withLock(input.sessionID)( @@ -210,7 +210,7 @@ export const admitCompaction = Effect.fn("SessionPending.admitCompaction")(funct } const pending = yield* compaction(db, input.sessionID) if (pending) return pending - return yield* events + return yield* bus .publish(SessionEvent.Compaction.Admitted, { inputID: input.id, sessionID: input.sessionID, @@ -413,7 +413,7 @@ export const equivalent = ( const publish = Effect.fn("SessionPending.publish")(function* ( db: DatabaseService, - events: EventV2.Interface, + bus: Bus.Interface, sessionID: SessionSchema.ID, rows: ReadonlyArray, ) { @@ -423,7 +423,7 @@ const publish = Effect.fn("SessionPending.publish")(function* ( (row) => { const entry = fromRow(row) if (entry.type === "compaction") return Effect.die(new LifecycleConflict({ id: entry.id })) - return events + return bus .publish(SessionEvent.InputPromoted, { sessionID, inputID: entry.id, @@ -450,7 +450,7 @@ const publish = Effect.fn("SessionPending.publish")(function* ( */ export const promote = Effect.fn("SessionPending.promote")(function* ( db: DatabaseService, - events: EventV2.Interface, + bus: Bus.Interface, sessionID: SessionSchema.ID, scope: Promotable, ) { @@ -464,7 +464,7 @@ export const promote = Effect.fn("SessionPending.promote")(function* ( .orderBy(asc(SessionPendingTable.admitted_seq)) .all() .pipe(Effect.orDie) - if (steers.length > 0 || scope === "steer") return yield* publish(db, events, sessionID, steers) + if (steers.length > 0 || scope === "steer") return yield* publish(db, bus, sessionID, steers) const queued = yield* db .select() @@ -475,7 +475,7 @@ export const promote = Effect.fn("SessionPending.promote")(function* ( .get() .pipe(Effect.orDie) if (!queued) return 0 - const promoted = yield* publish(db, events, sessionID, [queued]) + const promoted = yield* publish(db, bus, sessionID, [queued]) const arrivedSteers = yield* db .select() .from(SessionPendingTable) @@ -483,7 +483,7 @@ export const promote = Effect.fn("SessionPending.promote")(function* ( .orderBy(asc(SessionPendingTable.admitted_seq)) .all() .pipe(Effect.orDie) - return promoted + (yield* publish(db, events, sessionID, arrivedSteers)) + return promoted + (yield* publish(db, bus, sessionID, arrivedSteers)) }), ) }) diff --git a/packages/core/src/session/projector.ts b/packages/core/src/session/projector.ts index f759a3757f28..5c888137621b 100644 --- a/packages/core/src/session/projector.ts +++ b/packages/core/src/session/projector.ts @@ -3,16 +3,16 @@ export * as SessionProjector from "./projector" import { and, asc, desc, eq, gt, gte, inArray, lt, sql } from "drizzle-orm" import { DateTime, Effect, Layer, Schema, Stream } from "effect" import { Database } from "../database/database" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" -import { ModelV2 } from "../model" +import { Model } from "../model" import { SessionEvent } from "./event" import { SessionV1 } from "../v1/session" import { WorkspaceTable } from "../control-plane/workspace.sql" import { SessionMessage } from "./message" import { SessionMessageUpdater } from "./message-updater" import { SessionPending } from "./pending" -import { WorkspaceV2 } from "../workspace" +import { Workspace } from "../workspace" import { InstructionState } from "./instruction-state" import { MessageTable, PartTable, SessionPendingTable, SessionMessageTable, SessionTable } from "./sql" import type { DeepMutable } from "../schema" @@ -134,7 +134,7 @@ function applyUsage( const publishSessionUsage = Effect.fn("SessionProjector.publishUsage")(function* ( db: DatabaseService, - events: EventV2.Interface, + bus: Bus.Interface, sessionID: (typeof SessionEvent.Step.Ended.Type)["data"]["sessionID"], ) { const row = yield* db @@ -151,7 +151,7 @@ const publishSessionUsage = Effect.fn("SessionProjector.publishUsage")(function* .get() .pipe(Effect.orDie) if (!row) return - yield* events.publish(SessionEvent.UsageUpdated, { + yield* bus.publish(SessionEvent.UsageUpdated, { sessionID, cost: Money.USD.make(row.cost), tokens: { @@ -314,7 +314,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* ( cursor = rows.at(-1)!.seq } - yield* EventV2.reserveSequence(db, event.data.sessionID, event.data.parentSeq) + yield* Bus.reserveSequence(db, event.data.sessionID, event.data.parentSeq) yield* InstructionState.rebuild(db, event.data.sessionID) }) @@ -349,7 +349,7 @@ function run(db: DatabaseService, event: MessageEvent) { .get() .pipe( Effect.orDie, - Effect.map((row) => (row?.model ? Schema.decodeUnknownSync(ModelV2.Ref)(row.model) : undefined)), + Effect.map((row) => (row?.model ? Schema.decodeUnknownSync(Model.Ref)(row.model) : undefined)), ) }, getCurrentAssistant() { @@ -460,9 +460,9 @@ function insertMessage(db: DatabaseService, event: SessionEvent.DurableEvent, me const layer = Layer.effectDiscard( Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const db = (yield* Database.Service).db - yield* events.project(SessionV1.Event.Created, (event) => + yield* bus.project(SessionV1.Event.Created, (event) => Effect.gen(function* () { const stored = yield* db .insert(SessionTable) @@ -482,7 +482,7 @@ const layer = Layer.effectDiscard( } }), ) - yield* events.project(SessionV1.Event.Updated, (event) => + yield* bus.project(SessionV1.Event.Updated, (event) => db .update(SessionTable) .set(sessionRow(event.data.info)) @@ -490,7 +490,7 @@ const layer = Layer.effectDiscard( .run() .pipe(Effect.orDie), ) - yield* events.project(SessionEvent.Moved, (event) => + yield* bus.project(SessionEvent.Moved, (event) => Effect.gen(function* () { yield* db .update(SessionTable) @@ -498,7 +498,7 @@ const layer = Layer.effectDiscard( directory: event.data.location.directory, path: event.data.subpath, ...(event.data.projectID ? { project_id: event.data.projectID } : {}), - workspace_id: event.data.location.workspaceID ? WorkspaceV2.ID.make(event.data.location.workspaceID) : null, + workspace_id: event.data.location.workspaceID ? Workspace.ID.make(event.data.location.workspaceID) : null, time_updated: DateTime.toEpochMillis(event.created), }) .where(eq(SessionTable.id, event.data.sessionID)) @@ -507,13 +507,13 @@ const layer = Layer.effectDiscard( yield* InstructionState.reset(db, event.data.sessionID) }), ) - yield* events.project(SessionV1.Event.Deleted, (event) => + yield* bus.project(SessionV1.Event.Deleted, (event) => db.delete(SessionTable).where(eq(SessionTable.id, event.data.sessionID)).run().pipe(Effect.orDie), ) - yield* events.project(SessionEvent.Deleted, (event) => + yield* bus.project(SessionEvent.Deleted, (event) => db.delete(SessionTable).where(eq(SessionTable.id, event.data.sessionID)).run().pipe(Effect.orDie), ) - yield* events.project(SessionV1.Event.MessageUpdated, (event) => + yield* bus.project(SessionV1.Event.MessageUpdated, (event) => Effect.gen(function* () { const time_created = event.data.info.time.created const id = event.data.info.id @@ -527,7 +527,7 @@ const layer = Layer.effectDiscard( .pipe(Effect.orDie) }), ) - yield* events.project(SessionV1.Event.MessageRemoved, (event) => + yield* bus.project(SessionV1.Event.MessageRemoved, (event) => Effect.gen(function* () { const rows = yield* db .select() @@ -546,7 +546,7 @@ const layer = Layer.effectDiscard( .pipe(Effect.orDie) }), ) - yield* events.project(SessionV1.Event.PartRemoved, (event) => + yield* bus.project(SessionV1.Event.PartRemoved, (event) => Effect.gen(function* () { const row = yield* db .select() @@ -563,7 +563,7 @@ const layer = Layer.effectDiscard( .pipe(Effect.orDie) }), ) - yield* events.project(SessionV1.Event.PartUpdated, (event) => + yield* bus.project(SessionV1.Event.PartUpdated, (event) => Effect.gen(function* () { const id = event.data.part.id const messageID = event.data.part.messageID @@ -582,7 +582,7 @@ const layer = Layer.effectDiscard( if (next) yield* applyUsage(db, sessionID, next) }), ) - yield* events.project(SessionEvent.AgentSelected, (event) => + yield* bus.project(SessionEvent.AgentSelected, (event) => db .update(SessionTable) .set({ agent: event.data.agent, time_updated: DateTime.toEpochMillis(event.created) }) @@ -590,7 +590,7 @@ const layer = Layer.effectDiscard( .run() .pipe(Effect.orDie, Effect.andThen(run(db, event))), ) - yield* events.project(SessionEvent.ModelSelected, (event) => + yield* bus.project(SessionEvent.ModelSelected, (event) => Effect.gen(function* () { yield* run(db, event) yield* db @@ -601,7 +601,7 @@ const layer = Layer.effectDiscard( .pipe(Effect.orDie) }), ) - yield* events.project(SessionEvent.Renamed, (event) => + yield* bus.project(SessionEvent.Renamed, (event) => db .update(SessionTable) .set({ title: event.data.title, time_updated: DateTime.toEpochMillis(event.created) }) @@ -609,9 +609,9 @@ const layer = Layer.effectDiscard( .run() .pipe(Effect.orDie), ) - yield* events.project(SessionEvent.UsageRecorded, (event) => applyUsage(db, event.data.sessionID, event.data)) - yield* events.project(SessionEvent.Forked, (event) => projectFork(db, event)) - yield* events.project(SessionEvent.InputPromoted, (event) => + yield* bus.project(SessionEvent.UsageRecorded, (event) => applyUsage(db, event.data.sessionID, event.data)) + yield* bus.project(SessionEvent.Forked, (event) => projectFork(db, event)) + yield* bus.project(SessionEvent.InputPromoted, (event) => Effect.gen(function* () { if (event.durable === undefined) return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence")) @@ -643,7 +643,7 @@ const layer = Layer.effectDiscard( ) }), ) - yield* events.project(SessionEvent.InputAdmitted, (event) => + yield* bus.project(SessionEvent.InputAdmitted, (event) => Effect.gen(function* () { if (event.durable === undefined) return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence")) @@ -656,7 +656,7 @@ const layer = Layer.effectDiscard( }) }), ) - yield* events.project(SessionEvent.Compaction.Admitted, (event) => + yield* bus.project(SessionEvent.Compaction.Admitted, (event) => Effect.gen(function* () { if (event.durable === undefined) return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence")) @@ -668,42 +668,42 @@ const layer = Layer.effectDiscard( }) }), ) - yield* events.project(SessionEvent.Execution.Succeeded, (event) => run(db, event)) - yield* events.project(SessionEvent.Execution.Failed, (event) => run(db, event)) - yield* events.project(SessionEvent.Execution.Interrupted, (event) => run(db, event)) - yield* events.project(SessionEvent.InstructionsUpdated, (event) => + yield* bus.project(SessionEvent.Execution.Succeeded, (event) => run(db, event)) + yield* bus.project(SessionEvent.Execution.Failed, (event) => run(db, event)) + yield* bus.project(SessionEvent.Execution.Interrupted, (event) => run(db, event)) + yield* bus.project(SessionEvent.InstructionsUpdated, (event) => InstructionState.apply(db, event.data.sessionID, event.durable.seq, event.data.delta), ) - yield* events.project(SessionEvent.Synthetic, (event) => run(db, event)) - yield* events.project(SessionEvent.Skill.Activated, (event) => run(db, event)) - yield* events.project(SessionEvent.Shell.Started, (event) => run(db, event)) - yield* events.project(SessionEvent.Shell.Ended, (event) => run(db, event)) - yield* events.project(SessionEvent.Step.Started, (event) => run(db, event)) - yield* events.project(SessionEvent.Step.Ended, (event) => + yield* bus.project(SessionEvent.Synthetic, (event) => run(db, event)) + yield* bus.project(SessionEvent.Skill.Activated, (event) => run(db, event)) + yield* bus.project(SessionEvent.Shell.Started, (event) => run(db, event)) + yield* bus.project(SessionEvent.Shell.Ended, (event) => run(db, event)) + yield* bus.project(SessionEvent.Step.Started, (event) => run(db, event)) + yield* bus.project(SessionEvent.Step.Ended, (event) => Effect.gen(function* () { yield* run(db, event) yield* applyUsage(db, event.data.sessionID, event.data) }), ) - yield* events.project(SessionEvent.Step.Failed, (event) => + yield* bus.project(SessionEvent.Step.Failed, (event) => Effect.gen(function* () { yield* run(db, event) if (event.data.cost !== undefined && event.data.tokens !== undefined) yield* applyUsage(db, event.data.sessionID, { cost: event.data.cost, tokens: event.data.tokens }) }), ) - yield* events.project(SessionEvent.Text.Started, (event) => run(db, event)) - yield* events.project(SessionEvent.Text.Ended, (event) => run(db, event)) - yield* events.project(SessionEvent.Tool.Input.Started, (event) => run(db, event)) - yield* events.project(SessionEvent.Tool.Input.Ended, (event) => run(db, event)) - yield* events.project(SessionEvent.Tool.Called, (event) => run(db, event)) - yield* events.project(SessionEvent.Tool.Success, (event) => run(db, event)) - yield* events.project(SessionEvent.Tool.Failed, (event) => run(db, event)) - yield* events.project(SessionEvent.Reasoning.Started, (event) => run(db, event)) - yield* events.project(SessionEvent.Reasoning.Ended, (event) => run(db, event)) - yield* events.project(SessionEvent.RetryScheduled, (event) => run(db, event)) - yield* events.project(SessionEvent.Compaction.Started, (event) => run(db, event)) - yield* events.project(SessionEvent.Compaction.Ended, (event) => + yield* bus.project(SessionEvent.Text.Started, (event) => run(db, event)) + yield* bus.project(SessionEvent.Text.Ended, (event) => run(db, event)) + yield* bus.project(SessionEvent.Tool.Input.Started, (event) => run(db, event)) + yield* bus.project(SessionEvent.Tool.Input.Ended, (event) => run(db, event)) + yield* bus.project(SessionEvent.Tool.Called, (event) => run(db, event)) + yield* bus.project(SessionEvent.Tool.Success, (event) => run(db, event)) + yield* bus.project(SessionEvent.Tool.Failed, (event) => run(db, event)) + yield* bus.project(SessionEvent.Reasoning.Started, (event) => run(db, event)) + yield* bus.project(SessionEvent.Reasoning.Ended, (event) => run(db, event)) + yield* bus.project(SessionEvent.RetryScheduled, (event) => run(db, event)) + yield* bus.project(SessionEvent.Compaction.Started, (event) => run(db, event)) + yield* bus.project(SessionEvent.Compaction.Ended, (event) => Effect.gen(function* () { yield* run(db, event) yield* InstructionState.advanceEpoch(db, event.data.sessionID, event.durable.seq) @@ -713,7 +713,7 @@ const layer = Layer.effectDiscard( yield* SessionPending.settleCompaction(db, { sessionID: event.data.sessionID }) }), ) - yield* events.project(SessionEvent.Compaction.Failed, (event) => + yield* bus.project(SessionEvent.Compaction.Failed, (event) => Effect.gen(function* () { yield* run(db, event) if (event.durable === undefined) @@ -722,7 +722,7 @@ const layer = Layer.effectDiscard( yield* SessionPending.settleCompaction(db, { sessionID: event.data.sessionID }) }), ) - yield* events.project(SessionEvent.RevertEvent.Staged, (event) => + yield* bus.project(SessionEvent.RevertEvent.Staged, (event) => Effect.gen(function* () { const revert = event.data.revert yield* db @@ -736,7 +736,7 @@ const layer = Layer.effectDiscard( .pipe(Effect.orDie) }), ) - yield* events.project(SessionEvent.RevertEvent.Cleared, (event) => + yield* bus.project(SessionEvent.RevertEvent.Cleared, (event) => db .update(SessionTable) .set({ revert: null, time_updated: DateTime.toEpochMillis(event.created) }) @@ -744,7 +744,7 @@ const layer = Layer.effectDiscard( .run() .pipe(Effect.orDie, Effect.asVoid), ) - yield* events.project(SessionEvent.RevertEvent.Committed, (event) => + yield* bus.project(SessionEvent.RevertEvent.Committed, (event) => Effect.gen(function* () { const boundary = yield* db .select({ seq: SessionMessageTable.seq }) @@ -781,18 +781,18 @@ const layer = Layer.effectDiscard( yield* InstructionState.reset(db, event.data.sessionID) }), ) - yield* events.subscribe([SessionEvent.Step.Ended, SessionEvent.Step.Failed, SessionEvent.UsageRecorded]).pipe( + yield* bus.subscribe([SessionEvent.Step.Ended, SessionEvent.Step.Failed, SessionEvent.UsageRecorded]).pipe( Stream.runForEach((event) => { if ( event.type === SessionEvent.Step.Failed.type && (event.data.cost === undefined || event.data.tokens === undefined) ) return Effect.void - return publishSessionUsage(db, events, event.data.sessionID) + return publishSessionUsage(db, bus, event.data.sessionID) }), Effect.forkScoped({ startImmediately: true }), ) }), ) -export const node = makeGlobalNode({ name: "session-projector", layer, deps: [EventV2.node, Database.node] }) +export const node = makeGlobalNode({ name: "session-projector", layer, deps: [Bus.node, Database.node] }) diff --git a/packages/core/src/session/revert.ts b/packages/core/src/session/revert.ts index 42bc34e6943d..429170e9870d 100644 --- a/packages/core/src/session/revert.ts +++ b/packages/core/src/session/revert.ts @@ -3,7 +3,7 @@ export * as SessionRevert from "./revert" import { and, asc, eq, gt } from "drizzle-orm" import { Effect, Schema } from "effect" import { Database } from "../database/database" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { RelativePath } from "../schema" import { Snapshot } from "../snapshot" import { SessionEvent } from "./event" @@ -63,7 +63,7 @@ export const stage = Effect.fn("SessionRevert.stage")(function* (input: { readonly files?: boolean }) { const snapshot = yield* Snapshot.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const original = input.session.revert?.snapshot ? Snapshot.ID.make(input.session.revert.snapshot) : yield* snapshot.capture() @@ -83,7 +83,7 @@ export const stage = Effect.fn("SessionRevert.stage")(function* (input: { snapshot: original, files, } satisfies SessionSchema.Info["revert"] - yield* events.publish(SessionEvent.RevertEvent.Staged, { + yield* bus.publish(SessionEvent.RevertEvent.Staged, { sessionID: input.session.id, revert, }) @@ -98,16 +98,16 @@ export const clear = Effect.fn("SessionRevert.clear")(function* (session: Sessio yield* snapshot.restore({ files: new Map((session.revert.files ?? []).map((file) => [RelativePath.make(file.file), original])), }) - const events = yield* EventV2.Service - yield* events.publish(SessionEvent.RevertEvent.Cleared, { + const bus = yield* Bus.Service + yield* bus.publish(SessionEvent.RevertEvent.Cleared, { sessionID: session.id, }) }) export const commit = Effect.fn("SessionRevert.commit")(function* (session: SessionSchema.Info) { if (!session.revert) return - const events = yield* EventV2.Service - yield* events.publish(SessionEvent.RevertEvent.Committed, { + const bus = yield* Bus.Service + yield* bus.publish(SessionEvent.RevertEvent.Committed, { sessionID: session.id, to: session.revert.messageID, }) diff --git a/packages/core/src/session/runner/index.ts b/packages/core/src/session/runner/index.ts index 65fdb223524e..db45c0cd31f2 100644 --- a/packages/core/src/session/runner/index.ts +++ b/packages/core/src/session/runner/index.ts @@ -6,7 +6,6 @@ import { SessionSchema } from "../schema" import type { AgentNotFoundError, MessageDecodeError, StepFailedError, UserInterruptedError } from "../error" import { SessionRunnerModel } from "./model" import type { Instructions } from "../../instructions/index" -import type { ToolOutputStore } from "../../tool-output-store" export type RunError = | LLMError @@ -16,7 +15,6 @@ export type RunError = | StepFailedError | UserInterruptedError | Instructions.InitializationBlocked - | ToolOutputStore.Error /** Runs one local continuation from already-recorded Session history. */ export interface Interface { @@ -27,4 +25,4 @@ export interface Interface { }) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/SessionRunner") {} +export class Service extends Context.Service()("@opencode/SessionRunner") {} diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index da722b449f8f..42f871e8f704 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -3,10 +3,9 @@ export * as SessionRunnerLLM from "./llm" import { LLMClient, LLMError, LLMEvent, isContextOverflowFailure, type ProviderErrorEvent, type ToolCall } from "@opencode-ai/ai" import { Cause, Data, Effect, Exit, Fiber, FiberSet, Layer, Option, Pull, Schedule, Stream } from "effect" import { Database } from "../../database/database" -import { EventV2 } from "../../event" -import { PermissionV2 } from "../../permission" -import { QuestionTool } from "../../tool/question" -import { ToolOutputStore } from "../../tool-output-store" +import { Bus } from "../../bus" +import { Permission } from "../../permission" +import { QuestionTool } from "../../tool/plugin/question" import { InstructionState } from "../instruction-state" import { SessionCompaction } from "../compaction" import { SessionContext } from "../context" @@ -38,8 +37,8 @@ const CallOutcome = Data.taggedEnum() // Declining an interactive prompt halts the drain instead of becoming model-facing tool output. const isDecline = ( error: SessionModelRequest.ExecuteError, -): error is PermissionV2.DeclinedError | QuestionTool.CancelledError => - error._tag === "PermissionV2.DeclinedError" || error._tag === "QuestionTool.CancelledError" +): error is Permission.DeclinedError | QuestionTool.CancelledError => + error._tag === "Permission.DeclinedError" || error._tag === "QuestionTool.CancelledError" /** * Classifies how the owned tool fibers ended. Interrupts abort the step; a user decline @@ -66,8 +65,8 @@ const classifyToolExits = ( // drain's error channel never carries a decline. const failure = causes.flatMap((cause) => { if (Cause.hasInterrupts(cause)) return [] - const reasons = cause.reasons.flatMap((reason): Array> => - Cause.isFailReason(reason) ? (isDecline(reason.error) ? [] : [Cause.makeFailReason(reason.error)]) : [reason], + const reasons = cause.reasons.flatMap((reason): Array> => + Cause.isFailReason(reason) ? [] : [reason], ) return reasons.length > 0 ? [Cause.fromReasons(reasons)] : [] }).at(0) @@ -75,7 +74,6 @@ const classifyToolExits = ( interrupted: causes.some(Cause.hasInterrupts), declines, failure, - infraError: failure === undefined ? undefined : Option.getOrUndefined(Cause.findErrorOption(failure)), } } @@ -86,7 +84,7 @@ const RESULT_MISSING = { type: "tool.result-missing", message: "Provider did not const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const llm = yield* LLMClient.Service const store = yield* SessionStore.Service const context = yield* SessionContext.Service @@ -146,7 +144,7 @@ const layer = Layer.effect( // compaction boundary, so the rebuilt step needs identity inside the new epoch. let assistantMessageID = SessionMessage.ID.create() const retry = yield* Schedule.toStepWithSleep( - SessionRunnerRetry.schedule(events, sessionID, () => assistantMessageID), + SessionRunnerRetry.schedule(bus, sessionID, () => assistantMessageID), ) /** * Consumes one retry allowance: sleeps the scheduled backoff, or publishes @@ -157,7 +155,7 @@ const layer = Layer.effect( retry(failure).pipe( Effect.as(CallOutcome.Retry({ step: failure.step })), Pull.catchDone(() => - events + bus .publish(SessionEvent.Step.Failed, { sessionID, assistantMessageID, @@ -203,8 +201,8 @@ const layer = Layer.effect( const selected = yield* context.select(sessionID) // Establish what the model knows before admitting what the user said, so // a blocked first step leaves pending inputs untouched. - yield* InstructionState.prepare(db, events, selected.instructions, selected.session.id) - const promoted = promotable ? yield* SessionPending.promote(db, events, selected.session.id, promotable) : 0 + yield* InstructionState.prepare(db, bus, selected.instructions, selected.session.id) + const promoted = promotable ? yield* SessionPending.promote(db, bus, selected.session.id, promotable) : 0 // Promoted input opens a fresh step allowance. const currentStep = promoted > 0 ? 1 : step const loaded = yield* context.load(selected) @@ -231,7 +229,7 @@ const layer = Layer.effect( }> = [] const interruptTools = Effect.suspend(() => Fiber.interruptAll(toolRuns.map((run) => run.fiber))) const startSnapshot = yield* snapshots.capture() - const publisher = createLLMEventPublisher(events, { + const publisher = createLLMEventPublisher(bus, { sessionID: session.id, agent: agent.id, // The selected catalog identity, not model.id: route-level ids are provider API @@ -260,7 +258,7 @@ const layer = Layer.effect( const publishStepEnd = (finish: NonNullable) => Effect.gen(function* () { const end = yield* captureStepEnd() - yield* events.publish(SessionEvent.Step.Ended, { + yield* bus.publish(SessionEvent.Step.Ended, { sessionID: session.id, assistantMessageID: yield* publisher.startAssistant(), finish: finish.finish, @@ -310,6 +308,9 @@ const layer = Layer.effect( // The fiber owns its call: it publishes its own completion, masked so a // finished execution always reaches its durable settlement. Effect.flatMap((outcome) => publisher.toolExecution(event.id, event.name, outcome)), + Effect.catchTag("Tool.Error", (error) => + publisher.failTool(event.id, toSessionError(error)).pipe(Effect.asVoid), + ), ), ).pipe(Effect.forkScoped), }) @@ -386,9 +387,8 @@ const layer = Layer.effect( yield* publisher.failAssistant(STEP_INTERRUPTED) } if (tools.failure !== undefined) { - const error = toSessionError(tools.infraError ?? Cause.squash(tools.failure)) + const error = toSessionError(Cause.squash(tools.failure)) yield* publisher.failUnsettledTools(error) - if (tools.infraError !== undefined) yield* publisher.failAssistant(error) } // Local calls have joined, so the remaining sweeps only close hosted calls the // provider promised but never resolved. @@ -413,8 +413,7 @@ const layer = Layer.effect( if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause) if (tools.declines.length > 0) return yield* Effect.interrupt - if ((tools.interrupted || tools.infraError !== undefined) && tools.failure) - return yield* Effect.failCause(tools.failure) + if (tools.interrupted && tools.failure) return yield* Effect.failCause(tools.failure) if (tools.interrupted && joined._tag === "Failure") return yield* Effect.failCause(joined.cause) if (record.failure) return yield* new StepFailedError({ error: record.failure }) return CallOutcome.Completed({ @@ -450,7 +449,7 @@ const layer = Layer.effect( if (Exit.isSuccess(compacted)) return const unsettled = yield* SessionPending.compaction(db, sessionID) if (unsettled) - yield* events.publish(SessionEvent.Compaction.Failed, { + yield* bus.publish(SessionEvent.Compaction.Failed, { sessionID, reason: "manual", error: Cause.hasInterruptsOnly(compacted.cause) @@ -471,7 +470,7 @@ const layer = Layer.effect( if (message.type !== "assistant") continue for (const tool of message.content) { if (tool.type !== "tool" || (tool.state.status !== "streaming" && tool.state.status !== "running")) continue - yield* events.publish(SessionEvent.Tool.Failed, { + yield* bus.publish(SessionEvent.Tool.Failed, { sessionID, assistantMessageID: message.id, callID: tool.id, @@ -503,7 +502,7 @@ export const node = makeLocationNode({ service: Service, layer, deps: [ - EventV2.node, + Bus.node, llmClient, SessionContext.node, SessionModelRequest.node, diff --git a/packages/core/src/session/runner/model.ts b/packages/core/src/session/runner/model.ts index d0902d7d674b..3b842b72eb8a 100644 --- a/packages/core/src/session/runner/model.ts +++ b/packages/core/src/session/runner/model.ts @@ -5,8 +5,8 @@ import { Model } from "@opencode-ai/ai" import { Context, Effect, Layer, Schema } from "effect" import { Catalog } from "../../catalog" import { ModelResolver } from "../../model-resolver" -import { ModelV2 } from "../../model" -import { ProviderV2 } from "../../provider" +import { Capabilities, ID, Info, Ref, VariantID } from "../../model" +import { Provider } from "../../provider" import { SessionSchema } from "../schema" export class ModelNotSelectedError extends Schema.TaggedErrorClass()( @@ -20,7 +20,7 @@ export class ModelNotSelectedError extends Schema.TaggedErrorClass()( "SessionRunnerModel.ModelUnavailableError", - { providerID: ProviderV2.ID, modelID: ModelV2.ID }, + { providerID: Provider.ID, modelID: ID }, ) { override get message() { return `Model unavailable: ${this.providerID}/${this.modelID}` @@ -38,21 +38,21 @@ export interface Interface { readonly resolve: (session: SessionSchema.Info) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/SessionRunnerModel") {} +export class Service extends Context.Service()("@opencode/SessionRunnerModel") {} /** Builds a Resolved whose catalog identity mirrors the route model. Test or embedding seam. */ export const resolved = ( model: Model, options: { - readonly capabilities: ModelV2.Capabilities - readonly variant?: ModelV2.VariantID - readonly cost: ModelV2.Info["cost"] + readonly capabilities: Capabilities + readonly variant?: VariantID + readonly cost: Info["cost"] }, ): Resolved => ({ model, - ref: ModelV2.Ref.make({ - id: ModelV2.ID.make(model.id), - providerID: ProviderV2.ID.make(model.provider), + ref: Ref.make({ + id: ID.make(model.id), + providerID: Provider.ID.make(model.provider), ...(options.variant === undefined ? {} : { variant: options.variant }), }), capabilities: options.capabilities, diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts index 440f6779b865..99dc0ad77b54 100644 --- a/packages/core/src/session/runner/publish-llm-event.ts +++ b/packages/core/src/session/runner/publish-llm-event.ts @@ -1,23 +1,22 @@ import { type LLMEvent, type ProviderMetadata, type ToolResultValue } from "@opencode-ai/ai" import { Effect } from "effect" -import { EventV2 } from "../../event" -import { ModelV2 } from "../../model" +import { Bus } from "../../bus" +import { Model } from "../../model" import { SessionEvent } from "../event" import { SessionMessage } from "../message" import { SessionSchema } from "../schema" import { SessionError } from "@opencode-ai/schema/session-error" import { Money } from "@opencode-ai/schema/money" -import { AgentV2 } from "../../agent" +import { Agent } from "../../agent" import { Snapshot } from "../../snapshot" import { RelativePath } from "../../schema" import { SessionUsage } from "../usage" -import { Tool } from "../../tool/tool" -import type { ToolRegistry } from "../../tool/registry" +import { Tool } from "@opencode-ai/schema/tool" type Input = { readonly sessionID: SessionSchema.ID - readonly agent: AgentV2.ID - readonly model: ModelV2.Ref + readonly agent: Agent.ID + readonly model: Model.Ref readonly providerMetadataKey: string readonly snapshot?: Snapshot.ID readonly assistantMessageID: SessionMessage.ID @@ -48,12 +47,26 @@ export interface StepRecord { } /** Derives canonical model content from a provider-hosted tool result. */ -const hostedContent = (result: ToolResultValue): Tool.NonEmptyContent => { +type NonEmptyContent = readonly [Tool.Content, ...Tool.Content[]] + +const nonEmpty = (content: ReadonlyArray): NonEmptyContent | undefined => + content.length > 0 ? (content as NonEmptyContent) : undefined + +const stringify = (value: unknown) => { + if (typeof value === "string") return value + try { + return JSON.stringify(value) ?? String(value) + } catch { + return String(value) + } +} + +const hostedContent = (result: ToolResultValue): NonEmptyContent => { if (result.type === "content") { - const content = Tool.nonEmpty(result.value) + const content = nonEmpty(result.value) if (content !== undefined) return content } - return [{ type: "text", text: Tool.stringify(result.value) }] + return [{ type: "text", text: stringify(result.value) }] } /** @@ -67,7 +80,7 @@ const hostedContent = (result: ToolResultValue): Tool.NonEmptyContent => { * order: each publishing fiber is sequential, so per-source order holds by construction, * and consumers fold by callID/ordinal rather than global position. */ -export const createLLMEventPublisher = (events: Pick, input: Input) => { +export const createLLMEventPublisher = (bus: Pick, input: Input) => { const tools = new Map< string, { @@ -76,10 +89,10 @@ export const createLLMEventPublisher = (events: Pick() - const failureSnapshot = (tool: { readonly progress?: ToolRegistry.Progress }) => + const failureSnapshot = (tool: { readonly progress?: Tool.Metadata }) => tool.progress === undefined ? {} : { metadata: tool.progress } const assistantMessageID = input.assistantMessageID let stepStarted = false @@ -92,7 +105,7 @@ export const createLLMEventPublisher = (events: Pick Effect.gen(function* () { - yield* events.publish(SessionEvent.Text.Ended, { + yield* bus.publish(SessionEvent.Text.Ended, { sessionID: input.sessionID, assistantMessageID: yield* currentAssistantMessageID(), ordinal, @@ -165,7 +178,7 @@ export const createLLMEventPublisher = (events: Pick Effect.gen(function* () { - yield* events.publish(SessionEvent.Reasoning.Ended, { + yield* bus.publish(SessionEvent.Reasoning.Ended, { sessionID: input.sessionID, assistantMessageID: yield* currentAssistantMessageID(), ordinal, @@ -179,7 +192,7 @@ export const createLLMEventPublisher = (events: Pick ${event.name}`)) if (toolInput.has(event.id)) yield* endToolInput(event, event.raw) tool.settled = true - yield* events.publish(SessionEvent.Tool.Failed, { + yield* bus.publish(SessionEvent.Tool.Failed, { sessionID: input.sessionID, assistantMessageID: tool.assistantMessageID, callID: event.id, @@ -263,7 +276,7 @@ export const createLLMEventPublisher = (events: Pick ${event.name}`)) if (!toolInput.has(event.id)) return yield* Effect.die(new Error(`Tool input delta after end: ${event.id}`)) yield* toolInput.append(event.id, event.text) - yield* events.publish(SessionEvent.Tool.Input.Delta, { + yield* bus.publish(SessionEvent.Tool.Input.Delta, { sessionID: input.sessionID, assistantMessageID: tool.assistantMessageID, callID: event.id, @@ -408,7 +421,7 @@ export const createLLMEventPublisher = (events: Pick ${event.name}`)) if (tool.settled) return yield* Effect.die(new Error(`Duplicate tool error: ${event.id}`)) tool.settled = true - yield* events.publish(SessionEvent.Tool.Failed, { + yield* bus.publish(SessionEvent.Tool.Failed, { sessionID: input.sessionID, assistantMessageID: tool.assistantMessageID, callID: event.id, @@ -495,12 +508,12 @@ export const createLLMEventPublisher = (events: Pick ${name}`)) - if (tool.settled) { - if (execution.status === "error") return - return yield* Effect.die(new Error(`Duplicate tool execution: ${callID}`)) - } + if (tool.settled) return yield* Effect.die(new Error(`Duplicate tool execution: ${callID}`)) tool.settled = true - if (execution.status === "completed") { - yield* events.publish(SessionEvent.Tool.Success, { - sessionID: input.sessionID, - assistantMessageID: tool.assistantMessageID, - callID, - content: execution.content, - ...(execution.metadata === undefined ? {} : { metadata: execution.metadata }), - executed: tool.providerExecuted, - }) - return - } - // An execution-provided snapshot wins; otherwise fall back to retained progress. - const snapshot = - execution.content !== undefined || execution.metadata !== undefined - ? { - ...(execution.content === undefined ? {} : { content: execution.content }), - ...(execution.metadata === undefined ? {} : { metadata: execution.metadata }), - } - : failureSnapshot(tool) - yield* events.publish(SessionEvent.Tool.Failed, { + const content = + typeof result.content === "string" + ? [{ type: "text" as const, text: result.content }] + : result.content === undefined + ? [] + : [...result.content] + if (content.length === 0) return yield* Effect.die(new Error(`Tool execution has no content: ${callID}`)) + yield* bus.publish(SessionEvent.Tool.Success, { sessionID: input.sessionID, assistantMessageID: tool.assistantMessageID, callID, - error: execution.error, - ...snapshot, + content: [content[0], ...content.slice(1)], + ...(result.metadata === undefined ? {} : { metadata: result.metadata }), executed: tool.providerExecuted, }) }) diff --git a/packages/core/src/session/runner/retry.ts b/packages/core/src/session/runner/retry.ts index de94d36ec6b7..3e22554d60f7 100644 --- a/packages/core/src/session/runner/retry.ts +++ b/packages/core/src/session/runner/retry.ts @@ -3,7 +3,7 @@ export * as SessionRunnerRetry from "./retry" import { LLMError } from "@opencode-ai/ai" import { SessionError } from "@opencode-ai/schema/session-error" import { Data, Duration, Effect, Schedule } from "effect" -import { EventV2 } from "../../event" +import { Bus } from "../../bus" import { SessionEvent } from "../event" import { SessionMessage } from "../message" import { SessionSchema } from "../schema" @@ -41,7 +41,7 @@ const retryAfter = (failure: RetryableFailure) => { return undefined } -export const schedule = (events: EventV2.Interface, sessionID: SessionSchema.ID, assistantMessageID: () => SessionMessage.ID) => +export const schedule = (bus: Bus.Interface, sessionID: SessionSchema.ID, assistantMessageID: () => SessionMessage.ID) => Schedule.max([Schedule.exponential("2 seconds"), Schedule.recurs(4)]).pipe( Schedule.setInputType(), Schedule.modifyDelay(({ input: failure, duration: delay }) => { @@ -49,7 +49,7 @@ export const schedule = (events: EventV2.Interface, sessionID: SessionSchema.ID, return Effect.succeed(minimum === undefined ? delay : Duration.max(delay, Duration.millis(minimum))) }), Schedule.tap((metadata) => - events.publish(SessionEvent.RetryScheduled, { + bus.publish(SessionEvent.RetryScheduled, { sessionID, assistantMessageID: assistantMessageID(), attempt: metadata.attempt + 1, diff --git a/packages/core/src/session/runner/to-llm-message.ts b/packages/core/src/session/runner/to-llm-message.ts index d708563eff6d..a58ced07be6c 100644 --- a/packages/core/src/session/runner/to-llm-message.ts +++ b/packages/core/src/session/runner/to-llm-message.ts @@ -1,6 +1,6 @@ import { Message, ToolCallPart, ToolResultPart, type ContentPart, type ProviderMetadata } from "@opencode-ai/ai" import { Option, Schema } from "effect" -import type { ModelV2 } from "../../model" +import type { Model } from "../../model" import { SessionMessage } from "../message" import type { FileAttachment } from "@opencode-ai/schema/prompt" @@ -108,7 +108,7 @@ const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: Provid } } -const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref, providerMetadataKey: string) => { +const assistant = (message: SessionMessage.Assistant, model: Model.Ref, providerMetadataKey: string) => { const sameProvider = String(message.model.providerID) === String(model.providerID) const sameModel = sameProvider && String(message.model.id) === String(model.id) const reuseProviderMetadata = sameModel && message.error === undefined @@ -177,7 +177,7 @@ const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref, provid ] } -function toLLMMessage(message: SessionMessage.Info, model: ModelV2.Ref, providerMetadataKey: string): Message[] { +function toLLMMessage(message: SessionMessage.Info, model: Model.Ref, providerMetadataKey: string): Message[] { switch (message.type) { case "agent-switched": case "model-switched": @@ -239,9 +239,9 @@ ${message.recent} } } -/** Translate projected V2 Session history into canonical @opencode-ai/ai context. */ +/** Translate projected Session history into canonical @opencode-ai/ai context. */ export const toLLMMessages = ( messages: readonly SessionMessage.Info[], - model: ModelV2.Ref, + model: Model.Ref, providerMetadataKey: string = model.providerID, ) => messages.flatMap((message) => toLLMMessage(message, model, providerMetadataKey)) diff --git a/packages/core/src/session/sql.ts b/packages/core/src/session/sql.ts index 21d0ab5f2675..17654df1b38e 100644 --- a/packages/core/src/session/sql.ts +++ b/packages/core/src/session/sql.ts @@ -6,10 +6,10 @@ import type { SessionMessage } from "./message" import type { SessionPending } from "./pending" import type { FileDiff } from "@opencode-ai/schema/file-diff" import { PermissionV1 } from "../v1/permission" -import { ProjectV2 } from "../project" +import { Project } from "../project" import type { SessionSchema } from "./schema" import type { MessageID, PartID, SessionV1 } from "../v1/session" -import { WorkspaceV2 } from "../workspace" +import { Workspace } from "../workspace" import { Timestamps } from "../database/schema.sql" import type { Instruction } from "@opencode-ai/schema/instruction" import type { Session } from "@opencode-ai/schema/session" @@ -26,10 +26,10 @@ export const SessionTable = sqliteTable( { id: text().$type().primaryKey(), project_id: text() - .$type() + .$type() .notNull() .references(() => ProjectTable.id, { onDelete: "cascade" }), - workspace_id: text().$type(), + workspace_id: text().$type(), parent_id: text().$type(), fork_session_id: text().$type(), fork_message_id: text().$type(), diff --git a/packages/core/src/session/store.ts b/packages/core/src/session/store.ts index 0018ace4a645..8a6058ca2abe 100644 --- a/packages/core/src/session/store.ts +++ b/packages/core/src/session/store.ts @@ -23,7 +23,7 @@ export interface Interface { readonly suspend: (sessionIDs: Iterable) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/SessionStore") {} +export class Service extends Context.Service()("@opencode/SessionStore") {} const layer = Layer.effect( Service, diff --git a/packages/core/src/session/title.ts b/packages/core/src/session/title.ts index d9cba66a8a82..ed795477b4cc 100644 --- a/packages/core/src/session/title.ts +++ b/packages/core/src/session/title.ts @@ -2,9 +2,9 @@ export * as SessionTitle from "./title" import { LLM, LLMClient, LLMError, LLMEvent, Message, type LLMRequest } from "@opencode-ai/ai" import { Context, Effect, Layer, Stream } from "effect" -import { AgentV2 } from "../agent" +import { Agent } from "../agent" import { Database } from "../database/database" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { App } from "../app" import { llmClient } from "../effect/app-node-platform" @@ -19,11 +19,11 @@ const MAX_LENGTH = 100 type Dependencies = { readonly app: App.Info - readonly events: EventV2.Interface + readonly bus: Bus.Interface readonly llm: { readonly stream: (request: LLMRequest) => Stream.Stream } - readonly agents: AgentV2.Interface + readonly agents: Agent.Interface readonly models: SessionRunnerModel.Interface } @@ -32,7 +32,7 @@ export interface Interface { readonly generateForFirstPrompt: (session: SessionSchema.Info) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/SessionTitle") {} +export class Service extends Context.Service()("@opencode/SessionTitle") {} const truncate = (value: string) => (value.length <= MAX_LENGTH ? value : `${value.slice(0, MAX_LENGTH - 3)}...`) @@ -44,7 +44,7 @@ const make = (dependencies: Dependencies) => { if (session.parentID) return const firstUser = yield* SessionHistory.firstUserMessageIfOnly(db, session.id) if (!firstUser) return - const agent = yield* dependencies.agents.get(AgentV2.ID.make("title")) + const agent = yield* dependencies.agents.get(Agent.ID.make("title")) if (!agent) return const resolved = yield* ( agent.model @@ -57,7 +57,7 @@ const make = (dependencies: Dependencies) => { let usage: SessionUsage.Recorded | undefined const recordUsage = Effect.suspend(() => usage - ? dependencies.events.publish(SessionEvent.UsageRecorded, { + ? dependencies.bus.publish(SessionEvent.UsageRecorded, { sessionID: session.id, source: "title", ...usage, @@ -96,7 +96,7 @@ const make = (dependencies: Dependencies) => { .map((line) => line.trim()) .find((line) => line.length > 0) if (!title) return - yield* dependencies.events.publish(SessionEvent.Renamed, { + yield* dependencies.bus.publish(SessionEvent.Renamed, { sessionID: session.id, title: truncate(title), }) @@ -107,13 +107,13 @@ const make = (dependencies: Dependencies) => { export const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const llm = yield* LLMClient.Service - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const models = yield* SessionRunnerModel.Service const database = yield* Database.Service const app = yield* App.Metadata - const title = make({ events, llm, agents, models, app }) + const title = make({ bus, llm, agents, models, app }) return Service.of({ generateForFirstPrompt: (session) => title.generateForFirstPrompt(database.db, session), }) @@ -123,5 +123,5 @@ export const layer = Layer.effect( export const node = makeLocationNode({ service: Service, layer, - deps: [EventV2.node, llmClient, AgentV2.node, SessionRunnerModel.node, Database.node, App.node], + deps: [Bus.node, llmClient, Agent.node, SessionRunnerModel.node, Database.node, App.node], }) diff --git a/packages/core/src/session/to-session-error.ts b/packages/core/src/session/to-session-error.ts index fe21051d8ab4..d7ccb3dc7e93 100644 --- a/packages/core/src/session/to-session-error.ts +++ b/packages/core/src/session/to-session-error.ts @@ -1,10 +1,9 @@ import { LLMError, ToolFailure } from "@opencode-ai/ai" -import { Tool } from "@opencode-ai/plugin/v2/effect/tool" +import { Tool } from "@opencode-ai/schema/tool" import { SessionError } from "@opencode-ai/schema/session-error" -import { PermissionV2 } from "../permission" -import { QuestionV2 } from "../question" +import { Permission } from "../permission" +import { Question } from "../question" import { Integration } from "../integration" -import { ToolOutputStore } from "../tool-output-store" import { AgentNotFoundError, StepFailedError, UserInterruptedError } from "./error" import { SessionRunnerModel } from "./runner/model" @@ -37,9 +36,9 @@ export function toSessionError(cause: unknown): SessionError.Error { } } } - if (cause instanceof PermissionV2.BlockedError) return { type: "permission.rejected", message: cause.message } - if (cause instanceof QuestionV2.RejectedError) return { type: "aborted", message: cause.message } - if (cause instanceof ToolFailure || cause instanceof Tool.Failure) { + if (cause instanceof Permission.BlockedError) return { type: "permission.rejected", message: cause.message } + if (cause instanceof Question.RejectedError) return { type: "aborted", message: cause.message } + if (cause instanceof ToolFailure || cause instanceof Tool.Error) { if (cause.error === undefined) return { type: "tool.execution", message: cause.message } // The canonical error is the sole model-visible representation, so a cause // with no message must not erase the tool's curated failure message. @@ -57,6 +56,5 @@ export function toSessionError(cause: unknown): SessionError.Error { ) return { type: "provider.no-route", message: cause.message } if (cause instanceof Integration.AuthorizationError) return { type: "provider.auth", message: cause.message } - if (cause instanceof ToolOutputStore.StorageError) return { type: "unknown", message: cause.message } return { type: "unknown", message: cause instanceof Error ? cause.message : String(cause) } } diff --git a/packages/core/src/session/usage.ts b/packages/core/src/session/usage.ts index 33db0926e114..dd38e948aaef 100644 --- a/packages/core/src/session/usage.ts +++ b/packages/core/src/session/usage.ts @@ -3,7 +3,7 @@ export * as SessionUsage from "./usage" import type { Usage } from "@opencode-ai/ai" import { Money } from "@opencode-ai/schema/money" import type { TokenUsage } from "@opencode-ai/schema/token-usage" -import type { ModelV2 } from "../model" +import type { Model } from "../model" const safe = (value: number | undefined) => Math.max(0, Number.isFinite(value) ? (value ?? 0) : 0) @@ -18,7 +18,7 @@ export const tokens = (usage: Usage | undefined): TokenUsage.Info => ({ }) // TODO(#35765): Use Copilot's reported billed amount once billing has a dedicated typed runtime contract. -export function calculateCost(costs: ModelV2.Info["cost"], usage: TokenUsage.Info) { +export function calculateCost(costs: Model.Info["cost"], usage: TokenUsage.Info) { const context = usage.input + usage.cache.read + usage.cache.write const tier = costs .filter((cost) => cost.tier?.type === "context" && context > cost.tier.size) @@ -36,7 +36,7 @@ export function calculateCost(costs: ModelV2.Info["cost"], usage: TokenUsage.Inf export type Recorded = { readonly tokens: TokenUsage.Info; readonly cost: Money.USD } -export const record = (usage: Usage | undefined, costs: ModelV2.Info["cost"]): Recorded => { +export const record = (usage: Usage | undefined, costs: Model.Info["cost"]): Recorded => { const normalized = tokens(usage) return { tokens: normalized, cost: calculateCost(costs, normalized) } } diff --git a/packages/core/src/shell.ts b/packages/core/src/shell.ts index 4a2be872bf5e..1c851a154063 100644 --- a/packages/core/src/shell.ts +++ b/packages/core/src/shell.ts @@ -8,7 +8,7 @@ import { Shell } from "@opencode-ai/schema/shell" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { AppProcess } from "@opencode-ai/util/process" import { Config } from "./config" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { Location } from "./location" import { Global } from "@opencode-ai/util/global" import { ShellSelect } from "./shell/select" @@ -57,12 +57,12 @@ export interface Interface { readonly remove: (id: Shell.ID) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Shell") {} +export class Service extends Context.Service()("@opencode/Shell") {} export const layer = (options?: ShellSelect.Options) => Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const location = yield* Location.Service const config = yield* Config.Service const global = yield* Global.Service @@ -105,7 +105,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( // Unblock any wait still pending when the command is removed before it terminated. yield* Deferred.fail(session.done, new NotFoundError({ id })) yield* Effect.promise(() => unlink(session.file).catch(() => {})) - yield* events.publish(Shell.Event.Deleted, { id }) + yield* bus.publish(Shell.Event.Deleted, { id }) }) const remove = Effect.fn("Shell.remove")(function* (id: Shell.ID) { @@ -259,7 +259,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( // the timeout-fiber interrupt below, which on the timeout path would otherwise cancel // this very fiber (finish is invoked by the timeout fiber) before waiters are resolved. yield* Deferred.succeed(session.done, session.info) - yield* events.publish(Shell.Event.Exited, { + yield* bus.publish(Shell.Event.Exited, { id, ...(exit !== undefined ? { exit } : {}), status, @@ -299,7 +299,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( ), ) - yield* events.publish(Shell.Event.Created, { info }) + yield* bus.publish(Shell.Event.Created, { info }) yield* Deferred.succeed(ready, session) // Hold the handle's scope open until the command terminates; closing it earlier would // release (kill) the process before its exit is observed. @@ -320,7 +320,7 @@ export function configured(options?: ShellSelect.Options) { return makeLocationNode({ service: Service, layer: layer(options), - deps: [EventV2.node, Location.node, Config.node, Global.node, AppProcess.node], + deps: [Bus.node, Location.node, Config.node, Global.node, AppProcess.node], }) } diff --git a/packages/core/src/skill.ts b/packages/core/src/skill.ts index c66fb4f46c49..521d3d6eb553 100644 --- a/packages/core/src/skill.ts +++ b/packages/core/src/skill.ts @@ -1,15 +1,15 @@ -export * as SkillV2 from "./skill" +export * as Skill from "./skill" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import path from "path" import { Context, Effect, Layer, Schema, Stream, Types } from "effect" import { FileSystem } from "@opencode-ai/schema/filesystem" import { Skill } from "@opencode-ai/schema/skill" -import { AgentV2 } from "./agent" +import { Agent } from "./agent" import { ConfigMarkdown } from "./config/markdown" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { FSUtil } from "@opencode-ai/util/fs-util" -import { PermissionV2 } from "./permission" +import { Permission } from "./permission" import { AbsolutePath } from "./schema" import { SkillDiscovery } from "./skill/discovery" import { State } from "./state" @@ -33,10 +33,10 @@ export type ID = Skill.ID export const Name = Skill.Name export type Name = Skill.Name -export const Event = Skill.Event +export { Event } from "@opencode-ai/schema/skill" -export const available = (skills: ReadonlyArray, agent: AgentV2.Info) => - skills.filter((skill) => PermissionV2.evaluate("skill", skill.id, agent.permissions).effect !== "deny") +export const available = (skills: ReadonlyArray, agent: Agent.Info) => + skills.filter((skill) => Permission.evaluate("skill", skill.id, agent.permissions).effect !== "deny") const Frontmatter = Schema.Struct({ name: Schema.String.pipe(Schema.optional), @@ -73,14 +73,14 @@ export interface Interface extends State.Transformable { readonly list: () => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Skill") {} +export class Service extends Context.Service()("@opencode/Skill") {} const layer = Layer.effect( Service, Effect.gen(function* () { const discovery = yield* SkillDiscovery.Service const fs = yield* FSUtil.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const state = State.create({ name: "skill", @@ -92,10 +92,10 @@ const layer = Layer.effect( }, list: () => draft.sources as Source[], }), - finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid), + finalize: () => bus.publish(Skill.Event.Updated, {}).pipe(Effect.asVoid), }) - const load = Effect.fn("SkillV2.load")(function* (source: Source) { + const load = Effect.fn("Skill.load")(function* (source: Source) { const skills: Info[] = [] if (source.type === "embedded") { yield* Effect.logDebug("skill source loaded", { @@ -143,7 +143,7 @@ const layer = Layer.effect( }) const cache = new Map() - const invalidate = Effect.fn("SkillV2.invalidateFromWatcher")(function* (file: string) { + const invalidate = Effect.fn("Skill.invalidateFromWatcher")(function* (file: string) { const invalidated = Array.from(cache.entries()).filter(([, loaded]) => loaded.directories.some((directory) => FSUtil.contains(directory, file)), ) @@ -154,15 +154,15 @@ const layer = Layer.effect( sources: invalidated.map(([key]) => key), skills: invalidated.flatMap(([, loaded]) => loaded.skills.map((skill) => skill.id)), }) - yield* events.publish(Event.Updated, {}).pipe(Effect.asVoid) + yield* bus.publish(Skill.Event.Updated, {}).pipe(Effect.asVoid) }) - yield* events.subscribe(FileSystem.Event.Changed).pipe( + yield* bus.subscribe(FileSystem.Event.Changed).pipe( Stream.runForEach((event) => invalidate(event.data.file)), Effect.forkScoped({ startImmediately: true }), ) - const list = Effect.fn("SkillV2.list")(function* () { + const list = Effect.fn("Skill.list")(function* () { const skills = new Map() for (const source of state.get().sources) { const key = Source.key(source) @@ -176,7 +176,7 @@ const layer = Layer.effect( return Service.of({ transform: state.transform, reload: state.reload, - sources: Effect.fn("SkillV2.sources")(function* () { + sources: Effect.fn("Skill.sources")(function* () { return state.get().sources }), list, @@ -187,5 +187,5 @@ const layer = Layer.effect( export const node = makeLocationNode({ service: Service, layer, - deps: [SkillDiscovery.node, FSUtil.node, EventV2.node], + deps: [SkillDiscovery.node, FSUtil.node, Bus.node], }) diff --git a/packages/core/src/skill/discovery.ts b/packages/core/src/skill/discovery.ts index 074434ec61db..2a21a4c86398 100644 --- a/packages/core/src/skill/discovery.ts +++ b/packages/core/src/skill/discovery.ts @@ -67,7 +67,7 @@ export interface Interface { readonly pull: (url: string) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/SkillDiscovery") {} +export class Service extends Context.Service()("@opencode/SkillDiscovery") {} const layer = Layer.effect( Service, diff --git a/packages/core/src/skill/instructions.ts b/packages/core/src/skill/instructions.ts index 382fede3b581..382987c0249a 100644 --- a/packages/core/src/skill/instructions.ts +++ b/packages/core/src/skill/instructions.ts @@ -2,13 +2,13 @@ export * as SkillInstructions from "./instructions" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Context, Effect, Layer, Schema } from "effect" -import { AgentV2 } from "../agent" -import { SkillV2 } from "../skill" +import { Agent } from "../agent" +import { Skill } from "../skill" import { Instructions } from "../instructions/index" const Summary = Schema.Struct({ - id: SkillV2.ID, - name: SkillV2.Name, + id: Skill.ID, + name: Skill.Name, description: Schema.String, }) type Summary = typeof Summary.Type @@ -57,21 +57,21 @@ const update = (previous: ReadonlyArray, current: ReadonlyArray Effect.Effect + readonly load: (agent: Agent.Selection) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/SkillInstructions") {} +export class Service extends Context.Service()("@opencode/SkillInstructions") {} const layer = Layer.effect( Service, Effect.gen(function* () { - const skills = yield* SkillV2.Service + const skills = yield* Skill.Service return Service.of({ load: Effect.fn("SkillInstructions.load")(function* (selection) { const agent = selection.info if (!agent) return Instructions.empty - const permitted = SkillV2.available(yield* skills.list(), agent) + const permitted = Skill.available(yield* skills.list(), agent) const available = permitted .flatMap((skill) => skill.description === undefined || skill.autoinvoke === false @@ -94,4 +94,4 @@ const layer = Layer.effect( }), ) -export const node = makeLocationNode({ service: Service, layer, deps: [SkillV2.node] }) +export const node = makeLocationNode({ service: Service, layer, deps: [Skill.node] }) diff --git a/packages/core/src/snapshot.ts b/packages/core/src/snapshot.ts index 72982fbd90ae..5113b3f4c6f4 100644 --- a/packages/core/src/snapshot.ts +++ b/packages/core/src/snapshot.ts @@ -81,7 +81,7 @@ export interface Interface { readonly checkout: (snapshot: ID) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Snapshot") {} +export class Service extends Context.Service()("@opencode/Snapshot") {} const layer = Layer.effect( Service, diff --git a/packages/core/src/tool-output-store.ts b/packages/core/src/tool-output-store.ts deleted file mode 100644 index d59a342159fd..000000000000 --- a/packages/core/src/tool-output-store.ts +++ /dev/null @@ -1,201 +0,0 @@ -export * as ToolOutputStore from "./tool-output-store" - -import path from "path" -import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect" -import { Config } from "./config" -import { FSUtil } from "@opencode-ai/util/fs-util" -import { Global } from "@opencode-ai/util/global" -import { makeGlobalNode, makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { SessionSchema } from "./session/schema" -import { Identifier } from "./util/identifier" -import type { ToolContent } from "@opencode-ai/ai" - -export const MAX_LINES = 2_000 -export const MAX_BYTES = 50 * 1024 -export const RETENTION = Duration.days(7) - -export const MANAGED_DIRECTORY = "tool-output" - -export interface BoundInput { - readonly sessionID: SessionSchema.ID - readonly callID: string - readonly content: ReadonlyArray -} - -export interface BoundResult { - readonly content: ReadonlyArray - readonly outputPaths: ReadonlyArray -} - -export class StorageError extends Schema.TaggedErrorClass()("ToolOutputStore.StorageError", { - operation: Schema.Literals(["encode", "write"]), - cause: Schema.Defect(), -}) { - override get message() { - const detail = this.cause instanceof Error ? this.cause.message : String(this.cause) - return `Failed to ${this.operation} tool output${detail ? `: ${detail}` : ""}` - } -} - -export type Error = StorageError - -export interface Interface { - readonly limits: () => Effect.Effect<{ readonly maxLines: number; readonly maxBytes: number }> - readonly bound: (input: BoundInput) => Effect.Effect - readonly cleanup: () => Effect.Effect -} - -export class Service extends Context.Service()("@opencode/v2/ToolOutputStore") {} - -const takePrefix = (input: string, maximumBytes: number) => { - let bytes = 0 - let content = "" - for (const char of input) { - const size = Buffer.byteLength(char, "utf-8") - if (bytes + size > maximumBytes) break - content += char - bytes += size - } - return content -} - -const takeSuffix = (input: string, maximumBytes: number) => { - let bytes = 0 - const content: string[] = [] - for (const char of Array.from(input).toReversed()) { - const size = Buffer.byteLength(char, "utf-8") - if (bytes + size > maximumBytes) break - content.unshift(char) - bytes += size - } - return content.join("") -} - -const preview = (text: string, maxLines: number, maxBytes: number) => { - const lines = text.split("\n") - const headLines = Math.ceil(maxLines / 2) - const tailLines = Math.floor(maxLines / 2) - const sampled = - lines.length <= maxLines - ? text - : [ - lines.slice(0, headLines).join("\n"), - ...(tailLines > 0 ? [lines.slice(lines.length - tailLines).join("\n")] : []), - ].join("\n") - if (Buffer.byteLength(sampled, "utf-8") <= maxBytes) { - return lines.length <= maxLines - ? { head: sampled, tail: "" } - : { - head: lines.slice(0, headLines).join("\n"), - tail: tailLines > 0 ? lines.slice(lines.length - tailLines).join("\n") : "", - } - } - const headBytes = Math.ceil(maxBytes / 2) - const tailBytes = Math.floor(maxBytes / 2) - return { head: takePrefix(sampled, headBytes), tail: takeSuffix(sampled, tailBytes) } -} - -const boundedPreview = (text: string, marker: string, maxLines: number, maxBytes: number) => { - const markerOnly = takePrefix(marker, maxBytes).split("\n").slice(0, maxLines).join("\n") - const markerBytes = Buffer.byteLength(marker, "utf-8") - if (maxLines <= 4 || maxBytes <= markerBytes + 4) return markerOnly - const bounded = preview(text, maxLines - 4, maxBytes - markerBytes - 4) - return bounded.tail ? `${bounded.head}\n\n${marker}\n\n${bounded.tail}` : `${bounded.head}\n\n${marker}` -} - -const lineCount = (text: string) => { - let count = 1 - for (const char of text) if (char === "\n") count++ - return count -} - -const layer = Layer.effect( - Service, - Effect.gen(function* () { - const fs = yield* FSUtil.Service - const global = yield* Global.Service - const config = yield* Effect.serviceOption(Config.Service) - const directory = path.join(global.data, MANAGED_DIRECTORY) - const limits = Effect.fn("ToolOutputStore.limits")(function* () { - if (Option.isNone(config)) return { maxLines: MAX_LINES, maxBytes: MAX_BYTES } - const entries = yield* config.value.entries().pipe(Effect.catch(() => Effect.succeed([] as Config.Entry[]))) - const configured = Object.assign( - {}, - ...entries.flatMap((entry) => (entry.type === "document" ? [entry.info.tool_output ?? {}] : [])), - ) - return { maxLines: configured.max_lines ?? MAX_LINES, maxBytes: configured.max_bytes ?? MAX_BYTES } - }) - - const write = Effect.fn("ToolOutputStore.write")(function* (content: string) { - const file = path.join(directory, `tool_${Identifier.ascending()}`) - yield* fs.ensureDir(directory).pipe(Effect.mapError((cause) => new StorageError({ operation: "write", cause }))) - yield* fs - .writeFileString(file, content, { flag: "wx" }) - .pipe(Effect.mapError((cause) => new StorageError({ operation: "write", cause }))) - return file - }) - - const bound = Effect.fn("ToolOutputStore.bound")(function* (input: BoundInput) { - const outputLimits = yield* limits() - const media = input.content.filter((item) => item.type === "file") - const contextual = input.content.flatMap((item) => (item.type === "text" ? [item.text] : [])).join("") - if ( - lineCount(contextual) <= outputLimits.maxLines && - Buffer.byteLength(contextual, "utf-8") <= outputLimits.maxBytes - ) - return { - content: input.content, - outputPaths: [], - } - - const outputPath = yield* write(contextual) - const marker = `... output truncated; full content saved to ${outputPath} ...` - - return { - content: [ - { - type: "text" as const, - text: boundedPreview(contextual, marker, outputLimits.maxLines, outputLimits.maxBytes), - }, - ...media, - ], - outputPaths: [outputPath], - } - }) - - const cleanup = Effect.fn("ToolOutputStore.cleanup")(function* () { - const entries = yield* fs.readDirectory(directory).pipe(Effect.catch(() => Effect.succeed([]))) - const cutoff = Date.now() - Duration.toMillis(RETENTION) - for (const entry of entries) { - if (!entry.startsWith("tool_")) continue - const file = path.join(directory, entry) - const info = yield* fs.stat(file).pipe(Effect.catch(() => Effect.void)) - const modified = info?.mtime.pipe( - Option.map((date) => date.getTime()), - Option.getOrElse(() => 0), - ) - if (modified !== undefined && modified < cutoff) yield* fs.remove(file).pipe(Effect.catch(() => Effect.void)) - } - }) - - return Service.of({ limits, bound, cleanup }) - }), -) - -export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Global.node, Config.node] }) - -export const nodeWithoutConfig = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Global.node] }) - -/** Runs retention scanning once globally rather than once per active Location. */ -export const cleanupLayer = Layer.effectDiscard( - Effect.gen(function* () { - const store = yield* Service - yield* store.cleanup().pipe(Effect.repeat(Schedule.spaced(Duration.hours(1))), Effect.forkScoped) - }), -) - -export const cleanupNode = makeGlobalNode({ - name: "tool-output-cleanup", - layer: Layer.merge(layer, cleanupLayer.pipe(Layer.provide(layer))), - deps: [FSUtil.node, Global.node], -}) diff --git a/packages/core/src/tool.ts b/packages/core/src/tool.ts new file mode 100644 index 000000000000..43bc900c46a4 --- /dev/null +++ b/packages/core/src/tool.ts @@ -0,0 +1,285 @@ +export * as Tool from "./tool.js" +export { CallID, Content, Error, FileContent, TextContent } from "@opencode-ai/schema/tool" +export type { Context, Metadata, Options, Result } from "@opencode-ai/schema/tool" + +import type { ToolCall, ToolDefinition } from "@opencode-ai/ai" +import { Tool } from "@opencode-ai/schema/tool" +import { Context, Effect, Layer, Schema, Scope, Semaphore } from "effect" +import { makeLocationNode } from "@opencode-ai/util/effect/app-node" +import type { Agent } from "./agent" +import { CodeModeCatalog } from "./codemode/catalog" +import { CodeModeTool } from "./codemode/tool" +import { Image } from "./image" +import { Permission } from "./permission" +import { PluginHooks } from "./plugin/hooks" +import { SessionMessage } from "./session/message" +import { SessionSchema } from "./session/schema" +import { definition, execute, normalizeContent } from "./tool/runtime" +import { Wildcard } from "./util/wildcard" + +export class RegistrationError extends Schema.TaggedErrorClass()("Tool.RegistrationError", { + name: Schema.String, + message: Schema.String, +}) {} + +export interface Interface { + readonly transform: ( + callback: (draft: { readonly add: (tool: Tool.Info) => void }) => void, + ) => Effect.Effect + readonly snapshot: (permissions?: Permission.Ruleset) => Effect.Effect +} + +export interface Snapshot { + readonly definitions: ReadonlyArray + readonly codeModeCatalog?: ReadonlyArray + readonly execute: (input: { + readonly sessionID: SessionSchema.ID + readonly agent: Agent.ID + readonly messageID: SessionMessage.ID + readonly call: ToolCall + readonly progress?: (update: Tool.Metadata) => Effect.Effect + }) => Effect.Effect }, Tool.Error> +} + +export class Service extends Context.Service()("@opencode/Tool") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const hooks = yield* PluginHooks.Service + const image = yield* Image.Service + + type NormalizedItem = Tool.Content | "decode" | "size" + const normalizeImages = Effect.fn("Tool.normalizeImages")(function* (content: ReadonlyArray) { + const normalized = yield* Effect.forEach(content, (item): Effect.Effect => { + if (item.type !== "file" || !item.mime.startsWith("image/")) return Effect.succeed(item) + const base64 = /^data:[^,]*;base64,(.*)$/s.exec(item.uri)?.[1] + if (base64 === undefined) return Effect.succeed(item) + const resource = item.name ?? `${item.mime} tool output` + return image.normalize(resource, { uri: resource, content: base64, encoding: "base64", mime: item.mime }).pipe( + Effect.map((result) => ({ + ...item, + uri: `data:${result.mime};base64,${result.content}`, + mime: result.mime, + })), + Effect.catchTag("Image.ResizerUnavailableError", () => Effect.succeed(item)), + Effect.catchTag("Image.DecodeError", () => Effect.succeed("decode" as const)), + Effect.catchTag("Image.SizeError", () => Effect.succeed("size" as const)), + ) + }) + const note = (reason: "decode" | "size", text: string) => { + const count = normalized.filter((item) => item === reason).length + if (count === 0) return [] + return [{ type: "text" as const, text: `[${count} image${count === 1 ? "" : "s"} omitted: ${text}]` }] + } + return [ + ...normalized.filter((item) => typeof item !== "string"), + ...note("decode", "could not be decoded."), + ...note("size", "could not be resized below the image size limit."), + ] + }) + + const local = new Map>() + const lock = Semaphore.makeUnsafe(1) + + const executeTool = Effect.fn("Tool.execute")(function* ( + tool: Tool.Info, + name: string, + input: unknown, + context: Tool.Context, + ) { + const beforeEvent: PluginHooks.Domains["tool"]["execute.before"] = { + tool: name, + sessionID: context.sessionID, + agent: context.agent, + messageID: context.messageID, + callID: context.callID, + input, + } + yield* hooks.trigger("tool", "execute.before", beforeEvent) + const execution = yield* execute(tool, beforeEvent.input, context).pipe( + Effect.map((value) => ({ value })), + Effect.catchTag("Tool.Error", (failure) => Effect.succeed({ failure })), + ) + const base = { + tool: name, + sessionID: context.sessionID, + agent: context.agent, + messageID: context.messageID, + callID: context.callID, + input: beforeEvent.input, + } + if ("failure" in execution) { + const afterEvent: PluginHooks.Domains["tool"]["execute.after"] = { + ...base, + status: "error", + error: execution.failure, + } + yield* hooks.trigger("tool", "execute.after", afterEvent) + return yield* afterEvent.error + } + const content = yield* normalizeImages(execution.value.content) + const afterEvent: PluginHooks.Domains["tool"]["execute.after"] = { + ...base, + status: "completed", + result: { + ...(execution.value.output === undefined ? {} : { output: execution.value.output }), + content: content.length > 0 ? content : execution.value.content, + ...(execution.value.metadata === undefined ? {} : { metadata: execution.value.metadata }), + }, + } + yield* hooks.trigger("tool", "execute.after", afterEvent) + const afterContent = yield* normalizeImages(normalizeContent(afterEvent.result.content, afterEvent.result.output)) + return { + ...(afterEvent.result.output === undefined ? {} : { output: afterEvent.result.output }), + content: afterContent, + ...(afterEvent.result.metadata === undefined ? {} : { metadata: afterEvent.result.metadata }), + } + }) + + const transform: Interface["transform"] = Effect.fn("Tool.transform")(function* (callback) { + const tools: Array = [] + yield* Effect.sync(() => callback({ add: (tool) => tools.push(tool) })) + yield* Effect.forEach( + tools.flatMap((tool) => (tool.options?.namespace === undefined ? [] : [tool.options.namespace])), + validateNamespace, + { discard: true }, + ) + const entries = normalizedEntries(tools) + yield* Effect.forEach(entries, (entry) => validateName(normalizedName(entry.tool)), { discard: true }) + const collision = entries.find( + (entry, index) => entries.findIndex((candidate) => candidate.key === entry.key) !== index, + ) + if (collision) + return yield* Effect.fail( + new RegistrationError({ + name: collision.key, + message: `Duplicate normalized tool name: ${collision.key}`, + }), + ) + const reserved = entries.find((entry) => entry.tool.options?.codemode === false && entry.key === "execute") + if (reserved) + return yield* Effect.fail( + new RegistrationError({ + name: reserved.key, + message: 'Tool name "execute" is reserved for CodeMode', + }), + ) + if (entries.length === 0) return + yield* Effect.uninterruptible( + lock.withPermit( + Effect.gen(function* () { + const token = {} + for (const entry of entries) + local.set(entry.key, [...(local.get(entry.key) ?? []), { token, tool: entry.tool }]) + yield* Effect.addFinalizer(() => + lock.withPermit( + Effect.sync(() => { + for (const entry of entries) { + const remaining = local.get(entry.key)?.filter((item) => item.token !== token) ?? [] + if (remaining.length > 0) local.set(entry.key, remaining) + else local.delete(entry.key) + } + }), + ), + ) + }), + ), + ) + }) + + return Service.of({ + transform, + snapshot: Effect.fn("Tool.snapshot")((permissions) => + lock.withPermit( + Effect.gen(function* () { + const active = new Map() + const rules = permissions ?? [] + for (const [name, entries] of local) { + const tool = entries.at(-1)?.tool + if (!tool) continue + if (whollyDisabled(tool.options?.permission ?? name, rules)) continue + active.set(name, tool) + } + const direct = new Map(Array.from(active).filter(([, tool]) => tool.options?.codemode === false)) + const codemode = new Map(Array.from(active).filter(([, tool]) => tool.options?.codemode !== false)) + const executeRule = rules.findLast((rule) => Wildcard.match("execute", rule.action)) + const codemodeEnabled = executeRule?.resource !== "*" || executeRule.effect !== "deny" + const codemodeTool = codemodeEnabled + ? CodeModeTool.create(codemode, (name, tool, input, context) => executeTool(tool, name, input, context)) + : undefined + const codeModeCatalog = codemodeEnabled ? CodeModeTool.catalog(codemode) : undefined + return { + ...(codeModeCatalog === undefined ? {} : { codeModeCatalog }), + definitions: [ + ...Array.from(direct) + .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)) + .map(([, tool]) => definition(tool)), + ...(codemodeTool ? [definition(codemodeTool)] : []), + ], + execute: (input: { + readonly sessionID: SessionSchema.ID + readonly agent: Agent.ID + readonly messageID: SessionMessage.ID + readonly call: ToolCall + readonly progress?: (update: Tool.Metadata) => Effect.Effect + }) => { + const context: Tool.Context = { + sessionID: input.sessionID, + agent: input.agent, + messageID: input.messageID, + callID: Tool.CallID.make(input.call.id), + progress: input.progress ?? (() => Effect.void), + } + if (input.call.name === "execute" && codemodeTool) + return executeTool(codemodeTool, input.call.name, input.call.input, context) + const tool = direct.get(input.call.name) + if (tool) return executeTool(tool, input.call.name, input.call.input, context) + return new Tool.Error({ message: `Unknown tool: ${input.call.name}` }) + }, + } + }), + ), + ), + }) + }), +) + +const whollyDisabled = (action: string, rules: Permission.Ruleset) => { + const rule = rules.findLast((rule) => Wildcard.match(action, rule.action)) + return rule?.resource === "*" && rule.effect === "deny" +} + +const validateName = (name: string) => + /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(name) + ? Effect.void + : Effect.fail(new RegistrationError({ name, message: `Invalid tool name: ${name}` })) + +const validateNamespace = (namespace: string) => + namespace.split(".").every((segment) => /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(segment)) + ? Effect.void + : Effect.fail( + new RegistrationError({ + name: namespace, + message: `Invalid tool namespace: ${JSON.stringify(namespace)}`, + }), + ) + +const normalizedName = (tool: Tool.Info) => tool.name.replace(/[^a-zA-Z0-9_-]/g, "_") + +const effectiveName = (tool: Tool.Info) => + tool.options?.namespace === undefined + ? normalizedName(tool) + : `${tool.options.namespace.replaceAll(".", "_")}_${normalizedName(tool)}` + +const normalizedEntries = (tools: ReadonlyArray) => + tools.map((tool) => ({ + key: effectiveName(tool), + tool, + })) + +export const node = makeLocationNode({ + service: Service, + layer, + deps: [PluginHooks.node, Image.node], +}) diff --git a/packages/core/src/tool/AGENTS.md b/packages/core/src/tool/AGENTS.md index 2c1e9292bc09..a6bc1541dfc9 100644 --- a/packages/core/src/tool/AGENTS.md +++ b/packages/core/src/tool/AGENTS.md @@ -1,20 +1,22 @@ # Core Tool Architecture -This folder owns Core's local tools, Location-scoped registrations, effective lookup, execution, and terminal outcomes. +`src/tool.ts` owns the Location-scoped tool service, registrations, effective lookup, execution, and terminal outcomes. This folder contains its supporting runtime modules and built-in plugins. ## Representations -- `tool.ts` defines the structural canonical `Tool.make({ description, input, output?, execute })` tool. Executors return model content and metadata alongside declared machine output. Shipped built-ins and plugin tools use the same type. -- `tools.ts` exposes the registration-only `Tools.Service` view used by Location producers. -- `registry.ts` stores only canonical Location registrations, derives LLM definitions, executes tools, and applies generic output bounding. +- Plugin authors get schema-derived input types at the `ToolDraft.add` boundary through `Tool`. +- The heterogeneous Core registry deliberately erases registered definitions to `Tool.Info`. Use `any` at this internal boundary; do not replace it with `unknown`, JSON-value plumbing, casts, or compiled wrapper types solely to preserve type safety after registration. +- Executors return model content and metadata alongside declared machine output. Shipped built-ins and plugin tools use the same runtime shape after registration. +- `src/tool.ts` stores canonical Location registrations, derives LLM definitions, executes tools, and applies generic output bounding. +- Built-in tool plugins live in `tool/plugin`. Do not add a second executable entry type, registry-owned executor, authorization callback, output-path callback, or legacy normalization path. ## Construction -Tool schemas use `input` and `output` terminology. A tool carries schemas and executable behavior without public identity. A registration binds its name, namespace, CodeMode placement, and optional catalog permission action. +Tool schemas use `input` and `output` terminology. Each tool carries its name, options, schemas, and executable behavior in one object. -Location-scoped built-in layers acquire `PermissionV2.Service` and every other required Location service while the layer is constructed. The executor captures those services. Permission sources are always constructed from the canonical invocation context: +Location-scoped built-in layers acquire `Permission.Service` and every other required Location service while the layer is constructed. The executor captures those services. Permission sources are always constructed from the canonical invocation context: ```ts const source = { @@ -24,13 +26,11 @@ const source = { } ``` -Leaves own resolution, permission, and side-effect ordering. Translate only expected typed errors into `ToolFailure`; do not use `catchCause`, because interruption and defects must survive. User declines from `PermissionV2.assert` and question dismissals travel as defects beneath leaf `mapError` blankets and resurface as typed failures at `SessionModelRequest.executeTool`; leaves must never catch or convert them. A decline with feedback (`PermissionV2.CorrectedError`) stays typed so the leaf converts it into `ToolFailure` and the model continues. +Leaves own resolution, permission, and side-effect ordering. Translate only expected typed errors into `ToolFailure`; do not use `catchCause`, because interruption and defects must survive. User declines from `Permission.assert` and question dismissals travel as defects beneath leaf `mapError` blankets and resurface as typed failures at `SessionModelRequest.executeTool`; leaves must never catch or convert them. A decline with feedback (`Permission.CorrectedError`) stays typed so the leaf converts it into `ToolFailure` and the model continues. ## Registration -Built-ins and plugin tools register through `Tools.Service.register({ [name]: tool })`. Registrations may provide a -namespace, which flattens direct model names to `_`, and default into CodeMode (`codemode` defaults true; -`codemode: false` keeps the tool on the provider's native tool list). +Built-ins, plugins, and MCP install tools through `ToolRegistry.Service.transform`, adding complete tool objects to the draft. A tool may provide a namespace, which flattens direct model names to `_`, and defaults into CodeMode (`codemode` defaults true; `codemode: false` keeps the tool on the provider's native tool list). Registrations are scoped: @@ -38,21 +38,22 @@ Registrations are scoped: - Closing any registration removes only that registration and reveals the next active one. - Each model request captures the effective tools it advertises; later registration changes affect later requests. +Type safety ends at registration. The registry validates model input and declared output at runtime and should not carry producer schema generics through storage or execution. + `ToolRegistry.Service` is Location-scoped. Do not make the registry process-global or construct a separate application-tool service for each Location. ## Permissions -The registry has no `PermissionV2.Service` dependency and performs no execution authorization. Registration options may attach a permission action solely to preserve whole-tool definition filtering. Most registrations default to their effective name; `edit`, `write`, and `patch` use the shared `edit` action. +The registry has no `Permission.Service` dependency and performs no execution authorization. Registration options may attach a permission action solely to preserve whole-tool definition filtering. Most registrations default to their effective name; `edit`, `write`, and `patch` use the shared `edit` action. Tool filtering is catalog visibility, not execution authorization. A call still executes the captured tool's leaf policy if it reaches execution. ## Output -Built-ins return complete tool responses. `ToolRegistry.ToolSet.execute` is the only local execution and generic model-output bounding boundary and owns managed retention paths. +Built-ins return complete tool responses. `Tool.Snapshot.execute` is the local execution boundary. -Producer capture limits are separate. For example, Bash keeps `AppProcess.maxOutputBytes` and accurately reports stdout/stderr capture loss, but it does not run model-output truncation or return a managed `outputPath`. +Producer capture limits remain local to producers. For example, Bash keeps `AppProcess.maxOutputBytes` and accurately reports stdout/stderr capture loss. ## Current Gaps - MCP and future Session-scoped registrations still need an explicit canonical registration design. -- The public Session result shape currently exposes managed `outputPaths`; full storage encapsulation requires a future opaque managed-output reference design. diff --git a/packages/core/src/tool/hooks.ts b/packages/core/src/tool/hooks.ts deleted file mode 100644 index 2e6cf32942aa..000000000000 --- a/packages/core/src/tool/hooks.ts +++ /dev/null @@ -1,79 +0,0 @@ -export * as ToolHooks from "./hooks" - -import { makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { State } from "../state" -import { Context, Effect, Layer, Scope } from "effect" -import type { Tool } from "./tool" - -export type BeforeEvent = Tool.ToolExecuteBeforeEvent - -/** The canonical execution outcome. Hooks never observe the raw domain output. */ -export type AfterEvent = Tool.ToolExecuteAfterEvent - -export interface Interface { - readonly hook: { - readonly before: ( - callback: (event: BeforeEvent) => Effect.Effect | void, - ) => Effect.Effect - readonly after: ( - callback: (event: AfterEvent) => Effect.Effect | void, - ) => Effect.Effect - } - readonly runBefore: (event: BeforeEvent) => Effect.Effect - readonly runAfter: (event: AfterEvent) => Effect.Effect -} - -export class Service extends Context.Service()("@opencode/v2/ToolHooks") {} - -const layer = Layer.effect( - Service, - Effect.gen(function* () { - let beforeHooks: ((event: BeforeEvent) => Effect.Effect | void)[] = [] - let afterHooks: ((event: AfterEvent) => Effect.Effect | void)[] = [] - - const register = ( - hooks: () => ((event: Event) => Effect.Effect | void)[], - update: (hooks: ((event: Event) => Effect.Effect | void)[]) => void, - ) => - Effect.fn("ToolHooks.hook")(function* (callback: (event: Event) => Effect.Effect | void) { - const scope = yield* Scope.Scope - let active = true - update([...hooks(), callback]) - const dispose = Effect.sync(() => { - if (!active) return - active = false - update(hooks().filter((item) => item !== callback)) - }) - yield* Scope.addFinalizer(scope, dispose) - return { dispose } - }) - - const run = Effect.fnUntraced(function* ( - hooks: readonly ((event: Event) => Effect.Effect | void)[], - event: Event, - ) { - for (const hook of hooks) { - const result = hook(event) - if (Effect.isEffect(result)) yield* result - } - return event - }) - - return Service.of({ - hook: { - before: register( - () => beforeHooks, - (next) => (beforeHooks = next), - ), - after: register( - () => afterHooks, - (next) => (afterHooks = next), - ), - }, - runBefore: (event) => run(beforeHooks, event), - runAfter: (event) => run(afterHooks, event), - }) - }), -) - -export const node = makeLocationNode({ service: Service, layer, deps: [] }) diff --git a/packages/core/src/tool/mcp.ts b/packages/core/src/tool/mcp.ts index 24b10f79dadc..d82e45ae33e0 100644 --- a/packages/core/src/tool/mcp.ts +++ b/packages/core/src/tool/mcp.ts @@ -4,13 +4,11 @@ import { ToolFailure } from "@opencode-ai/ai" import { McpEvent } from "@opencode-ai/schema/mcp-event" import { Effect, Exit, type JsonSchema, Layer, Scope, Semaphore, Stream } from "effect" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { MCP } from "../mcp" -import { PermissionV2 } from "../permission" -import { Tool } from "./tool" -import { Tools } from "./tools" -import { ToolRegistry } from "./registry" +import { Permission } from "../permission" +import { Tool } from "../tool" /** * Registry namespace and permission action names for MCP tools. @@ -21,9 +19,9 @@ export const name = (server: string, tool: string) => `${namespace(server)}_${to export const layer = Layer.effectDiscard( Effect.gen(function* () { const mcp = yield* MCP.Service - const tools = yield* Tools.Service - const events = yield* EventV2.Service - const permission = yield* PermissionV2.Service + const tools = yield* Tool.Service + const bus = yield* Bus.Service + const permission = yield* Permission.Service const scope = yield* Scope.Scope const lock = Semaphore.makeUnsafe(1) let current: Scope.Closeable | undefined @@ -32,27 +30,25 @@ export const layer = Layer.effectDiscard( // registry never has a gap where MCP tools disappear mid-swap. const reconcile = lock.withPermit( Effect.gen(function* () { - const groups = new Map< - string, - { - tools: Record - codemode: boolean - } - >() - for (const tool of yield* mcp.tools()) { - const group = groups.get(tool.server) ?? { tools: {}, codemode: tool.codemode !== false } - const schema = (tool.inputSchema ?? {}) as JsonSchema.JsonSchema - group.tools[tool.name] = Tool.make({ - description: tool.description ?? "", - input: { - ...schema, - type: "object", - properties: schema.properties ?? {}, - additionalProperties: false, - }, - output: (tool.outputSchema ?? {}) as JsonSchema.JsonSchema, - execute: (input, context) => - Effect.gen(function* () { + const discovered = yield* mcp.tools() + const next = yield* Scope.fork(scope) + yield* tools + .transform((draft) => { + for (const tool of discovered) { + const schema = (tool.inputSchema ?? {}) as JsonSchema.JsonSchema + draft.add({ + name: tool.name, + options: { namespace: namespace(tool.server), codemode: tool.codemode !== false }, + description: tool.description ?? "", + input: { + ...schema, + type: "object", + properties: schema.properties ?? {}, + additionalProperties: false, + }, + output: (tool.outputSchema ?? {}) as JsonSchema.JsonSchema, + execute: (input, context) => + Effect.gen(function* () { yield* permission.assert({ action: name(tool.server, tool.name), resources: ["*"], @@ -90,31 +86,27 @@ export const layer = Layer.effectDiscard( const content = result.content.map((part) => part.type === "text" ? { type: "text" as const, text: part.text } - : { type: "file" as const, data: part.data, mime: part.mimeType }, + : { + type: "file" as const, + uri: `data:${part.mimeType};base64,${part.data}`, + mime: part.mimeType, + }, ) const text = content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n") return { output: result.structured ?? (text === "" ? null : text), - ...(content.length === 0 ? {} : { content: content as [Tool.Content, ...Tool.Content[]] }), + ...(content.length === 0 ? {} : { content }), } - }).pipe( - Effect.mapError((error) => - error instanceof ToolFailure - ? error - : new ToolFailure({ message: `Unable to execute ${name(tool.server, tool.name)}` }), - ), - ), + }).pipe( + Effect.mapError((error) => + error instanceof ToolFailure + ? error + : new ToolFailure({ message: `Unable to execute ${name(tool.server, tool.name)}` }), + ), + ), + }) + } }) - groups.set(tool.server, group) - } - const next = yield* Scope.fork(scope) - yield* tools - .registerBatch( - Array.from(groups, ([server, group]) => ({ - tools: group.tools, - options: { namespace: namespace(server), codemode: group.codemode }, - })), - ) .pipe(Scope.provide(next), Effect.orDie) if (current) yield* Scope.close(current, Exit.void) current = next @@ -122,7 +114,7 @@ export const layer = Layer.effectDiscard( ) yield* reconcile.pipe(Effect.forkScoped) - yield* events.subscribe(McpEvent.ToolsChanged).pipe( + yield* bus.subscribe(McpEvent.ToolsChanged).pipe( Stream.runForEach(() => reconcile), Effect.forkScoped({ startImmediately: true }), ) @@ -132,5 +124,5 @@ export const layer = Layer.effectDiscard( export const node = makeLocationNode({ name: "mcp-tools", layer, - deps: [ToolRegistry.toolsNode, MCP.node, EventV2.node, PermissionV2.node], + deps: [Tool.node, MCP.node, Bus.node, Permission.node], }) diff --git a/packages/core/src/tool/edit.ts b/packages/core/src/tool/plugin/edit.ts similarity index 92% rename from packages/core/src/tool/edit.ts rename to packages/core/src/tool/plugin/edit.ts index 00b49ca0d0ae..3807020ab247 100644 --- a/packages/core/src/tool/edit.ts +++ b/packages/core/src/tool/plugin/edit.ts @@ -1,21 +1,20 @@ /** - * Model-facing V2 exact-edit leaf. Relative paths resolve within the active + * Model-facing exact-edit leaf. Relative paths resolve within the active * Location. Absolute paths inside that Location are accepted, while explicit * absolute external paths retain mutation capability through a separate * external_directory approval before edit approval. */ export * as EditTool from "./edit" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { ToolFailure } from "@opencode-ai/ai" import { FileDiff } from "@opencode-ai/schema/file-diff" import { createTwoFilesPatch, diffLines } from "diff" import { Effect, Schema } from "effect" -import { FileMutation } from "../file-mutation" +import { FileMutation } from "../../file-mutation" import { FSUtil } from "@opencode-ai/util/fs-util" -import { LocationMutation } from "../location-mutation" -import { PermissionV2 } from "../permission" -import { Tool } from "./tool" +import { LocationMutation } from "../../location-mutation" +import { Permission } from "../../permission" export const name = "edit" @@ -78,12 +77,12 @@ export const toModelOutput = (output: Output, oldString: string, newString: stri "```", ].join("\n") -/** Deferred V2 edit behavior and UX integrations remain visible at the model-facing seam. */ +/** Deferred edit behavior and UX integrations remain visible at the model-facing seam. */ // TODO: Port V1 fuzzy correction strategies only after exact-edit behavior is established: line-trimmed matching, block-anchor fallback, indentation correction, and similarity-threshold review. -// TODO: Add formatter integration after V2 formatter runtime exists. -// TODO: Publish watcher/file-edit events after V2 watcher integration exists. +// TODO: Add formatter integration after formatter runtime exists. +// TODO: Publish watcher/file-edit events after watcher integration exists. // TODO: Add snapshots / undo after design exists. -// TODO: Add LSP notification and diagnostics after V2 LSP runtime exists. +// TODO: Add LSP notification and diagnostics after LSP runtime exists. export const Plugin = { id: "opencode.tool.edit", @@ -91,13 +90,14 @@ export const Plugin = { const mutation = yield* LocationMutation.Service const files = yield* FileMutation.Service const fs = yield* FSUtil.Service - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false, permission: "edit" }, description: "Replace exact text in one file. Relative paths resolve within the active Location. Absolute paths inside the Location are accepted. Explicit external absolute paths require external_directory approval before edit approval.", input: Input, @@ -212,7 +212,6 @@ export const Plugin = { ) }, }), - { codemode: false, permission: "edit" }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/glob.ts b/packages/core/src/tool/plugin/glob.ts similarity index 92% rename from packages/core/src/tool/glob.ts rename to packages/core/src/tool/plugin/glob.ts index 39929ad44bf5..f1003ef55660 100644 --- a/packages/core/src/tool/glob.ts +++ b/packages/core/src/tool/plugin/glob.ts @@ -1,17 +1,16 @@ export * as GlobTool from "./glob" import { ToolFailure } from "@opencode-ai/ai" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { Effect, Schema } from "effect" import path from "path" -import { FileSystem } from "../filesystem" +import { FileSystem } from "../../filesystem" import { FSUtil } from "@opencode-ai/util/fs-util" -import { Location } from "../location" -import { LocationMutation } from "../location-mutation" -import { Ripgrep } from "../ripgrep" -import { RelativePath } from "../schema" -import { PermissionV2 } from "../permission" -import { Tool } from "./tool" +import { Location } from "../../location" +import { LocationMutation } from "../../location-mutation" +import { Ripgrep } from "../../ripgrep" +import { RelativePath } from "../../schema" +import { Permission } from "../../permission" export const name = "glob" @@ -47,13 +46,14 @@ export const Plugin = { const ripgrep = yield* Ripgrep.Service const location = yield* Location.Service const mutation = yield* LocationMutation.Service - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false }, description: 'Search file paths using a glob pattern (examples: "**/*.ts", "src/**/*.tsx").', input: Input, @@ -130,7 +130,6 @@ export const Plugin = { ), ), }), - { codemode: false }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/grep.ts b/packages/core/src/tool/plugin/grep.ts similarity index 93% rename from packages/core/src/tool/grep.ts rename to packages/core/src/tool/plugin/grep.ts index 93267b34bea1..64dcf45fe793 100644 --- a/packages/core/src/tool/grep.ts +++ b/packages/core/src/tool/plugin/grep.ts @@ -1,17 +1,16 @@ export * as GrepTool from "./grep" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { ToolFailure } from "@opencode-ai/ai" import { Effect, Schema } from "effect" import path from "path" -import { FileSystem } from "../filesystem" +import { FileSystem } from "../../filesystem" import { FSUtil } from "@opencode-ai/util/fs-util" -import { Location } from "../location" -import { LocationMutation } from "../location-mutation" -import { PermissionV2 } from "../permission" -import { Ripgrep } from "../ripgrep" -import { RelativePath } from "../schema" -import { Tool } from "./tool" +import { Location } from "../../location" +import { LocationMutation } from "../../location-mutation" +import { Permission } from "../../permission" +import { Ripgrep } from "../../ripgrep" +import { RelativePath } from "../../schema" export const name = "grep" @@ -63,13 +62,14 @@ export const Plugin = { const ripgrep = yield* Ripgrep.Service const location = yield* Location.Service const mutation = yield* LocationMutation.Service - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false }, description: "Search file contents using regular expressions. Use it to locate specific code, symbols, or text patterns, and narrow searches with `path` or `include`. Returns matching file paths, line numbers, and line previews.", input: Input, @@ -154,7 +154,6 @@ export const Plugin = { ), ), }), - { codemode: false }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/patch.ts b/packages/core/src/tool/plugin/patch.ts similarity index 98% rename from packages/core/src/tool/patch.ts rename to packages/core/src/tool/plugin/patch.ts index c7307c069ef2..ea29a4829c9f 100644 --- a/packages/core/src/tool/patch.ts +++ b/packages/core/src/tool/plugin/patch.ts @@ -1,6 +1,6 @@ export * as PatchTool from "./patch" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { ToolFailure } from "@opencode-ai/ai" import { FileDiff } from "@opencode-ai/schema/file-diff" import { createTwoFilesPatch, diffLines } from "diff" @@ -8,11 +8,10 @@ import { Effect, Schema } from "effect" import { PlatformError } from "effect/PlatformError" import path from "path" import { FSUtil } from "@opencode-ai/util/fs-util" -import { Location } from "../location" +import { Location } from "../../location" import { Patch } from "@opencode-ai/util/patch" -import { PermissionV2 } from "../permission" -import { Tool } from "./tool" -import DESCRIPTION from "./patch.txt" +import { Permission } from "../../permission" +import DESCRIPTION from "../patch.txt" export const name = "patch" @@ -70,13 +69,14 @@ export const Plugin = { effect: Effect.fn("PatchTool.Plugin")(function* (ctx: PluginContext) { const fs = yield* FSUtil.Service const location = yield* Location.Service - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false, permission: "edit" }, description: DESCRIPTION, input: Input, output: Output, @@ -310,7 +310,6 @@ export const Plugin = { ) }, }), - { codemode: false, permission: "edit" }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/question.ts b/packages/core/src/tool/plugin/question.ts similarity index 86% rename from packages/core/src/tool/question.ts rename to packages/core/src/tool/plugin/question.ts index 1ad52a8f1b1a..f402cb350de2 100644 --- a/packages/core/src/tool/question.ts +++ b/packages/core/src/tool/plugin/question.ts @@ -1,12 +1,11 @@ export * as QuestionTool from "./question" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { ToolFailure } from "@opencode-ai/ai" import { Effect, Schema } from "effect" -import { Form } from "../form" -import { PermissionV2 } from "../permission" -import { QuestionV2 } from "../question" -import { Tool } from "./tool" +import { Form } from "../../form" +import { Permission } from "../../permission" +import { Question } from "../../question" export const name = "question" @@ -22,11 +21,11 @@ Usage notes: - If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label` export const Input = Schema.Struct({ - questions: Schema.NonEmptyArray(QuestionV2.Prompt).annotate({ description: "Questions to ask" }), + questions: Schema.NonEmptyArray(Question.Prompt).annotate({ description: "Questions to ask" }), }) export const Output = Schema.Struct({ - answers: Schema.Array(QuestionV2.Answer), + answers: Schema.Array(Question.Answer), }) export type Output = typeof Output.Type @@ -37,8 +36,8 @@ export class CancelledError extends Schema.TaggedErrorClass()("Q } export const toModelOutput = ( - questions: ReadonlyArray, - answers: ReadonlyArray, + questions: ReadonlyArray, + answers: ReadonlyArray, ) => { const formatted = questions .map( @@ -53,13 +52,14 @@ export const Plugin = { id: "opencode.tool.question", effect: Effect.fn("QuestionTool.Plugin")(function* (ctx: PluginContext) { const forms = yield* Form.Service - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false }, description, input: Input, output: Output, @@ -91,12 +91,12 @@ export const Plugin = { .pipe(Effect.orDie), ), Effect.flatMap((state) => { - // Deliberate defect tunnel (see PermissionV2.assert): a dismissal must dodge + // Deliberate defect tunnel (see Permission.assert): a dismissal must dodge // leaf `mapError` blankets so it never becomes model-facing tool output; it // resurfaces as a typed failure at SessionModelRequest.executeTool. if (state.status === "cancelled") return Effect.die(new CancelledError()) const output = { - answers: input.questions.map((_, index): QuestionV2.Answer => { + answers: input.questions.map((_, index): Question.Answer => { const value = state.answer[`q${index}`] if (value === undefined) return [] if (typeof value === "object") return Array.from(value) @@ -111,14 +111,13 @@ export const Plugin = { }), ), }), - { codemode: false }, ), ) .pipe(Effect.orDie) }), } -function toField(question: QuestionV2.Prompt, index: number): Form.Field { +function toField(question: Question.Prompt, index: number): Form.Field { return { key: `q${index}`, title: question.header, diff --git a/packages/core/src/tool/read.ts b/packages/core/src/tool/plugin/read.ts similarity index 89% rename from packages/core/src/tool/read.ts rename to packages/core/src/tool/plugin/read.ts index 5c0b9254beb7..2de9757bd687 100644 --- a/packages/core/src/tool/read.ts +++ b/packages/core/src/tool/plugin/read.ts @@ -1,18 +1,17 @@ export * as ReadTool from "./read" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { dirname } from "path" import { ToolFailure } from "@opencode-ai/ai" import { Effect, Schema } from "effect" -import { FileSystem } from "../filesystem" +import { FileSystem } from "../../filesystem" import { FSUtil } from "@opencode-ai/util/fs-util" -import { Location } from "../location" -import { LocationMutation } from "../location-mutation" -import { PermissionV2 } from "../permission" -import { SessionInstructions } from "../session/instructions" -import { AbsolutePath } from "../schema" -import { ReadToolFileSystem } from "./read-filesystem" -import { Tool } from "./tool" +import { Location } from "../../location" +import { LocationMutation } from "../../location-mutation" +import { Permission } from "../../permission" +import { SessionInstructions } from "../../session/instructions" +import { AbsolutePath } from "../../schema" +import { ReadToolFileSystem } from "../read-filesystem" export const name = "read" const FILENAME = "AGENTS.md" @@ -34,7 +33,7 @@ export const Plugin = { effect: Effect.fn("ReadTool.Plugin")(function* (ctx: PluginContext) { const reader = yield* ReadToolFileSystem.Service const mutation = yield* LocationMutation.Service - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service const sessionInstructions = yield* SessionInstructions.Service const fs = yield* FSUtil.Service const location = yield* Location.Service @@ -42,8 +41,9 @@ export const Plugin = { yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false }, description: "Read a text file or supported image, page through a large UTF-8 text file by line offset, or list a directory page. Relative paths resolve from the current location; absolute paths inside it are accepted, while external absolute paths require external_directory approval.", input: Input, @@ -119,7 +119,12 @@ export const Plugin = { ? SUPPORTED_IMAGE_MIMES.has(output.mime) ? ([ { type: "text", text: "Image read successfully" }, - { type: "file", data: output.content, mime: output.mime, name: input.path }, + { + type: "file", + uri: `data:${output.mime};base64,${output.content}`, + mime: output.mime, + name: input.path, + }, ] as const) : JSON.stringify({ ...output, content: "" }, null, 2) : JSON.stringify(output, null, 2) @@ -136,7 +141,6 @@ export const Plugin = { ) }, }), - { codemode: false }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/shell.ts b/packages/core/src/tool/plugin/shell.ts similarity index 94% rename from packages/core/src/tool/shell.ts rename to packages/core/src/tool/plugin/shell.ts index 534855a78d63..f26224caeabd 100644 --- a/packages/core/src/tool/shell.ts +++ b/packages/core/src/tool/plugin/shell.ts @@ -2,16 +2,16 @@ export * as ShellTool from "./shell" import path from "path" import { ToolFailure } from "@opencode-ai/ai" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Content } from "@opencode-ai/schema/tool" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { Deferred, Effect, Schema, Scope } from "effect" import { FSUtil } from "@opencode-ai/util/fs-util" -import { LocationMutation } from "../location-mutation" -import { PermissionV2 } from "../permission" -import { PluginRuntime } from "../plugin/runtime" -import { NonNegativeInt } from "../schema" -import { SessionSchema } from "../session/schema" -import { Shell } from "../shell" -import { Tool, type Content } from "./tool" +import { LocationMutation } from "../../location-mutation" +import { Permission } from "../../permission" +import { PluginRuntime } from "../../plugin/runtime" +import { NonNegativeInt } from "../../schema" +import { SessionSchema } from "../../session/schema" +import { Shell } from "../../shell" export const name = "shell" export const DEFAULT_TIMEOUT_MS = 2 * 60 * 1_000 @@ -64,14 +64,14 @@ const modelOutput = (output: Output): string | undefined => { } /** - * Minimal V2 core shell boundary. Keep parity debt visible without pulling the + * Minimal core shell boundary. Keep parity debt visible without pulling the * legacy shell runtime into core. */ // TODO: Port tree-sitter bash / PowerShell parser-based approval reduction. // TODO: Port BashArity reusable command-prefix approvals. // TODO: Replace token-based command-argument external-directory advisories with parser-based detection. // TODO: Restore PowerShell and cmd-specific invocation/path handling on Windows. -// TODO: Add plugin shell.env environment augmentation once V2 plugin hooks exist. +// TODO: Add plugin shell.env environment augmentation once plugin hooks exist. // TODO: Persist job status and define restart recovery before exposing remote observation. // TODO: Add HTTP job observation only after durable status, restart recovery, and authorization are defined. // TODO: Revisit process-group cleanup and platform coverage with shell-specific tests if current AppProcess semantics do not fully cover it. @@ -104,7 +104,7 @@ export const Plugin = { const fsUtil = yield* FSUtil.Service const mutation = yield* LocationMutation.Service const shell = yield* Shell.Service - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service const notifyWhenDone = Effect.fn("ShellTool.notifyWhenDone")(function* ( sessionID: SessionSchema.ID, @@ -142,8 +142,9 @@ export const Plugin = { yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false }, description: `Execute one shell command string with the host user's filesystem, process, and network authority. The active Location is the default working directory. Relative workdir values resolve from that Location. External workdir values require external_directory approval; best-effort command-argument path warnings are advisory only. An optional timeout may be provided in milliseconds (zero: unlimited; foreground default: ${DEFAULT_TIMEOUT_MS}; maximum: ${MAX_TIMEOUT_MS}). Background commands default to unlimited. Uses the configured shell when set; otherwise uses /bin/sh on POSIX and COMSPEC or cmd.exe on Windows. Background mode (background=true) launches the command asynchronously and returns immediately; you are notified when it finishes.`, input: Input, output: Output, @@ -267,7 +268,7 @@ export const Plugin = { return { ...(yield* Deferred.await(settled)), ...(warnings.length ? { warnings } : {}) } }).pipe( Effect.map((output) => { - const content: [Content, ...Content[]] = [{ type: "text", text: output.output }] + const content: Array = [{ type: "text", text: output.output }] const model = modelOutput(output) if (model) content.push({ type: "text", text: model }) return { @@ -286,7 +287,6 @@ export const Plugin = { ), ), }), - { codemode: false }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/skill.ts b/packages/core/src/tool/plugin/skill.ts similarity index 87% rename from packages/core/src/tool/skill.ts rename to packages/core/src/tool/plugin/skill.ts index 48bdb0b17f7a..f8a4109a63ab 100644 --- a/packages/core/src/tool/skill.ts +++ b/packages/core/src/tool/plugin/skill.ts @@ -1,23 +1,22 @@ export * as SkillTool from "./skill" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import path from "path" import { ToolFailure } from "@opencode-ai/ai" import { Effect, Schema } from "effect" import { FSUtil } from "@opencode-ai/util/fs-util" -import { SkillV2 } from "../skill" -import { PermissionV2 } from "../permission" -import { Tool } from "./tool" +import { Skill } from "../../skill" +import { Permission } from "../../permission" export const name = "skill" const FILE_LIMIT = 10 export const Input = Schema.Struct({ - id: SkillV2.ID.annotate({ description: "The ID of the skill from the available skills list" }), + id: Skill.ID.annotate({ description: "The ID of the skill from the available skills list" }), }) export const Output = Schema.Struct({ - name: SkillV2.Name, + name: Skill.Name, directory: Schema.String, output: Schema.String, }) @@ -27,7 +26,7 @@ export const description = [ "The skill ID must match one of the available skills in the instructions.", ].join("\n") -export const toModelOutput = (skill: SkillV2.Info, files: ReadonlyArray) => { +export const toModelOutput = (skill: Skill.Info, files: ReadonlyArray) => { const directory = path.dirname(skill.location) return [ ``, @@ -53,13 +52,14 @@ export const Plugin = { id: "opencode.tool.skill", effect: Effect.fn("SkillTool.Plugin")(function* (ctx: PluginContext) { const fs = yield* FSUtil.Service - const skills = yield* SkillV2.Service - const permission = yield* PermissionV2.Service + const skills = yield* Skill.Service + const permission = yield* Permission.Service yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false }, description, input: Input, output: Output, @@ -99,7 +99,6 @@ export const Plugin = { })), ), }), - { codemode: false }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/subagent.ts b/packages/core/src/tool/plugin/subagent.ts similarity index 95% rename from packages/core/src/tool/subagent.ts rename to packages/core/src/tool/plugin/subagent.ts index daef4e1cf391..ee7f54769a07 100644 --- a/packages/core/src/tool/subagent.ts +++ b/packages/core/src/tool/plugin/subagent.ts @@ -1,14 +1,13 @@ export * as SubagentTool from "./subagent" import { ToolFailure } from "@opencode-ai/ai" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { Effect, Schema, Scope } from "effect" -import { AgentV2 } from "../agent" -import { Config } from "../config" -import { PluginRuntime } from "../plugin/runtime" -import { PermissionV2 } from "../permission" -import { SessionSchema } from "../session/schema" -import { Tool } from "./tool" +import { Agent } from "../../agent" +import { Config } from "../../config" +import { PluginRuntime } from "../../plugin/runtime" +import { Permission } from "../../permission" +import { SessionSchema } from "../../session/schema" export const name = "subagent" @@ -42,9 +41,9 @@ export const Plugin = { id: "opencode.tool.subagent", effect: Effect.fn("SubagentTool.Plugin")(function* (ctx: PluginContext) { const runtime = yield* PluginRuntime.Service - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const config = yield* Config.Service - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service const scope = yield* Scope.Scope // Concatenate the child's final completed assistant text. Distinguishes "completed with no @@ -109,8 +108,9 @@ export const Plugin = { yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false }, description, input: Input, output: Output, @@ -165,7 +165,7 @@ export const Plugin = { .create({ parentID: context.sessionID, title: input.description, - agent: AgentV2.ID.make(input.agent), + agent: Agent.ID.make(input.agent), model, // TODO(opencode kkdvxn): derive restricted subagent permissions from the parent // session (V1 deriveSubagentSessionPermission). MVP uses the agent's own permissions. @@ -238,7 +238,6 @@ export const Plugin = { })), ), }), - { codemode: false }, ), ) .pipe(Effect.orDie) @@ -254,7 +253,7 @@ export const Plugin = { (agent) => agent.mode !== "primary" && !agent.hidden && - PermissionV2.evaluate(name, agent.id, selected.permissions).effect !== "deny", + Permission.evaluate(name, agent.id, selected.permissions).effect !== "deny", ) .toSorted((a, b) => a.id.localeCompare(b.id)) if (available.length === 0) return diff --git a/packages/core/src/tool/webfetch.ts b/packages/core/src/tool/plugin/webfetch.ts similarity index 96% rename from packages/core/src/tool/webfetch.ts rename to packages/core/src/tool/plugin/webfetch.ts index d3f894bdbc48..b4298cfbc54c 100644 --- a/packages/core/src/tool/webfetch.ts +++ b/packages/core/src/tool/plugin/webfetch.ts @@ -1,14 +1,13 @@ export * as WebFetchTool from "./webfetch" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { ToolFailure } from "@opencode-ai/ai" import { Duration, Effect, Schema } from "effect" import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { Parser } from "htmlparser2" import TurndownService from "turndown" -import { PermissionV2 } from "../permission" -import { collectBoundedResponseBody } from "./http-body" -import { Tool } from "./tool" +import { Permission } from "../../permission" +import { collectBoundedResponseBody } from "../http-body" export const name = "webfetch" export const MAX_RESPONSE_BYTES = 5 * 1024 * 1024 @@ -115,13 +114,14 @@ export const Plugin = { id: "opencode.tool.webfetch", effect: Effect.fn("WebFetchTool.Plugin")(function* (ctx: PluginContext) { const http = yield* HttpClient.HttpClient - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false }, description, input: Input, output: Output, @@ -173,7 +173,6 @@ export const Plugin = { return { output: result, content: result.output, metadata: { contentType: result.contentType } } }).pipe(Effect.mapError((error) => new ToolFailure({ message: `Unable to fetch ${input.url}`, error }))), }), - { codemode: false }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/websearch.ts b/packages/core/src/tool/plugin/websearch.ts similarity index 94% rename from packages/core/src/tool/websearch.ts rename to packages/core/src/tool/plugin/websearch.ts index 9e3a6256bd11..98e0bc0e5727 100644 --- a/packages/core/src/tool/websearch.ts +++ b/packages/core/src/tool/plugin/websearch.ts @@ -1,12 +1,12 @@ export * as WebSearchTool from "./websearch" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { ToolFailure } from "@opencode-ai/ai" import { Effect, Schema } from "effect" -import { Form } from "../form" -import { KV } from "../kv" -import { PermissionV2 } from "../permission" -import { WebSearch } from "../websearch" +import { Form } from "../../form" +import { KV } from "../../kv" +import { Permission } from "../../permission" +import { WebSearch } from "../../websearch" export const name = "websearch" export const NO_RESULTS = "No search results found. Please try a different query." @@ -26,15 +26,16 @@ const Output = Schema.Struct({ export const Plugin = { id: "opencode.tool.websearch", effect: Effect.fn("WebSearchTool.Plugin")(function* (ctx: PluginContext) { - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service const forms = yield* Form.Service const kv = yield* KV.Service yield* ctx.tool .transform((draft) => draft.add( - name, { + name, + options: { codemode: false }, description, input: Input, output: Output, @@ -109,7 +110,6 @@ export const Plugin = { ), ), }, - { codemode: false }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/write.ts b/packages/core/src/tool/plugin/write.ts similarity index 82% rename from packages/core/src/tool/write.ts rename to packages/core/src/tool/plugin/write.ts index 4d86df9729c8..d5e9411a5277 100644 --- a/packages/core/src/tool/write.ts +++ b/packages/core/src/tool/plugin/write.ts @@ -1,18 +1,17 @@ /** - * Model-facing V2 file-write leaf. Relative paths resolve within the active + * Model-facing file-write leaf. Relative paths resolve within the active * Location. Absolute paths inside that Location are accepted, while explicit * absolute external paths retain mutation capability through a separate * external_directory approval before edit approval. */ export * as WriteTool from "./write" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { ToolFailure } from "@opencode-ai/ai" import { Effect, Schema } from "effect" -import { FileMutation } from "../file-mutation" -import { LocationMutation } from "../location-mutation" -import { PermissionV2 } from "../permission" -import { Tool } from "./tool" +import { FileMutation } from "../../file-mutation" +import { LocationMutation } from "../../location-mutation" +import { Permission } from "../../permission" export const name = "write" @@ -36,24 +35,25 @@ export type Output = typeof Output.Type export const toModelOutput = (output: Output) => `${output.existed ? "Wrote" : "Created"} file successfully: ${output.resource}` -/** Deferred V2 write UX integrations remain visible at the model-facing seam. */ -// TODO: Add formatter integration after V2 formatter runtime exists. -// TODO: Publish watcher/file-edit events after V2 watcher integration exists. +/** Deferred write UX integrations remain visible at the model-facing seam. */ +// TODO: Add formatter integration after formatter runtime exists. +// TODO: Publish watcher/file-edit events after watcher integration exists. // TODO: Add snapshots / undo after design exists. -// TODO: Add LSP notification and diagnostics after V2 LSP runtime exists. +// TODO: Add LSP notification and diagnostics after LSP runtime exists. export const Plugin = { id: "opencode.tool.write", effect: Effect.fn("WriteTool.Plugin")(function* (ctx: PluginContext) { const mutation = yield* LocationMutation.Service const files = yield* FileMutation.Service - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false, permission: "edit" }, description: "Write content to one file. Relative paths resolve within the active Location. Absolute paths inside the Location are accepted. Explicit external absolute paths require external_directory approval before edit approval.", input: Input, @@ -88,7 +88,6 @@ export const Plugin = { Effect.mapError((error) => new ToolFailure({ message: `Unable to write ${input.path}`, error })), ), }), - { codemode: false, permission: "edit" }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/registry.ts b/packages/core/src/tool/registry.ts deleted file mode 100644 index c5e3d1014b57..000000000000 --- a/packages/core/src/tool/registry.ts +++ /dev/null @@ -1,377 +0,0 @@ -export * as ToolRegistry from "./registry" - -import { type ToolCall, type ToolContent, type ToolDefinition } from "@opencode-ai/ai" -import { Context, Effect, Layer, Schema, Scope, Semaphore } from "effect" -import type { AgentV2 } from "../agent" -import { CodeModeCatalog } from "../codemode/catalog" -import { Image } from "../image" -import { PermissionV2 } from "../permission" -import { SessionMessage } from "../session/message" -import { SessionSchema } from "../session/schema" -import { ToolOutputStore } from "../tool-output-store" -import { Wildcard } from "../util/wildcard" -import { CodeMode } from "../codemode" -import { Tool, nonEmpty, registrationEntries, toLLMDefinition, validateName, validateNamespace } from "./tool" -import { Tools } from "./tools" -import { ToolHooks } from "./hooks" -import { makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { toSessionError } from "../session/to-session-error" - -export type ExecuteInput = { - readonly sessionID: SessionSchema.ID - readonly agent: AgentV2.ID - readonly messageID: SessionMessage.ID - readonly call: ToolCall - readonly progress?: (update: Progress) => Effect.Effect -} - -/** Live replacement metadata for a running tool. */ -export type Progress = Tool.Metadata - -export interface Interface { - readonly snapshot: (permissions?: PermissionV2.Ruleset) => Effect.Effect - /** Internal registration capability exposed publicly only through Tools.Service. */ - readonly register: ( - tools: Readonly>, - options?: Tools.RegisterOptions, - ) => Effect.Effect - /** Internal atomic registration capability used by plugin transforms. */ - readonly registerBatch: ( - registrations: ReadonlyArray<{ - readonly tools: Readonly> - readonly options?: Tools.RegisterOptions - }>, - ) => Effect.Effect -} - -/** - * One request-scoped snapshot pairing the Code Mode catalog and advertised - * definitions with captured tools. A model request executes exactly the tool - * values it advertised even if registration changes while it is in flight. - */ -export interface ToolSet { - readonly definitions: ReadonlyArray - readonly codeModeCatalog?: ReadonlyArray - readonly execute: (input: ExecuteInput) => Effect.Effect -} - -/** - * The canonical outcome of one local tool execution. `output` is the validated - * machine value for Code Mode and remains ephemeral; durable publication drops it. - */ -export type ToolOutcome = - | (Extract & { readonly output?: unknown }) - | Extract - -export class Service extends Context.Service()("@opencode/v2/ToolRegistry") {} - -const registryLayer = Layer.effect( - Service, - Effect.gen(function* () { - const resources = yield* ToolOutputStore.Service - const toolHooks = yield* ToolHooks.Service - const image = yield* Image.Service - const codeMode = yield* CodeMode.Service - - type NormalizedItem = ToolContent | "decode" | "size" - const normalizeImages = Effect.fn("ToolRegistry.normalizeImages")(function* (content: ReadonlyArray) { - const normalized = yield* Effect.forEach(content, (item): Effect.Effect => { - if (item.type !== "file" || !item.mime.startsWith("image/")) return Effect.succeed(item) - // RFC 2397 permits parameters between the mime and ";base64". - const base64 = /^data:[^,]*;base64,(.*)$/s.exec(item.uri)?.[1] - if (base64 === undefined) return Effect.succeed(item) - const resource = item.name ?? `${item.mime} tool output` - return image.normalize(resource, { uri: resource, content: base64, encoding: "base64", mime: item.mime }).pipe( - Effect.map((result) => ({ - ...item, - uri: `data:${result.mime};base64,${result.content}`, - mime: result.mime, - })), - Effect.catchTag("Image.ResizerUnavailableError", () => Effect.succeed(item)), - Effect.catchTag("Image.DecodeError", () => Effect.succeed("decode" as const)), - Effect.catchTag("Image.SizeError", () => Effect.succeed("size" as const)), - ) - }) - const note = (reason: "decode" | "size", text: string) => { - const count = normalized.filter((item) => item === reason).length - if (count === 0) return [] - return [{ type: "text" as const, text: `[${count} image${count === 1 ? "" : "s"} omitted: ${text}]` }] - } - return [ - ...normalized.filter((item) => typeof item !== "string"), - ...note("decode", "could not be decoded."), - ...note("size", "could not be resized below the image size limit."), - ] - }) - - // Invalid or oversized metadata is dropped with a warning; it never fails a - // successful side-effecting tool. - const validMetadata = Effect.fnUntraced(function* (tool: string, metadata: Tool.Metadata | undefined) { - if (metadata === undefined) return undefined - const limits = yield* resources.limits() - const valid = Tool.jsonMetadata(metadata, limits.maxBytes) - if (valid === undefined) - yield* Effect.logWarning("dropping invalid or oversized tool metadata").pipe(Effect.annotateLogs({ tool })) - return valid - }) - - type Registration = Tool.Registration - const local = new Map>() - const registrationLock = Semaphore.makeUnsafe(1) - - const executeTool = Effect.fn("ToolRegistry.executeTool")(function* (input: ExecuteInput, tool: Tool.Any) { - // Hooks fire only for hosted/local tools; provider-executed calls never reach executeTool. - const beforeEvent: ToolHooks.BeforeEvent = { - tool: input.call.name, - sessionID: input.sessionID, - agent: input.agent, - messageID: input.messageID, - callID: input.call.id, - input: input.call.input, - } - yield* toolHooks.runBefore(beforeEvent) - const execution = yield* Tool.execute(tool, beforeEvent.input, { - sessionID: input.sessionID, - agent: input.agent, - messageID: input.messageID, - callID: input.call.id, - progress: (metadata) => { - const progress = input.progress - if (!progress) return Effect.void - return validMetadata(input.call.name, metadata).pipe( - Effect.flatMap((valid) => (valid === undefined ? Effect.void : progress(valid))), - ) - }, - }).pipe( - Effect.map((value) => ({ value })), - Effect.catchTag("LLM.ToolFailure", (failure) => Effect.succeed({ failure: toSessionError(failure) })), - ) - - const outcome: ToolOutcome = yield* Effect.gen(function* () { - if ("failure" in execution) return { status: "error" as const, error: execution.failure } - const bounded = yield* resources.bound({ - sessionID: input.sessionID, - callID: input.call.id, - content: yield* normalizeImages(execution.value.content), - }) - const metadata = yield* validMetadata(input.call.name, execution.value.metadata) - return { - status: "completed" as const, - ...(execution.value.output === undefined ? {} : { output: execution.value.output }), - content: nonEmpty(bounded.content) ?? execution.value.content, - ...(metadata === undefined ? {} : { metadata }), - ...(bounded.outputPaths.length > 0 ? { outputPaths: bounded.outputPaths } : {}), - } - }) - - const base = { - tool: input.call.name, - sessionID: input.sessionID, - agent: input.agent, - messageID: input.messageID, - callID: input.call.id, - input: beforeEvent.input, - } - const afterEvent: ToolHooks.AfterEvent = - outcome.status === "completed" - ? { - ...base, - status: "completed", - content: outcome.content, - ...(outcome.metadata === undefined ? {} : { metadata: outcome.metadata }), - ...(outcome.outputPaths === undefined ? {} : { outputPaths: outcome.outputPaths }), - } - : { - ...base, - status: "error", - error: outcome.error, - ...(outcome.content === undefined ? {} : { content: outcome.content }), - ...(outcome.metadata === undefined ? {} : { metadata: outcome.metadata }), - ...(outcome.outputPaths === undefined ? {} : { outputPaths: outcome.outputPaths }), - } - yield* toolHooks.runAfter(afterEvent) - const afterMetadata = yield* validMetadata(input.call.name, afterEvent.metadata) - const afterContent = yield* Effect.gen(function* () { - if ( - afterEvent.content === undefined || - (outcome.status === "completed" && afterEvent.content === outcome.content) - ) - return { content: afterEvent.content, outputPaths: afterEvent.outputPaths } - const bounded = yield* resources.bound({ - sessionID: input.sessionID, - callID: input.call.id, - content: yield* normalizeImages(afterEvent.content), - }) - return { - content: nonEmpty(bounded.content), - outputPaths: - bounded.outputPaths.length === 0 - ? afterEvent.outputPaths - : Array.from(new Set([...(afterEvent.outputPaths ?? []), ...bounded.outputPaths])), - } - }) - if (afterEvent.status === "completed") - return { - status: "completed" as const, - ...(outcome.status === "completed" && outcome.output !== undefined ? { output: outcome.output } : {}), - content: afterContent.content ?? afterEvent.content, - ...(afterMetadata === undefined ? {} : { metadata: afterMetadata }), - ...(afterContent.outputPaths === undefined ? {} : { outputPaths: afterContent.outputPaths }), - } - return { - status: "error" as const, - error: afterEvent.error, - ...(afterContent.content === undefined ? {} : { content: afterContent.content }), - ...(afterMetadata === undefined ? {} : { metadata: afterMetadata }), - ...(afterContent.outputPaths === undefined ? {} : { outputPaths: afterContent.outputPaths }), - } - }) - - const registerBatch: Interface["registerBatch"] = Effect.fn("ToolRegistry.registerBatch")( - function* (registrations) { - const planned = yield* Effect.forEach(registrations, ({ tools, options }) => - Effect.gen(function* () { - if (options?.namespace !== undefined) yield* validateNamespace(options.namespace) - const entries = registrationEntries(tools, options) - yield* Effect.forEach(entries, (entry) => validateName(entry.name), { discard: true }) - const collision = entries.find( - (entry, index) => entries.findIndex((candidate) => candidate.key === entry.key) !== index, - ) - if (collision) - return yield* Effect.fail( - new Tool.RegistrationError({ - name: collision.key, - message: `Duplicate normalized tool name: ${collision.key}`, - }), - ) - const codemode = options?.codemode ?? true - const reserved = codemode ? undefined : entries.find((entry) => entry.key === "execute") - if (reserved) - return yield* Effect.fail( - new Tool.RegistrationError({ - name: reserved.key, - message: 'Tool name "execute" is reserved for CodeMode', - }), - ) - return { tools, options, entries, codemode } - }), - ) - // CodeMode registrations live in the CodeMode service; the registry keeps only direct tools. - yield* Effect.forEach( - planned.filter((plan) => plan.codemode && plan.entries.length > 0), - (plan) => codeMode.register(plan.entries), - { discard: true }, - ) - const direct = planned.filter((plan) => !plan.codemode) - if (direct.every((plan) => plan.entries.length === 0)) return - yield* Effect.uninterruptible( - registrationLock.withPermit( - Effect.gen(function* () { - const token = {} - for (const { entries } of direct) - for (const entry of entries) - local.set(entry.key, [ - ...(local.get(entry.key) ?? []), - { - token, - registration: { - tool: entry.tool, - name: entry.name, - namespace: entry.namespace, - permission: entry.permission, - }, - }, - ]) - yield* Effect.addFinalizer(() => - registrationLock.withPermit( - Effect.sync(() => { - for (const { entries } of direct) - for (const entry of entries) { - const registrations = - local.get(entry.key)?.filter((registration) => registration.token !== token) ?? [] - if (registrations.length > 0) local.set(entry.key, registrations) - else local.delete(entry.key) - } - }), - ), - ) - }), - ), - ) - }, - ) - - return Service.of({ - register: Effect.fn("ToolRegistry.register")((tools, options) => - registerBatch([ - { - tools, - ...(options === undefined ? {} : { options }), - }, - ]), - ), - registerBatch, - snapshot: Effect.fn("ToolRegistry.snapshot")((permissions) => - registrationLock.withPermit( - Effect.gen(function* () { - const direct = new Map() - const rules = permissions ?? [] - for (const [name, entries] of local) { - const registration = entries.at(-1)?.registration - if (!registration) continue - if (whollyDisabled(registration.permission, rules)) continue - direct.set(name, registration) - } - const codeModeMaterialization = yield* codeMode.materialize(permissions) - const codemodeTool = codeModeMaterialization.tool - return { - ...(codeModeMaterialization.catalog === undefined - ? {} - : { codeModeCatalog: codeModeMaterialization.catalog }), - definitions: [ - // Definitions are prompt-cache prefix bytes, so order only after effective registrations settle. - ...Array.from(direct) - .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)) - .map(([name, registration]) => toLLMDefinition(name, registration.tool)), - ...(codemodeTool ? [toLLMDefinition("execute", codemodeTool)] : []), - ], - execute: (input: ExecuteInput) => { - if (input.call.name === "execute" && codemodeTool) return executeTool(input, codemodeTool) - const registration = direct.get(input.call.name) - if (registration) return executeTool(input, registration.tool) - return Effect.succeed({ - status: "error", - error: { type: "tool.unknown", message: `Unknown tool: ${input.call.name}` }, - }) - }, - } - }), - ), - ), - }) - }), -) - -const layer = Layer.effect( - Tools.Service, - Service.use((registry) => - Effect.succeed(Tools.Service.of({ register: registry.register, registerBatch: registry.registerBatch })), - ), -).pipe(Layer.provideMerge(registryLayer)) - -function whollyDisabled(action: string, rules: PermissionV2.Ruleset) { - const rule = rules.findLast((rule) => Wildcard.match(action, rule.action)) - return rule?.resource === "*" && rule.effect === "deny" -} - -export const node = makeLocationNode({ - service: Service, - layer, - deps: [CodeMode.node, ToolOutputStore.node, ToolHooks.node, Image.node], -}) - -export const toolsNode = makeLocationNode({ - service: Tools.Service, - layer, - deps: [CodeMode.node, ToolOutputStore.node, ToolHooks.node, Image.node], -}) diff --git a/packages/core/src/tool/runtime.ts b/packages/core/src/tool/runtime.ts new file mode 100644 index 000000000000..80872798e021 --- /dev/null +++ b/packages/core/src/tool/runtime.ts @@ -0,0 +1,125 @@ +import type { ToolDefinition } from "@opencode-ai/ai" +import { Tool } from "@opencode-ai/schema/tool" +import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec" +import { Effect, JsonSchema, Schema } from "effect" + +export const definition = (tool: Tool.Info): ToolDefinition => ({ + name: effectiveName(tool), + description: tool.description, + inputSchema: inputJsonSchema(tool.input), + ...(tool.output === undefined ? {} : { outputSchema: outputJsonSchema(tool.output) }), +}) + +export const execute = (tool: Tool.Info, input: unknown, context: Tool.Context) => + Effect.gen(function* () { + const decoded = yield* decodeInput(tool.input, input) + const result = yield* tool.execute(decoded, context) + if (tool.output === undefined) { + if ("output" in result) return yield* Effect.die("Tool result declared output without an output schema") + return { + output: undefined, + content: normalizeContent(result.content), + ...(result.metadata === undefined ? {} : { metadata: result.metadata }), + } + } + if (!("output" in result)) return yield* new Tool.Error({ message: "Tool did not return its declared output" }) + const output = yield* encodeOutput(tool.output, result.output) + return { + output, + content: normalizeContent(result.content, output), + ...(result.metadata === undefined ? {} : { metadata: result.metadata }), + } + }) + +const decodeInput = (schema: Tool.ValueSchema, value: unknown) => { + if (Schema.isSchema(schema)) + return Schema.decodeUnknownEffect(schema)(value).pipe( + Effect.mapError((error) => new Tool.Error({ message: `Invalid tool input: ${error.message}` })), + ) + if (isStandardSchema(schema)) return validateStandard(schema, value, "Invalid tool input") + return Effect.succeed(value) +} + +const encodeOutput = (schema: Tool.ValueSchema, value: unknown) => { + if (Schema.isSchema(schema)) + return Schema.encodeEffect(schema)(value).pipe( + Effect.mapError( + (error) => new Tool.Error({ message: `Tool returned an invalid value for its output schema: ${error.message}` }), + ), + ) + if (isStandardSchema(schema)) + return validateStandard(schema, value, "Tool returned an invalid value for its output schema") + return Schema.decodeUnknownEffect(Schema.Json)(value).pipe( + Effect.mapError( + (error) => new Tool.Error({ message: `Tool returned a non-JSON value for its output schema: ${error.message}` }), + ), + ) +} + +const isStandardSchema = ( + schema: Tool.ValueSchema, +): schema is StandardSchemaV1 & StandardJSONSchemaV1 => "~standard" in schema + +const validateStandard = ( + schema: StandardSchemaV1 & StandardJSONSchemaV1, + value: unknown, + prefix: string, +) => + Effect.gen(function* () { + const pending = yield* Effect.try({ + try: () => schema["~standard"].validate(value), + catch: (error) => standardFailure(prefix, error), + }) + const result = + pending instanceof Promise + ? yield* Effect.tryPromise({ try: () => pending, catch: (error) => standardFailure(prefix, error) }) + : pending + if (result.issues) + return yield* new Tool.Error({ + message: `${prefix}: ${result.issues.map((issue) => issue.message).join(", ")}`, + }) + return result.value + }) + +const standardFailure = (prefix: string, error: unknown) => + new Tool.Error({ message: `${prefix}: ${error instanceof Error ? error.message : String(error)}` }) + +const inputJsonSchema = (schema: Tool.ValueSchema): JsonSchema.JsonSchema => { + if (isStandardSchema(schema)) + return schema["~standard"].jsonSchema.input({ target: "draft-2020-12" }) as JsonSchema.JsonSchema + return Schema.isSchema(schema) ? toJsonSchema(schema) : (schema as JsonSchema.JsonSchema) +} + +const outputJsonSchema = (schema: Tool.ValueSchema): JsonSchema.JsonSchema => { + if (isStandardSchema(schema)) + return schema["~standard"].jsonSchema.output({ target: "draft-2020-12" }) as JsonSchema.JsonSchema + return Schema.isSchema(schema) ? toJsonSchema(schema) : (schema as JsonSchema.JsonSchema) +} + +const toJsonSchema = (schema: Schema.Top): JsonSchema.JsonSchema => { + const document = Schema.toJsonSchemaDocument(schema) + if (Object.keys(document.definitions).length === 0) return document.schema + return { ...document.schema, $defs: document.definitions } +} + +export const normalizeContent = (value: string | ReadonlyArray | undefined, output?: unknown) => { + if (typeof value === "string") return [{ type: "text" as const, text: value }] + if (value !== undefined && value.length > 0) return [...value] + return [{ type: "text" as const, text: stringify(output) }] +} + +const stringify = (value: unknown) => { + if (typeof value === "string") return value + try { + return JSON.stringify(value) ?? String(value) + } catch { + return String(value) + } +} + +const normalizedName = (tool: Tool.Info) => tool.name.replace(/[^a-zA-Z0-9_-]/g, "_") + +const effectiveName = (tool: Tool.Info) => + tool.options?.namespace === undefined + ? normalizedName(tool) + : `${tool.options.namespace.replaceAll(".", "_")}_${normalizedName(tool)}` diff --git a/packages/core/src/tool/tool.ts b/packages/core/src/tool/tool.ts deleted file mode 100644 index e9917f94ddc6..000000000000 --- a/packages/core/src/tool/tool.ts +++ /dev/null @@ -1,90 +0,0 @@ -export * as Tool from "./tool" -export * from "@opencode-ai/plugin/v2/effect/tool" - -import type { ToolContent } from "@opencode-ai/ai" -import { - decodeInput, - encodeOutput, - type Any, - type Content, - type Context, - Failure, - type Metadata, -} from "@opencode-ai/plugin/v2/effect/tool" -import { Effect, Schema } from "effect" - -/** Non-empty canonical model content. */ -export type NonEmptyContent = readonly [ToolContent, ...ToolContent[]] - -/** - * The execution-local result of one tool call: the machine output for - * Code Mode, canonical model content, and optional UI metadata. The typed - * domain output never leaves this function. - */ -export type Execution = { - readonly output?: unknown - readonly content: NonEmptyContent - readonly metadata?: Metadata -} - -export const execute = (tool: Any, input: unknown, context: Context): Effect.Effect => - Effect.gen(function* () { - const decoded = yield* decodeInput(tool.input, input) - const result = yield* tool.execute(decoded, context) - if (tool.output === undefined) { - if ("output" in result) return yield* Effect.die("Tool result declared output without an output schema") - return { - content: contentFrom(result.content), - ...(result.metadata === undefined ? {} : { metadata: result.metadata }), - } - } - if (!("output" in result)) - return yield* Effect.fail(new Failure({ message: "Tool did not return its declared output" })) - const encoded = yield* encodeOutput(tool.output, result.output) - return { - output: encoded, - content: contentFrom(result.content, encoded), - ...(result.metadata === undefined ? {} : { metadata: result.metadata }), - } - }) - -/** Model content from the tool's projection, falling back to the stringified encoded output. */ -const contentFrom = (projected: string | ReadonlyArray | undefined, encoded?: unknown): NonEmptyContent => { - if (typeof projected === "string") return [textContent(projected)] - if (projected !== undefined) { - const mapped = nonEmpty(projected.map(toModelContent)) - if (mapped !== undefined) return mapped - } - return [textContent(stringify(encoded))] -} - -export const toModelContent = (part: Content): ToolContent => - part.type === "text" - ? { type: "text", text: part.text } - : { type: "file", uri: `data:${part.mime};base64,${part.data}`, mime: part.mime, name: part.name } - -export const nonEmpty = (content: ReadonlyArray): NonEmptyContent | undefined => - content.length > 0 ? (content as NonEmptyContent) : undefined - -const textContent = (text: string): ToolContent => ({ type: "text", text }) - -/** Human-readable text for an arbitrary value; strings pass through unchanged. */ -export const stringify = (value: unknown) => { - if (typeof value === "string") return value - try { - return JSON.stringify(value) ?? String(value) - } catch { - return String(value) - } -} - -const MetadataSchema = Schema.Record(Schema.String, Schema.Json) - -/** Defensive boundary: non-JSON or oversized metadata is dropped, never failing the producing call. */ -export const jsonMetadata = (value: unknown, maxBytes?: number): Metadata | undefined => { - if (value === undefined) return undefined - const decoded = Schema.decodeUnknownOption(MetadataSchema)(value) - if (decoded._tag === "None") return undefined - if (maxBytes !== undefined && Buffer.byteLength(JSON.stringify(decoded.value), "utf-8") > maxBytes) return undefined - return decoded.value -} diff --git a/packages/core/src/tool/tools.ts b/packages/core/src/tool/tools.ts deleted file mode 100644 index 31ecc10cdf04..000000000000 --- a/packages/core/src/tool/tools.ts +++ /dev/null @@ -1,23 +0,0 @@ -export * as Tools from "./tools" - -import { Context, Effect, Scope } from "effect" -import { Tool } from "./tool" - -export type RegisterOptions = Tool.RegisterOptions - -export interface Interface { - readonly register: ( - tools: Readonly>, - options?: Tool.RegisterOptions, - ) => Effect.Effect - /** Internal atomic registration capability used by plugin transforms. */ - readonly registerBatch: ( - registrations: ReadonlyArray<{ - readonly tools: Readonly> - readonly options?: Tool.RegisterOptions - }>, - ) => Effect.Effect -} - -/** Narrow registration-only Location capability. */ -export class Service extends Context.Service()("@opencode/v2/Tools") {} diff --git a/packages/core/src/v1/config/migrate.ts b/packages/core/src/v1/config/migrate.ts index 61a49f57cf47..afff5bdbd5b6 100644 --- a/packages/core/src/v1/config/migrate.ts +++ b/packages/core/src/v1/config/migrate.ts @@ -7,8 +7,8 @@ import { ConfigMCPV1 } from "./mcp" import { ConfigPermissionV1 } from "./permission" import { ConfigProviderV1 } from "./provider" import { ConfigProviderOptionsV1 } from "./provider-options" -import { ProviderV2 } from "../../provider" -import { ModelV2 } from "../../model" +import { Provider } from "../../provider" +import { Model } from "../../model" const keys = new Set([ "logLevel", @@ -242,7 +242,7 @@ function migrateProvider(info: ConfigProviderV1.Info) { return { name: info.name, env: info.env, - package: info.npm ? ProviderV2.aisdk(info.npm) : undefined, + package: info.npm ? Provider.aisdk(info.npm) : undefined, settings: info.api ? { ...options.settings, baseURL: info.api } : options.settings, headers: info.options && options.headers, body: info.options && options.body, @@ -279,8 +279,8 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type) { modelID: info.id, family: info.family, name: info.name, - compatibility: ModelV2.compatibility(info.interleaved), - package: info.provider?.npm ? ProviderV2.aisdk(info.provider.npm) : undefined, + compatibility: Model.compatibility(info.interleaved), + package: info.provider?.npm ? Provider.aisdk(info.provider.npm) : undefined, settings: info.provider?.api ? { ...settings, baseURL: info.provider.api } : settings, capabilities, headers: info.headers, diff --git a/packages/core/src/v2-schema.ts b/packages/core/src/v2-schema.ts deleted file mode 100644 index 4dfd51d92bd9..000000000000 --- a/packages/core/src/v2-schema.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * as V2Schema from "./v2-schema" - -export { DateTimeUtcFromMillis } from "@opencode-ai/schema/schema" diff --git a/packages/core/src/vcs.ts b/packages/core/src/vcs.ts index cb61e681f7e4..308520bba0cb 100644 --- a/packages/core/src/vcs.ts +++ b/packages/core/src/vcs.ts @@ -21,7 +21,7 @@ export interface Interface { readonly diff: (mode: Mode, options?: DiffOptions) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Vcs") {} +export class Service extends Context.Service()("@opencode/Vcs") {} // Adapter seam: one working-copy implementation per VCS type, selected by the // resolved location. Locations without a supported VCS degrade to empty diff --git a/packages/core/src/websearch.ts b/packages/core/src/websearch.ts index a1ac9107cd36..7f389bffce46 100644 --- a/packages/core/src/websearch.ts +++ b/packages/core/src/websearch.ts @@ -3,7 +3,7 @@ export * as WebSearch from "./websearch" import { WebSearch } from "@opencode-ai/schema/websearch" import { Context, Effect, Layer, Schema } from "effect" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { KV } from "./kv" import { State } from "./state" @@ -13,7 +13,7 @@ export type ID = WebSearch.ID export const Provider = WebSearch.Provider export type Provider = WebSearch.Provider -export const Event = WebSearch.Event +export { Event } from "@opencode-ai/schema/websearch" export const Input = WebSearch.Input export type Input = WebSearch.Input @@ -56,7 +56,7 @@ export interface Interface extends State.Transformable { readonly query: (input: Input) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/WebSearch") {} +export class Service extends Context.Service()("@opencode/WebSearch") {} type Data = { readonly providers: Map @@ -74,7 +74,7 @@ export type Draft = { const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const kv = yield* KV.Service const decodeResults = Schema.decodeUnknownEffect(Schema.Array(Result)) const state = State.create({ @@ -86,7 +86,7 @@ const layer = Layer.effect( set: (providerID) => (draft.defaultProviderID = providerID), }, }), - finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid), + finalize: () => bus.publish(WebSearch.Event.Updated, {}).pipe(Effect.asVoid), }) const requireProvider = (providers: Map, providerID: ID) => { @@ -140,5 +140,5 @@ const layer = Layer.effect( export const node = makeLocationNode({ service: Service, layer, - deps: [EventV2.node, KV.node], + deps: [Bus.node, KV.node], }) diff --git a/packages/core/src/wellknown.ts b/packages/core/src/wellknown.ts index 78a21e74800e..db5862a94886 100644 --- a/packages/core/src/wellknown.ts +++ b/packages/core/src/wellknown.ts @@ -6,7 +6,7 @@ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstab import { isDeepStrictEqual } from "node:util" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { httpClient } from "@opencode-ai/util/effect/app-node-platform" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { KV } from "./kv" export interface Auth extends Schema.Schema.Type {} @@ -51,10 +51,10 @@ export interface Interface { readonly resolve: (entry: Entry, variables: Readonly>) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/WellKnown") {} +export class Service extends Context.Service()("@opencode/WellKnown") {} export const Event = { - Updated: EventV2.ephemeral({ type: "wellknown.updated", schema: {} }), + Updated: Bus.ephemeral({ type: "wellknown.updated", schema: {} }), } export const inspect = Effect.fn("WellKnown.inspect")(function* (origin: string) { @@ -103,7 +103,7 @@ const layer = Layer.effect( Effect.gen(function* () { const http = yield* HttpClient.HttpClient const kv = yield* KV.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const cache = yield* Ref.make(new Map()) const lock = Semaphore.makeUnsafe(1) @@ -139,7 +139,7 @@ const layer = Layer.effect( const changed = !isDeepStrictEqual(Ref.getUnsafe(cache), next) if (!changed) return false yield* Ref.set(cache, next) - yield* events.publish(Event.Updated, {}) + yield* bus.publish(Event.Updated, {}) return true }), ) @@ -160,7 +160,7 @@ const layer = Layer.effect( const origins = Schema.is(Sources)(sources) ? sources : [] yield* kv.set(sourcesKey, Array.from(new Set([...origins, origin]))) yield* Ref.update(cache, (current) => new Map(current).set(origin, entry)) - yield* events.publish(Event.Updated, {}) + yield* bus.publish(Event.Updated, {}) return entry }), ) @@ -180,7 +180,7 @@ const layer = Layer.effect( next.delete(origin) return next }) - yield* events.publish(Event.Updated, {}) + yield* bus.publish(Event.Updated, {}) }), ) }), @@ -191,4 +191,4 @@ const layer = Layer.effect( }), ) -export const node = makeGlobalNode({ service: Service, layer, deps: [httpClient, KV.node, EventV2.node] }) +export const node = makeGlobalNode({ service: Service, layer, deps: [httpClient, KV.node, Bus.node] }) diff --git a/packages/core/src/wellknown/plugin.ts b/packages/core/src/wellknown/plugin.ts index 9be129a248ee..3e1f7dbed34d 100644 --- a/packages/core/src/wellknown/plugin.ts +++ b/packages/core/src/wellknown/plugin.ts @@ -1,14 +1,14 @@ export * as WellKnownPlugin from "./plugin" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Effect, Stream } from "effect" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { WellKnown } from "../wellknown" export const Plugin = define({ id: "opencode.wellknown", effect: Effect.fn(function* (ctx) { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const wellknown = yield* WellKnown.Service yield* wellknown.entries().pipe(Effect.orDie) yield* ctx.integration.transform((draft) => { @@ -28,7 +28,7 @@ export const Plugin = define({ }) }) }) - yield* events.subscribe(WellKnown.Event.Updated).pipe( + yield* bus.subscribe(WellKnown.Event.Updated).pipe( Stream.runForEach(() => ctx.integration.reload()), Effect.forkScoped({ startImmediately: true }), ) diff --git a/packages/core/src/workspace.ts b/packages/core/src/workspace.ts index d85bbe4ba47f..4cf18c32ffd2 100644 --- a/packages/core/src/workspace.ts +++ b/packages/core/src/workspace.ts @@ -1,4 +1,4 @@ -export * as WorkspaceV2 from "./workspace" +export * as Workspace from "./workspace" import { Workspace } from "@opencode-ai/schema/workspace" diff --git a/packages/core/test/agent.test.ts b/packages/core/test/agent.test.ts index 88bf879b6026..41aeee99eca5 100644 --- a/packages/core/test/agent.test.ts +++ b/packages/core/test/agent.test.ts @@ -1,12 +1,12 @@ import { describe, expect } from "bun:test" import { Effect, Exit, Fiber, Layer, Scope, Stream } from "effect" import { TestClock } from "effect/testing" -import { AgentV2 } from "@opencode-ai/core/agent" -import { EventV2 } from "@opencode-ai/core/event" +import { Agent } from "@opencode-ai/core/agent" +import { Bus } from "@opencode-ai/core/bus" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Location } from "@opencode-ai/core/location" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { AgentPlugin } from "@opencode-ai/core/plugin/agent" import { AbsolutePath } from "@opencode-ai/core/schema" import { location } from "./fixture/location" @@ -17,22 +17,22 @@ const testLocation = location({ directory: AbsolutePath.make("/project") }) const locationLayer = Layer.succeed(Location.Service, Location.Service.of(testLocation)) const it = testEffect( - AppNodeBuilder.build(LayerNode.group([AgentV2.node, EventV2.node, Location.node]), [ + AppNodeBuilder.build(LayerNode.group([Agent.node, Bus.node, Location.node]), [ [Location.node, locationLayer], ]) as unknown as Layer.Layer, ) -describe("AgentV2", () => { +describe("Agent", () => { it.effect("publishes an updated event after agent changes", () => Effect.gen(function* () { - const agent = yield* AgentV2.Service - const events = yield* EventV2.Service - const updated = yield* events - .subscribe(AgentV2.Event.Updated) + const agent = yield* Agent.Service + const bus = yield* Bus.Service + const updated = yield* bus + .subscribe(Agent.Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow - yield* agent.transform((editor) => editor.update(AgentV2.ID.make("reviewer"), () => {})) + yield* agent.transform((editor) => editor.update(Agent.ID.make("reviewer"), () => {})) expect(yield* Fiber.join(updated)).toMatchObject([{ location: { directory: testLocation.directory } }]) }), @@ -40,17 +40,17 @@ describe("AgentV2", () => { it.effect("starts without agents", () => Effect.gen(function* () { - const agent = yield* AgentV2.Service + const agent = yield* Agent.Service expect(yield* agent.list()).toEqual([]) - expect(yield* agent.get(AgentV2.ID.make("build"))).toBeUndefined() + expect(yield* agent.get(Agent.ID.make("build"))).toBeUndefined() }), ) it.effect("materializes replayable agent transforms", () => Effect.gen(function* () { - const agent = yield* AgentV2.Service - const id = AgentV2.ID.make("reviewer") + const agent = yield* Agent.Service + const id = Agent.ID.make("reviewer") yield* agent.transform((editor) => editor.update(id, (info) => { info.description = "Reviews code" @@ -65,8 +65,8 @@ describe("AgentV2", () => { it.effect("rebuilds state when a transform is replaced", () => Effect.gen(function* () { - const agent = yield* AgentV2.Service - const id = AgentV2.ID.make("reviewer") + const agent = yield* Agent.Service + const id = Agent.ID.make("reviewer") let description = "Old description" let hidden = true yield* agent.transform((editor) => @@ -87,8 +87,8 @@ describe("AgentV2", () => { it.effect("removes a transform when its scope closes", () => Effect.gen(function* () { - const agent = yield* AgentV2.Service - const id = AgentV2.ID.make("scoped") + const agent = yield* Agent.Service + const id = Agent.ID.make("scoped") const scope = yield* Scope.make() yield* agent.transform((editor) => editor.update(id, () => {})).pipe(Scope.provide(scope)) expect(yield* agent.get(id)).toBeDefined() @@ -100,8 +100,8 @@ describe("AgentV2", () => { it.effect("applies direct agent updates", () => Effect.gen(function* () { - const agent = yield* AgentV2.Service - const id = AgentV2.ID.make("build") + const agent = yield* Agent.Service + const id = Agent.ID.make("build") yield* agent.transform((editor) => editor.update(id, (info) => { @@ -116,11 +116,11 @@ describe("AgentV2", () => { it.effect("creates agents with runtime defaults and supports direct removal", () => Effect.gen(function* () { - const agent = yield* AgentV2.Service - const id = AgentV2.ID.make("custom") + const agent = yield* Agent.Service + const id = Agent.ID.make("custom") yield* agent.transform((editor) => editor.update(id, () => {})) - expect(yield* agent.get(id)).toEqual(AgentV2.Info.empty(id)) + expect(yield* agent.get(id)).toEqual(Agent.Info.empty(id)) yield* agent.transform((editor) => editor.remove(id)) expect(yield* agent.get(id)).toBeUndefined() @@ -129,7 +129,7 @@ describe("AgentV2", () => { it.effect("does not ambiently opt built-in agents into bash", () => Effect.gen(function* () { - const agent = yield* AgentV2.Service + const agent = yield* Agent.Service yield* AgentPlugin.Plugin.effect( host({ agent: agentHost(agent), @@ -151,7 +151,7 @@ describe("AgentV2", () => { "summary", "title", ]) - expect((yield* agent.get(AgentV2.defaultID))?.system).toBeUndefined() + expect((yield* agent.get(Agent.defaultID))?.system).toBeUndefined() for (const item of agents) { expect(item.permissions.some((rule) => rule.action === "bash" && rule.effect !== "deny")).toBe(false) } @@ -160,7 +160,7 @@ describe("AgentV2", () => { it.effect("denies the subagent tool for built-in subagents", () => Effect.gen(function* () { - const agent = yield* AgentV2.Service + const agent = yield* Agent.Service yield* AgentPlugin.Plugin.effect( host({ agent: agentHost(agent), @@ -174,11 +174,11 @@ describe("AgentV2", () => { yield* Effect.forEach(["general", "explore"], (id) => Effect.gen(function* () { - const info = yield* agent.get(AgentV2.ID.make(id)) + const info = yield* agent.get(Agent.ID.make(id)) if (!info) throw new Error(`expected built-in agent: ${id}`) expect(info.mode).toBe("subagent") expect(info.permissions).toContainEqual({ action: "subagent", resource: "*", effect: "deny" }) - expect(PermissionV2.evaluate("subagent", "*", info.permissions).effect).toBe("deny") + expect(Permission.evaluate("subagent", "*", info.permissions).effect).toBe("deny") }), ) }), diff --git a/packages/core/test/aisdk.test.ts b/packages/core/test/aisdk.test.ts index 03f05c65347f..87a02d1b3ce3 100644 --- a/packages/core/test/aisdk.test.ts +++ b/packages/core/test/aisdk.test.ts @@ -1,7 +1,7 @@ import type { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3StreamPart } from "@ai-sdk/provider" import { AISDK } from "@opencode-ai/core/aisdk" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { LLM, LLMError, LLMEvent, Message } from "@opencode-ai/ai" import { LLMClient, RequestExecutor } from "@opencode-ai/ai/route" import { expect } from "bun:test" @@ -11,10 +11,10 @@ import { testEffect } from "./lib/effect" const it = testEffect(AISDK.locationLayer) const model = (packageName: string, settings: Record = {}) => - ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("test-provider"), ModelV2.ID.make("catalog-model")), - modelID: ModelV2.ID.make("api-model"), - package: ProviderV2.aisdk(packageName), + Model.Info.make({ + ...Model.Info.default(Provider.ID.make("test-provider"), Model.ID.make("catalog-model")), + modelID: Model.ID.make("api-model"), + package: Provider.aisdk(packageName), settings, limit: { context: 100, output: 20 }, }) @@ -185,7 +185,7 @@ it.effect("routes AI Gateway model options by upstream prefix", () => gateway: { order: ["anthropic"] }, thinking: { type: "adaptive" }, }), - modelID: ModelV2.ID.make("anthropic/claude-sonnet-5"), + modelID: Model.ID.make("anthropic/claude-sonnet-5"), }) const anthropicPrepared = yield* LLMClient.prepare( LLM.request({ model: anthropic, prompt: "Hello" }), @@ -197,7 +197,7 @@ it.effect("routes AI Gateway model options by upstream prefix", () => const bedrock = yield* aisdk.model({ ...model("@ai-sdk/gateway", { reasoningConfig: { type: "enabled" } }), - modelID: ModelV2.ID.make("amazon/nova-2-lite"), + modelID: Model.ID.make("amazon/nova-2-lite"), }) const bedrockPrepared = yield* LLMClient.prepare( LLM.request({ model: bedrock, prompt: "Hello" }), @@ -208,7 +208,7 @@ it.effect("routes AI Gateway model options by upstream prefix", () => const fallback = yield* aisdk.model({ ...model("@ai-sdk/gateway", { reasoningEffort: "high" }), - modelID: ModelV2.ID.make("deepseek/deepseek-v4"), + modelID: Model.ID.make("deepseek/deepseek-v4"), }) const fallbackPrepared = yield* LLMClient.prepare( LLM.request({ model: fallback, prompt: "Hello" }), diff --git a/packages/core/test/event.test.ts b/packages/core/test/bus.test.ts similarity index 64% rename from packages/core/test/event.test.ts rename to packages/core/test/bus.test.ts index 5fa89af8972c..447afae7e63e 100644 --- a/packages/core/test/event.test.ts +++ b/packages/core/test/bus.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Option, Ref, Schema, Stream } from "effect" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Event } from "@opencode-ai/schema/event" import { Session } from "@opencode-ai/schema/session" import { SessionEvent } from "@opencode-ai/schema/session-event" @@ -11,7 +11,7 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { EventSequenceTable, EventTable } from "@opencode-ai/core/event/sql" import { Location } from "@opencode-ai/core/location" import { AbsolutePath } from "@opencode-ai/core/schema" -import { WorkspaceV2 } from "@opencode-ai/core/workspace" +import { Workspace } from "@opencode-ai/core/workspace" import { eq } from "drizzle-orm" import { location } from "./fixture/location" import { testEffect } from "./lib/effect" @@ -19,17 +19,17 @@ import { testEffect } from "./lib/effect" const locationLayer = Layer.succeed( Location.Service, Location.Service.of( - location({ directory: AbsolutePath.make("project"), workspaceID: WorkspaceV2.ID.make("wrk_test") }), + location({ directory: AbsolutePath.make("project"), workspaceID: Workspace.ID.make("wrk_test") }), ), ) -const Message = EventV2.ephemeral({ +const Message = Bus.ephemeral({ type: "test.message", schema: { text: Schema.String, }, }) -const SyncMessage = EventV2.durable({ +const SyncMessage = Bus.durable({ type: "test.sync", durable: { version: 1, @@ -41,7 +41,7 @@ const SyncMessage = EventV2.durable({ }, }) -const SyncSent = EventV2.durable({ +const SyncSent = Bus.durable({ type: "test.sent", durable: { version: 1, @@ -53,31 +53,31 @@ const SyncSent = EventV2.durable({ }, }) -const VersionedMessageV1 = EventV2.durable({ +const VersionedMessageV1 = Bus.durable({ type: "test.versioned", durable: { version: 1, aggregate: "id" }, schema: { id: Schema.String }, }) -const VersionedMessageV2 = EventV2.durable({ +const VersionedMessageV2 = Bus.durable({ type: "test.versioned", durable: { version: 2, aggregate: "id" }, schema: { id: Schema.String }, }) -const GlobalMessage = EventV2.ephemeral({ +const GlobalMessage = Bus.ephemeral({ type: "test.global", schema: { text: Schema.String, }, }) -const CountMessage = EventV2.ephemeral({ +const CountMessage = Bus.ephemeral({ type: "test.count", schema: { count: Schema.Number, }, }) -const VersionedMessage = EventV2.durable({ +const VersionedMessage = Bus.durable({ type: "test.versioned", durable: { version: 2, @@ -96,27 +96,27 @@ const durableData = (sessionID: Session.ID, text: string) => ({ }) /** Followed log read without markers: the old `durable` stream shape. */ -const tail = (events: EventV2.Interface, input: { aggregateID: string; after?: number }) => - events.log({ ...input, follow: true }).pipe(Stream.filter((item): item is EventV2.Payload => !EventV2.isSynced(item))) +const tail = (bus: Bus.Interface, input: { aggregateID: string; after?: number }) => + bus.log({ ...input, follow: true }).pipe(Stream.filter((item): item is Event.Payload => !Bus.isSynced(item))) const it = testEffect( - AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, Location.node]), [[Location.node, locationLayer]]), + AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, Location.node]), [[Location.node, locationLayer]]), ) -const itWithoutLocation = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node]))) +const itWithoutLocation = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node]))) -describe("EventV2", () => { +describe("Bus", () => { it.effect("subscribes to multiple event definitions with a discriminated payload union", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service // @ts-expect-error multi-definition subscriptions require at least one definition - events.subscribe([]) - const fiber = yield* events + bus.subscribe([]) + const fiber = yield* bus .subscribe([Message, CountMessage]) .pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow - yield* events.publish(Message, { text: "hello" }) - yield* events.publish(CountMessage, { count: 2 }) + yield* bus.publish(Message, { text: "hello" }) + yield* bus.publish(CountMessage, { count: 2 }) const received = Array.from(yield* Fiber.join(fiber)).map((event) => event.type === "test.message" ? event.data.text : event.data.count, @@ -127,10 +127,10 @@ describe("EventV2", () => { it.effect("publishes events with the current location", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const fiber = yield* events.subscribe(Message).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) + const bus = yield* Bus.Service + const fiber = yield* bus.subscribe(Message).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow - const event = yield* events.publish(Message, { text: "hello" }) + const event = yield* bus.publish(Message, { text: "hello" }) const received = Array.from(yield* Fiber.join(fiber)) expect(received).toEqual([event]) @@ -139,15 +139,15 @@ describe("EventV2", () => { expect(event.data).toEqual({ text: "hello" }) expect(event.location).toEqual({ directory: AbsolutePath.make("project"), - workspaceID: WorkspaceV2.ID.make("wrk_test"), + workspaceID: Workspace.ID.make("wrk_test"), }) }), ) itWithoutLocation.effect("omits location when no location is available", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const event = yield* events.publish(GlobalMessage, { text: "hello" }) + const bus = yield* Bus.Service + const event = yield* bus.publish(GlobalMessage, { text: "hello" }) expect(event).not.toHaveProperty("location") expect(event.type).toBe("test.global") @@ -156,22 +156,22 @@ describe("EventV2", () => { it.effect("publishes definition version", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const event = yield* events.publish(VersionedMessage, { id: "one", text: "hello" }) + const bus = yield* Bus.Service + const event = yield* bus.publish(VersionedMessage, { id: "one", text: "hello" }) expect(event.type).toBe("test.versioned") - expect(event.durable?.version).toBe(EventV2.Version.make(2)) + expect(event.durable?.version).toBe(Event.Version.make(2)) }), ) it.effect("selects the latest durable definition independent of declaration order", () => Effect.sync(() => { - const latest = EventV2.durable({ + const latest = Bus.durable({ type: "test.out-of-order", durable: { version: 2, aggregate: "id" }, schema: { id: Schema.String }, }) - const historical = EventV2.durable({ + const historical = Bus.durable({ type: "test.out-of-order", durable: { version: 1, aggregate: "id" }, schema: { id: Schema.String }, @@ -184,11 +184,11 @@ describe("EventV2", () => { it.effect("publishes to typed and wildcard subscriptions", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const typed = yield* events.subscribe(Message).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) - const wildcard = yield* events.subscribe().pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) + const bus = yield* Bus.Service + const typed = yield* bus.subscribe(Message).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) + const wildcard = yield* bus.subscribe().pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow - const event = yield* events.publish(Message, { text: "hello" }) + const event = yield* bus.publish(Message, { text: "hello" }) expect(Array.from(yield* Fiber.join(typed))).toEqual([event]) expect(Array.from(yield* Fiber.join(wildcard))).toEqual([event]) @@ -197,16 +197,16 @@ describe("EventV2", () => { it.effect("runs projectors inline", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const received = new Array() - yield* events.project(SyncMessage, (event) => + const bus = yield* Bus.Service + const received = new Array() + yield* bus.project(SyncMessage, (event) => Effect.sync(() => { received.push(event) }), ) - const event = yield* events.publish(SyncMessage, { id: "one", text: "hello" }) - yield* events.publish(SyncMessage, { id: "one", text: "after unsubscribe" }) + const event = yield* bus.publish(SyncMessage, { id: "one", text: "hello" }) + yield* bus.publish(SyncMessage, { id: "one", text: "after unsubscribe" }) expect(received[0]).toEqual(event) expect(received[1]?.data).toEqual({ id: "one", text: "after unsubscribe" }) @@ -215,12 +215,12 @@ describe("EventV2", () => { it.effect("commits local operational state inside a new durable event transaction", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const received = new Array() - const aggregateID = EventV2.ID.create() - yield* events.project(SyncMessage, () => Effect.sync(() => received.push("projector"))) + const aggregateID = Event.ID.create() + yield* bus.project(SyncMessage, () => Effect.sync(() => received.push("projector"))) - yield* events.publish( + yield* bus.publish( SyncMessage, { id: aggregateID, text: "hello" }, { commit: (seq) => Effect.sync(() => received.push(`commit:${seq}`)) }, @@ -232,16 +232,16 @@ describe("EventV2", () => { it.effect("rolls back the durable event and projector when the local commit fails", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Event.ID.create() yield* db.run("CREATE TABLE IF NOT EXISTS event_commit_probe (value text NOT NULL)") yield* db.run("DELETE FROM event_commit_probe") - yield* events.project(SyncMessage, () => + yield* bus.project(SyncMessage, () => db.run("INSERT INTO event_commit_probe (value) VALUES ('projected')").pipe(Effect.orDie, Effect.asVoid), ) - const exit = yield* events + const exit = yield* bus .publish(SyncMessage, { id: aggregateID, text: "hello" }, { commit: () => Effect.die("commit failed") }) .pipe(Effect.exit) @@ -256,8 +256,8 @@ describe("EventV2", () => { it.effect("rejects local commit hooks on live-only events", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const exit = yield* events.publish(Message, { text: "hello" }, { commit: () => Effect.void }).pipe(Effect.exit) + const bus = yield* Bus.Service + const exit = yield* bus.publish(Message, { text: "hello" }, { commit: () => Effect.void }).pipe(Effect.exit) expect(String(exit)).toContain("Local commit hooks require a durable event") }), @@ -265,21 +265,21 @@ describe("EventV2", () => { it.effect("runs projectors before publishing to streams", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const received = new Array() - const fiber = yield* events.subscribe().pipe( + const fiber = yield* bus.subscribe().pipe( Stream.take(1), Stream.runForEach(() => Effect.sync(() => received.push("stream"))), Effect.forkScoped, ) - yield* events.project(SyncMessage, (event) => + yield* bus.project(SyncMessage, (event) => Effect.sync(() => { received.push(event.type) }), ) yield* Effect.yieldNow - yield* events.publish(SyncMessage, { id: "one", text: "hello" }) + yield* bus.publish(SyncMessage, { id: "one", text: "hello" }) yield* Fiber.join(fiber) expect(received).toEqual([SyncMessage.type, "stream"]) @@ -288,22 +288,22 @@ describe("EventV2", () => { it.effect("runs listeners inline after projectors", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const received = new Array() - yield* events.project(SyncMessage, () => + yield* bus.project(SyncMessage, () => Effect.sync(() => { received.push("projector") }), ) - const unsubscribe = yield* events.listen(() => + const unsubscribe = yield* bus.listen(() => Effect.sync(() => { received.push("listener") }), ) - yield* events.publish(SyncMessage, { id: "one", text: "hello" }) + yield* bus.publish(SyncMessage, { id: "one", text: "hello" }) yield* unsubscribe - yield* events.publish(SyncMessage, { id: "one", text: "after unsubscribe" }) + yield* bus.publish(SyncMessage, { id: "one", text: "after unsubscribe" }) expect(received).toEqual(["projector", "listener", "projector"]) }), @@ -311,18 +311,18 @@ describe("EventV2", () => { it.effect("isolates observer defects after durable events commit", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const received = new Array() - yield* events.listen(() => { + yield* bus.listen(() => { throw new Error("listener defect") }) - yield* events.listen((event) => + yield* bus.listen((event) => Effect.sync(() => { received.push(event.type) }), ) - const event = yield* events.publish(SyncMessage, { id: "one", text: "hello" }) + const event = yield* bus.publish(SyncMessage, { id: "one", text: "hello" }) expect(received).toEqual([SyncMessage.type]) expect(event.durable?.seq).toBeNumber() @@ -331,11 +331,11 @@ describe("EventV2", () => { it.effect("notifies global listeners only after a durable event is committed", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Event.ID.create() const observed = new Array<{ id: string; seq: number }>() - yield* events.listen((event) => + yield* bus.listen((event) => event.type !== SyncMessage.type ? Effect.void : db @@ -354,7 +354,7 @@ describe("EventV2", () => { ), ) - const event = yield* events.publish(SyncMessage, { id: aggregateID, text: "committed" }) + const event = yield* bus.publish(SyncMessage, { id: aggregateID, text: "committed" }) if (!event.durable) throw new Error("Expected durable event metadata") expect(observed).toEqual([{ id: event.id, seq: event.durable.seq }]) @@ -363,11 +363,11 @@ describe("EventV2", () => { it.effect("preserves observer interruption", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service - yield* events.listen(() => Effect.interrupt) + yield* bus.listen(() => Effect.interrupt) - const exit = yield* events.publish(SyncMessage, { id: "interrupted", text: "hello" }).pipe(Effect.exit) + const exit = yield* bus.publish(SyncMessage, { id: "interrupted", text: "hello" }).pipe(Effect.exit) const committed = yield* db .select({ id: EventTable.id }) .from(EventTable) @@ -382,21 +382,21 @@ describe("EventV2", () => { it.effect("keeps live-only listener defects fail-fast", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const defect = new Error("listener defect") - yield* events.listen(() => Effect.die(defect)) + yield* bus.listen(() => Effect.die(defect)) - expect(yield* events.publish(Message, { text: "hello" }).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect) + expect(yield* bus.publish(Message, { text: "hello" }).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect) }), ) it.effect("inserts durable event rows on publish", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Event.ID.create() - yield* events.publish(SyncMessage, { id: aggregateID, text: "first" }) + yield* bus.publish(SyncMessage, { id: aggregateID, text: "first" }) const rows = yield* db .select() .from(EventTable) @@ -405,19 +405,19 @@ describe("EventV2", () => { .pipe(Effect.orDie) expect(rows).toHaveLength(1) - expect(rows[0]?.type).toBe(EventV2.versionedType(SyncMessage.type, 1)) + expect(rows[0]?.type).toBe(Bus.versionedType(SyncMessage.type, 1)) expect(rows[0]?.aggregate_id).toBe(aggregateID) }), ) it.effect("increments durable event seq per aggregate", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Event.ID.create() - yield* events.publish(SyncMessage, { id: aggregateID, text: "first" }) - yield* events.publish(SyncMessage, { id: aggregateID, text: "second" }) + yield* bus.publish(SyncMessage, { id: aggregateID, text: "first" }) + yield* bus.publish(SyncMessage, { id: aggregateID, text: "second" }) const rows = yield* db .select() .from(EventTable) @@ -431,18 +431,18 @@ describe("EventV2", () => { it.effect("replays durable aggregate events after a sequence and tails new events", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - yield* events.publish(DurableMessage, durableData(aggregateID, "zero")) - yield* events.publish(DurableMessage, durableData(aggregateID, "one")) - const fiber = yield* tail(events, { aggregateID, after: 0 }).pipe( + yield* bus.publish(DurableMessage, durableData(aggregateID, "zero")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "one")) + const fiber = yield* tail(bus, { aggregateID, after: 0 }).pipe( Stream.take(2), Stream.runCollect, Effect.forkScoped, ) yield* Effect.yieldNow - yield* events.publish(DurableMessage, durableData(aggregateID, "two")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "two")) expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([ [1, durableData(aggregateID, "one")], @@ -453,12 +453,12 @@ describe("EventV2", () => { it.effect("catches durable aggregate events published during replay handoff", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - yield* events.publish(DurableMessage, durableData(aggregateID, "zero")) - const fiber = yield* tail(events, { aggregateID }).pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped) + yield* bus.publish(DurableMessage, durableData(aggregateID, "zero")) + const fiber = yield* tail(bus, { aggregateID }).pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped) - yield* events.publish(DurableMessage, durableData(aggregateID, "one")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "one")) expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([ [0, durableData(aggregateID, "zero")], @@ -472,7 +472,7 @@ describe("EventV2", () => { const readStarted = yield* Deferred.make() const continueRead = yield* Deferred.make() let pause = true - const eventLayer = EventV2.layerWith({ + const eventLayer = Bus.layerWith({ beforeAggregateRead: () => pause ? Deferred.succeed(readStarted, undefined).pipe(Effect.andThen(Deferred.await(continueRead))) @@ -480,13 +480,13 @@ describe("EventV2", () => { }).pipe(Layer.provide(LayerNode.compile(Database.node))) yield* Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - const fiber = yield* tail(events, { aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) + const fiber = yield* tail(bus, { aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Deferred.await(readStarted) pause = false - yield* events.publish(DurableMessage, durableData(aggregateID, "during handoff")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "during handoff")) yield* Deferred.succeed(continueRead, undefined) expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([ @@ -498,14 +498,14 @@ describe("EventV2", () => { it.effect("coalesces durable aggregate wakes while draining every committed event", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() const count = 64 - const fiber = yield* tail(events, { aggregateID }).pipe(Stream.take(count), Stream.runCollect, Effect.forkScoped) + const fiber = yield* tail(bus, { aggregateID }).pipe(Stream.take(count), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow for (let index = 0; index < count; index++) { - yield* events.publish(DurableMessage, durableData(aggregateID, String(index))) + yield* bus.publish(DurableMessage, durableData(aggregateID, String(index))) } expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual( @@ -516,13 +516,13 @@ describe("EventV2", () => { it.effect("omits live-only events from durable aggregate streams", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - const fiber = yield* tail(events, { aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) + const fiber = yield* tail(bus, { aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow - yield* events.publish(Message, { text: "live only" }) - yield* events.publish(DurableMessage, durableData(aggregateID, "durable")) + yield* bus.publish(Message, { text: "live only" }) + yield* bus.publish(DurableMessage, durableData(aggregateID, "durable")) expect(Array.from(yield* Fiber.join(fiber)).map((event) => event.type)).toEqual([DurableMessage.type]) }), @@ -530,11 +530,11 @@ describe("EventV2", () => { it.effect("uses custom sync aggregate field", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Event.ID.create() - yield* events.publish(SyncSent, { messageID: aggregateID, text: "sent" }) + yield* bus.publish(SyncSent, { messageID: aggregateID, text: "sent" }) const rows = yield* db .select() .from(EventTable) @@ -549,19 +549,19 @@ describe("EventV2", () => { it.effect("replays durable events through projectors", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const received = new Array() - yield* events.project(DurableMessage, (event) => + const bus = yield* Bus.Service + const received = new Array() + yield* bus.project(DurableMessage, (event) => Effect.sync(() => { received.push(event) }), ) const aggregateID = Session.ID.create() - yield* events.replay({ - id: EventV2.ID.create(), + yield* bus.replay({ + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "hello"), @@ -574,14 +574,14 @@ describe("EventV2", () => { it.effect("replay inserts external event rows", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const aggregateID = Session.ID.create() - yield* events.replay({ - id: EventV2.ID.create(), + yield* bus.replay({ + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "replayed"), @@ -602,23 +602,23 @@ describe("EventV2", () => { "replay rejects an envelope aggregate that differs from its payload without mutating the payload aggregate", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const envelopeAggregateID = Session.ID.create() const payloadAggregateID = Session.ID.create() - const received = new Array() - yield* events.publish(DurableMessage, durableData(payloadAggregateID, "seed")) - yield* events.project(DurableMessage, (event) => + const received = new Array() + yield* bus.publish(DurableMessage, durableData(payloadAggregateID, "seed")) + yield* bus.project(DurableMessage, (event) => Effect.sync(() => { received.push(event) }), ) - const exit = yield* events + const exit = yield* bus .replay({ - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 1, aggregateID: envelopeAggregateID, data: durableData(payloadAggregateID, "replayed"), @@ -646,22 +646,22 @@ describe("EventV2", () => { it.effect("replay defects on sequence mismatch", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - yield* events.replay({ - id: EventV2.ID.create(), + yield* bus.replay({ + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "first"), }) - const exit = yield* events + const exit = yield* bus .replay({ - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 5, aggregateID, data: durableData(aggregateID, "bad"), @@ -674,19 +674,19 @@ describe("EventV2", () => { it.effect("replay decodes synchronized transformed values before projection", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() const received = new Array() - yield* events.project(SessionEvent.InstructionsUpdated, (event) => + yield* bus.project(SessionEvent.InstructionsUpdated, (event) => Effect.sync(() => { received.push(event) }), ) - yield* events.replay({ - id: EventV2.ID.create(), + yield* bus.replay({ + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(SessionEvent.InstructionsUpdated.type, 2), + type: Bus.versionedType(SessionEvent.InstructionsUpdated.type, 2), seq: 0, aggregateID, data: { sessionID: aggregateID, delta: { "core/context": "0".repeat(64) } }, @@ -698,33 +698,33 @@ describe("EventV2", () => { it.effect("dispatches durable projectors by exact event version", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() const received = new Array() - yield* events.project(VersionedMessageV2, (event) => + yield* bus.project(VersionedMessageV2, (event) => Effect.sync(() => { received.push(event) }), ) - yield* events.publish(VersionedMessageV1, { id: aggregateID }) - yield* events.publish(VersionedMessageV2, { id: aggregateID }) + yield* bus.publish(VersionedMessageV1, { id: aggregateID }) + yield* bus.publish(VersionedMessageV2, { id: aggregateID }) expect(received).toHaveLength(1) - expect(received[0]?.durable.version).toBe(EventV2.Version.make(2)) + expect(received[0]?.durable.version).toBe(Event.Version.make(2)) }), ) it.effect("replay defects on unknown event type", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const exit = yield* events + const bus = yield* Bus.Service + const exit = yield* bus .replay({ - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), type: "unknown.event.1", seq: 0, - aggregateID: EventV2.ID.create(), + aggregateID: Event.ID.create(), data: {}, }) .pipe(Effect.exit) @@ -735,21 +735,21 @@ describe("EventV2", () => { it.effect("replayAll validates contiguous aggregate events", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - const source = yield* events.replayAll([ + const source = yield* bus.replayAll([ { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "one"), }, { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, data: durableData(aggregateID, "two"), @@ -762,41 +762,41 @@ describe("EventV2", () => { it.effect("replayAll accepts later chunks after the first batch", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const aggregateID = Session.ID.create() - const one = yield* events.replayAll([ + const one = yield* bus.replayAll([ { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "one"), }, { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, data: durableData(aggregateID, "two"), }, ]) - const two = yield* events.replayAll([ + const two = yield* bus.replayAll([ { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 2, aggregateID, data: durableData(aggregateID, "three"), }, { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 3, aggregateID, data: durableData(aggregateID, "four"), @@ -817,22 +817,22 @@ describe("EventV2", () => { it.effect("claim fences replay owners", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const received = new Array() + const bus = yield* Bus.Service + const received = new Array() const aggregateID = Session.ID.create() - yield* events.publish(DurableMessage, durableData(aggregateID, "seed")) - yield* events.claim(aggregateID, "owner-a") - yield* events.project(DurableMessage, (event) => + yield* bus.publish(DurableMessage, durableData(aggregateID, "seed")) + yield* bus.claim(aggregateID, "owner-a") + yield* bus.project(DurableMessage, (event) => Effect.sync(() => { received.push(event) }), ) - yield* events.replay( + yield* bus.replay( { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, data: durableData(aggregateID, "ignored"), @@ -846,20 +846,20 @@ describe("EventV2", () => { it.effect("strict owner fences exact replay", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - const id = EventV2.ID.create() + const id = Event.ID.create() const replayed = { id, created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "owned"), } - yield* events.replay(replayed, { ownerID: "owner-a" }) + yield* bus.replay(replayed, { ownerID: "owner-a" }) - const exit = yield* events.replay(replayed, { ownerID: "owner-b", strictOwner: true }).pipe(Effect.exit) + const exit = yield* bus.replay(replayed, { ownerID: "owner-b", strictOwner: true }).pipe(Effect.exit) expect(String(exit)).toContain("Replay owner mismatch") }), @@ -867,20 +867,20 @@ describe("EventV2", () => { it.effect("exact replay claims an unowned aggregate", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const aggregateID = Session.ID.create() - const published = yield* events.publish(DurableMessage, durableData(aggregateID, "owned")) + const published = yield* bus.publish(DurableMessage, durableData(aggregateID, "owned")) const replayed = { id: published.id, created: published.created, - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: published.durable!.seq, aggregateID, data: published.data, } - yield* events.replay(replayed, { ownerID: "owner-a", strictOwner: true }) + yield* bus.replay(replayed, { ownerID: "owner-a", strictOwner: true }) const row = yield* db .select({ ownerID: EventSequenceTable.owner_id }) .from(EventSequenceTable) @@ -889,9 +889,9 @@ describe("EventV2", () => { .pipe(Effect.orDie) expect(row?.ownerID).toBe("owner-a") - const exit = yield* events + const exit = yield* bus .replay( - { ...replayed, id: EventV2.ID.create(), seq: 1, data: durableData(aggregateID, "conflict") }, + { ...replayed, id: Event.ID.create(), seq: 1, data: durableData(aggregateID, "conflict") }, { ownerID: "owner-b", strictOwner: true }, ) .pipe(Effect.exit) @@ -901,15 +901,15 @@ describe("EventV2", () => { it.effect("replay with owner claims an unowned sequence", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const aggregateID = Session.ID.create() - yield* events.replay( + yield* bus.replay( { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "owned"), @@ -929,27 +929,27 @@ describe("EventV2", () => { it.effect("replay claims an existing unowned sequence before fencing a different owner", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const aggregateID = Session.ID.create() - yield* events.publish(DurableMessage, durableData(aggregateID, "local")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "local")) - yield* events.replay( + yield* bus.replay( { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, data: durableData(aggregateID, "claimed"), }, { ownerID: "owner-1" }, ) - yield* events.replay( + yield* bus.replay( { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 2, aggregateID, data: durableData(aggregateID, "fenced"), @@ -976,13 +976,13 @@ describe("EventV2", () => { it.effect("strict replay rejects an owner conflict instead of silently skipping it", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - yield* events.replay( + yield* bus.replay( { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "claimed"), @@ -990,12 +990,12 @@ describe("EventV2", () => { { ownerID: "owner-1" }, ) - const exit = yield* events + const exit = yield* bus .replay( { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, data: durableData(aggregateID, "conflict"), @@ -1010,21 +1010,21 @@ describe("EventV2", () => { it.effect("publishes accepted replay with its durable sequence and suppresses stale replay", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const received = new Array() + const bus = yield* Bus.Service + const received = new Array() const aggregateID = Session.ID.create() - yield* events.listen((event) => Effect.sync(() => received.push(event))) + yield* bus.listen((event) => Effect.sync(() => received.push(event))) const replayed = { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "replayed"), } - yield* events.replay(replayed, { publish: true }) - yield* events.replay(replayed, { publish: true }) + yield* bus.replay(replayed, { publish: true }) + yield* bus.replay(replayed, { publish: true }) expect(received).toMatchObject([{ id: replayed.id, durable: { seq: 0, version: 1 }, data: replayed.data }]) }), @@ -1032,21 +1032,21 @@ describe("EventV2", () => { it.effect("rejects divergent stale replay without publishing it", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const received = new Array() + const bus = yield* Bus.Service + const received = new Array() const aggregateID = Session.ID.create() const replayed = { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "original"), } - yield* events.listen((event) => Effect.sync(() => received.push(event))) - yield* events.replay(replayed, { publish: true }) + yield* bus.listen((event) => Effect.sync(() => received.push(event))) + yield* bus.replay(replayed, { publish: true }) - const exit = yield* events + const exit = yield* bus .replay({ ...replayed, data: durableData(aggregateID, "divergent") }, { publish: true }) .pipe(Effect.exit) @@ -1057,23 +1057,23 @@ describe("EventV2", () => { it.effect("rejects an event ID reused at another aggregate position", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - const id = EventV2.ID.create() - yield* events.replay({ + const id = Event.ID.create() + yield* bus.replay({ id, created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "first"), }) - const exit = yield* events + const exit = yield* bus .replay({ id, created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, data: durableData(aggregateID, "second"), @@ -1086,28 +1086,28 @@ describe("EventV2", () => { it.effect("replay from a different owner leaves claimed sequence unchanged", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const aggregateID = Session.ID.create() - const received = new Array() - yield* events.listen((event) => Effect.sync(() => received.push(event))) + const received = new Array() + yield* bus.listen((event) => Effect.sync(() => received.push(event))) - yield* events.replay( + yield* bus.replay( { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "first"), }, { ownerID: "owner-1" }, ) - yield* events.replay( + yield* bus.replay( { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, data: durableData(aggregateID, "ignored"), @@ -1135,13 +1135,13 @@ describe("EventV2", () => { it.effect("claim updates the event sequence owner", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Event.ID.create() - yield* events.publish(SyncMessage, { id: aggregateID, text: "claimed" }) - yield* events.claim(aggregateID, "owner-1") - yield* events.claim(aggregateID, "owner-2") + yield* bus.publish(SyncMessage, { id: aggregateID, text: "claimed" }) + yield* bus.claim(aggregateID, "owner-1") + yield* bus.claim(aggregateID, "owner-2") const row = yield* db .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id }) .from(EventSequenceTable) @@ -1155,21 +1155,21 @@ describe("EventV2", () => { it.effect("remove clears durable event sequence", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const received = new Array() + const bus = yield* Bus.Service + const received = new Array() const aggregateID = Session.ID.create() - yield* events.publish(DurableMessage, durableData(aggregateID, "seed")) - yield* events.remove(aggregateID) - yield* events.project(DurableMessage, (event) => + yield* bus.publish(DurableMessage, durableData(aggregateID, "seed")) + yield* bus.remove(aggregateID) + yield* bus.project(DurableMessage, (event) => Effect.sync(() => { received.push(event) }), ) - yield* events.replay({ - id: EventV2.ID.create(), + yield* bus.replay({ + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "replayed"), @@ -1181,82 +1181,82 @@ describe("EventV2", () => { it.effect("log without follow replays events and completes with a synced marker", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - yield* events.publish(DurableMessage, durableData(aggregateID, "zero")) - yield* events.publish(DurableMessage, durableData(aggregateID, "one")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "zero")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "one")) - const items = Array.from(yield* Stream.runCollect(events.log({ aggregateID }))) + const items = Array.from(yield* Stream.runCollect(bus.log({ aggregateID }))) - expect(items.map((item) => (EventV2.isSynced(item) ? item.type : item.durable?.seq))).toEqual([ - EventV2.Seq.make(0), - EventV2.Seq.make(1), + expect(items.map((item) => (Bus.isSynced(item) ? item.type : item.durable?.seq))).toEqual([ + Event.Seq.make(0), + Event.Seq.make(1), "log.synced", ]) - expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID, seq: EventV2.Seq.make(1) }) + expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID, seq: Event.Seq.make(1) }) }), ) it.effect("log synced marker omits seq for an empty log and keeps the cursor otherwise", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - const empty = Array.from(yield* Stream.runCollect(events.log({ aggregateID }))) - yield* events.publish(DurableMessage, durableData(aggregateID, "zero")) - const drained = Array.from(yield* Stream.runCollect(events.log({ aggregateID, after: 0 }))) + const empty = Array.from(yield* Stream.runCollect(bus.log({ aggregateID }))) + yield* bus.publish(DurableMessage, durableData(aggregateID, "zero")) + const drained = Array.from(yield* Stream.runCollect(bus.log({ aggregateID, after: 0 }))) expect(empty).toEqual([{ type: "log.synced", aggregateID }]) expect(empty[0]).not.toHaveProperty("seq") - expect(drained).toEqual([{ type: "log.synced", aggregateID, seq: EventV2.Seq.make(0) }]) + expect(drained).toEqual([{ type: "log.synced", aggregateID, seq: Event.Seq.make(0) }]) }), ) it.effect("log with follow emits the synced marker at the replay-to-live boundary", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - yield* events.publish(DurableMessage, durableData(aggregateID, "zero")) - const fiber = yield* events + yield* bus.publish(DurableMessage, durableData(aggregateID, "zero")) + const fiber = yield* bus .log({ aggregateID, follow: true }) .pipe(Stream.take(3), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow - yield* events.publish(DurableMessage, durableData(aggregateID, "one")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "one")) const items = Array.from(yield* Fiber.join(fiber)) - expect(items.map((item) => (EventV2.isSynced(item) ? item : item.durable?.seq))).toEqual([ - EventV2.Seq.make(0), - { type: "log.synced", aggregateID, seq: EventV2.Seq.make(0) }, - EventV2.Seq.make(1), + expect(items.map((item) => (Bus.isSynced(item) ? item : item.durable?.seq))).toEqual([ + Event.Seq.make(0), + { type: "log.synced", aggregateID, seq: Event.Seq.make(0) }, + Event.Seq.make(1), ]) }), ) it.effect("log replays across configured read pages", () => Effect.gen(function* () { - const eventLayer = EventV2.layerWith({ logReadPageSize: 2 }).pipe(Layer.provide(LayerNode.compile(Database.node))) + const eventLayer = Bus.layerWith({ logReadPageSize: 2 }).pipe(Layer.provide(LayerNode.compile(Database.node))) yield* Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - yield* events.publish(DurableMessage, durableData(aggregateID, "zero")) - yield* events.publish(DurableMessage, durableData(aggregateID, "one")) - yield* events.publish(DurableMessage, durableData(aggregateID, "two")) - yield* events.publish(DurableMessage, durableData(aggregateID, "three")) - yield* events.publish(DurableMessage, durableData(aggregateID, "four")) - - const items = Array.from(yield* Stream.runCollect(events.log({ aggregateID }))) - - expect(items.map((item) => (EventV2.isSynced(item) ? item.type : item.durable?.seq))).toEqual([ - EventV2.Seq.make(0), - EventV2.Seq.make(1), - EventV2.Seq.make(2), - EventV2.Seq.make(3), - EventV2.Seq.make(4), + yield* bus.publish(DurableMessage, durableData(aggregateID, "zero")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "one")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "two")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "three")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "four")) + + const items = Array.from(yield* Stream.runCollect(bus.log({ aggregateID }))) + + expect(items.map((item) => (Bus.isSynced(item) ? item.type : item.durable?.seq))).toEqual([ + Event.Seq.make(0), + Event.Seq.make(1), + Event.Seq.make(2), + Event.Seq.make(3), + Event.Seq.make(4), "log.synced", ]) - expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID, seq: EventV2.Seq.make(4) }) + expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID, seq: Event.Seq.make(4) }) }).pipe(Effect.provide(Layer.merge(LayerNode.compile(Database.node), eventLayer))) }), ) @@ -1266,7 +1266,7 @@ describe("EventV2", () => { const readStarted = yield* Deferred.make() const releaseRead = yield* Deferred.make() const firstRead = yield* Ref.make(true) - const eventLayer = EventV2.layerWith({ + const eventLayer = Bus.layerWith({ beforeAggregateRead: () => Ref.getAndSet(firstRead, false).pipe( Effect.flatMap((shouldBlock) => { @@ -1277,22 +1277,22 @@ describe("EventV2", () => { }).pipe(Layer.provide(LayerNode.compile(Database.node))) yield* Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - yield* events.publish(DurableMessage, durableData(aggregateID, "zero")) - const fiber = yield* events + yield* bus.publish(DurableMessage, durableData(aggregateID, "zero")) + const fiber = yield* bus .log({ aggregateID, follow: true }) .pipe(Stream.take(3), Stream.runCollect, Effect.forkScoped) yield* Deferred.await(readStarted) - yield* events.publish(DurableMessage, durableData(aggregateID, "one")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "one")) yield* Deferred.succeed(releaseRead, undefined) const items = Array.from(yield* Fiber.join(fiber)) - expect(items.map((item) => (EventV2.isSynced(item) ? item : item.durable?.seq))).toEqual([ - EventV2.Seq.make(0), - { type: "log.synced", aggregateID, seq: EventV2.Seq.make(0) }, - EventV2.Seq.make(1), + expect(items.map((item) => (Bus.isSynced(item) ? item : item.durable?.seq))).toEqual([ + Event.Seq.make(0), + { type: "log.synced", aggregateID, seq: Event.Seq.make(0) }, + Event.Seq.make(1), ]) }).pipe(Effect.provide(Layer.merge(LayerNode.compile(Database.node), eventLayer))) }), @@ -1300,22 +1300,22 @@ describe("EventV2", () => { it.effect("sequences returns the latest committed seq per aggregate and omits unknown aggregates", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const first = Session.ID.create() const second = Session.ID.create() - yield* events.publish(DurableMessage, durableData(first, "zero")) - yield* events.publish(DurableMessage, durableData(first, "one")) - yield* events.publish(DurableMessage, durableData(second, "zero")) + yield* bus.publish(DurableMessage, durableData(first, "zero")) + yield* bus.publish(DurableMessage, durableData(first, "one")) + yield* bus.publish(DurableMessage, durableData(second, "zero")) - const sequences = yield* events.sequences([first, second, Session.ID.create()]) + const sequences = yield* bus.sequences([first, second, Session.ID.create()]) expect(sequences).toEqual( new Map([ - [first, EventV2.Seq.make(1)], - [second, EventV2.Seq.make(0)], + [first, Event.Seq.make(1)], + [second, Event.Seq.make(0)], ]), ) - expect(yield* events.sequences([])).toEqual(new Map()) + expect(yield* bus.sequences([])).toEqual(new Map()) }), ) }) diff --git a/packages/core/test/catalog.test.ts b/packages/core/test/catalog.test.ts index 502a46b7d415..8860cd90453b 100644 --- a/packages/core/test/catalog.test.ts +++ b/packages/core/test/catalog.test.ts @@ -7,10 +7,10 @@ import { Integration } from "@opencode-ai/core/integration" import { Credential } from "@opencode-ai/core/credential" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Location } from "@opencode-ai/core/location" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" import { location } from "./fixture/location" import { testEffect } from "./lib/effect" @@ -25,22 +25,22 @@ const locationLayer = Layer.succeed( Location.Service.of(location({ directory: AbsolutePath.make("test") })), ) const catalogLayer = AppNodeBuilder.build( - LayerNode.group([Catalog.node, EventV2.node, Credential.node, Integration.node]), + LayerNode.group([Catalog.node, Bus.node, Credential.node, Integration.node]), [[Location.node, locationLayer]], ) const it = testEffect(catalogLayer) -describe("CatalogV2", () => { +describe("Catalog", () => { it.effect("publishes an updated event after catalog changes", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const events = yield* EventV2.Service - const updated = yield* events + const bus = yield* Bus.Service + const updated = yield* bus .subscribe(Catalog.Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow - yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {})) + yield* catalog.transform((editor) => editor.provider.update(Provider.ID.make("test"), () => {})) expect((yield* Fiber.join(updated)).length).toBe(1) }), @@ -55,28 +55,28 @@ describe("CatalogV2", () => { return Effect.gen(function* () { const catalog = yield* Catalog.Service const credentials = yield* Credential.Service - yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {})) + yield* catalog.transform((editor) => editor.provider.update(Provider.ID.make("test"), () => {})) yield* credentials.create({ integrationID, label: "First", value: Credential.Key.make({ type: "key", key: "first", metadata: { tenant: "one" } }), }) - expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")]) - expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).body).toBeUndefined() + expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([Provider.ID.make("test")]) + expect(required(yield* catalog.provider.get(Provider.ID.make("test"))).body).toBeUndefined() yield* credentials.create({ integrationID, label: "Second", value: Credential.Key.make({ type: "key", key: "second", metadata: { tenant: "two" } }), }) - expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")]) - expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).body).toBeUndefined() + expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([Provider.ID.make("test")]) + expect(required(yield* catalog.provider.get(Provider.ID.make("test"))).body).toBeUndefined() }).pipe(Effect.provide(localCatalogLayer)) }) it.effect("derives availability from a provider's integration", () => { const integrationID = Integration.ID.make("gateway") - const providerID = ProviderV2.ID.make("remote") + const providerID = Provider.ID.make("remote") const localCatalogLayer = Layer.fresh( AppNodeBuilder.build(LayerNode.group([Catalog.node, Credential.node, Integration.node]), [ [Location.node, locationLayer], @@ -113,7 +113,7 @@ describe("CatalogV2", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service const integrations = yield* Integration.Service - const providerID = ProviderV2.ID.make("test") + const providerID = Provider.ID.make("test") yield* integrations.transform((editor) => editor.method.update({ integrationID: Integration.ID.make(providerID), @@ -135,16 +135,16 @@ describe("CatalogV2", () => { it.effect("stores provider package settings", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.make("test") + const providerID = Provider.ID.make("test") yield* catalog.transform((catalog) => catalog.provider.update(providerID, (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://override.example.com" } }), ) expect(required(yield* catalog.provider.get(providerID))).toMatchObject({ - package: ProviderV2.aisdk("@ai-sdk/openai-compatible"), + package: Provider.aisdk("@ai-sdk/openai-compatible"), settings: { baseURL: "https://override.example.com" }, }) }), @@ -153,23 +153,23 @@ describe("CatalogV2", () => { it.effect("uses model package settings over provider settings", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.make("test") - const modelID = ModelV2.ID.make("model") + const providerID = Provider.ID.make("test") + const modelID = Model.ID.make("model") yield* catalog.transform((catalog) => { catalog.provider.update(providerID, (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://provider.example.com" } }) catalog.model.update(providerID, modelID, (model) => { - model.modelID = ModelV2.ID.make("upstream-model") - model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + model.modelID = Model.ID.make("upstream-model") + model.package = Provider.aisdk("@ai-sdk/openai-compatible") model.settings = { baseURL: "https://override.example.com" } }) }) expect(required(yield* catalog.model.get(providerID, modelID))).toMatchObject({ - modelID: ModelV2.ID.make("upstream-model"), - package: ProviderV2.aisdk("@ai-sdk/openai-compatible"), + modelID: Model.ID.make("upstream-model"), + package: Provider.aisdk("@ai-sdk/openai-compatible"), settings: { baseURL: "https://override.example.com" }, }) }), @@ -178,18 +178,18 @@ describe("CatalogV2", () => { it.effect("resolves default model package settings from the provider", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.make("test") - const modelID = ModelV2.ID.make("model") + const providerID = Provider.ID.make("test") + const modelID = Model.ID.make("model") yield* catalog.transform((catalog) => { catalog.provider.update(providerID, (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://provider.example.com" } }) catalog.model.update(providerID, modelID, () => {}) }) expect(required(yield* catalog.model.get(providerID, modelID))).toMatchObject({ - package: ProviderV2.aisdk("@ai-sdk/openai-compatible"), + package: Provider.aisdk("@ai-sdk/openai-compatible"), settings: { baseURL: "https://provider.example.com" }, }) }), @@ -198,8 +198,8 @@ describe("CatalogV2", () => { it.effect("resolves provider and model overlay merges", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.make("test") - const modelID = ModelV2.ID.make("model") + const providerID = Provider.ID.make("test") + const modelID = Model.ID.make("model") yield* catalog.transform((catalog) => { catalog.provider.update(providerID, (provider) => { provider.settings = { provider: true, shared: "provider" } @@ -223,13 +223,13 @@ describe("CatalogV2", () => { it.effect("falls back to newest available model when no default is configured", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.make("test") + const providerID = Provider.ID.make("test") yield* catalog.transform((catalog) => { catalog.provider.update(providerID, () => {}) - catalog.model.update(providerID, ModelV2.ID.make("old"), (model) => { + catalog.model.update(providerID, Model.ID.make("old"), (model) => { model.time.released = 1000 }) - catalog.model.update(providerID, ModelV2.ID.make("new"), (model) => { + catalog.model.update(providerID, Model.ID.make("new"), (model) => { model.time.released = 2000 }) }) @@ -241,9 +241,9 @@ describe("CatalogV2", () => { it.effect("uses a transform-provided default model until that transform is replaced", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.make("test") - const old = ModelV2.ID.make("old") - const newest = ModelV2.ID.make("new") + const providerID = Provider.ID.make("test") + const old = Model.ID.make("old") + const newest = Model.ID.make("new") const models = (catalog: Catalog.Draft) => { catalog.provider.update(providerID, () => {}) catalog.model.update(providerID, old, (model) => { @@ -272,10 +272,10 @@ describe("CatalogV2", () => { it.effect("ignores a configured default on a disabled provider", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const disabledProvider = ProviderV2.ID.make("disabled") - const enabledProvider = ProviderV2.ID.make("enabled") - const disabledModel = ModelV2.ID.make("configured") - const fallbackModel = ModelV2.ID.make("fallback") + const disabledProvider = Provider.ID.make("disabled") + const enabledProvider = Provider.ID.make("enabled") + const disabledModel = Model.ID.make("configured") + const fallbackModel = Model.ID.make("fallback") yield* catalog.transform((catalog) => { catalog.provider.update(disabledProvider, (provider) => { provider.disabled = true @@ -296,10 +296,10 @@ describe("CatalogV2", () => { it.effect("small model prefers small keyword candidates before cost scoring", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.make("test") + const providerID = Provider.ID.make("test") yield* catalog.transform((catalog) => { catalog.provider.update(providerID, () => {}) - catalog.model.update(providerID, ModelV2.ID.make("cheap-large"), (model) => { + catalog.model.update(providerID, Model.ID.make("cheap-large"), (model) => { model.capabilities.input = ["text"] model.capabilities.output = ["text"] model.cost = [ @@ -314,7 +314,7 @@ describe("CatalogV2", () => { ] model.time.released = Date.now() }) - catalog.model.update(providerID, ModelV2.ID.make("expensive-mini"), (model) => { + catalog.model.update(providerID, Model.ID.make("expensive-mini"), (model) => { model.capabilities.input = ["text"] model.capabilities.output = ["text"] model.cost = [ diff --git a/packages/core/test/codemode.test.ts b/packages/core/test/codemode.test.ts index 9d9f8ccfd3c2..0de0096b188d 100644 --- a/packages/core/test/codemode.test.ts +++ b/packages/core/test/codemode.test.ts @@ -1,34 +1,32 @@ import { describe, expect } from "bun:test" -import { CodeMode } from "@opencode-ai/core/codemode" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { Tool } from "@opencode-ai/core/tool/tool" +import { Tool } from "@opencode-ai/core/tool" import { Effect, Schema } from "effect" import { it } from "./lib/effect" describe("CodeMode", () => { it.effect("owns registrations, execute, and catalog materialization", () => Effect.gen(function* () { - const codeMode = yield* CodeMode.Service - yield* codeMode.register( - Tool.registrationEntries({ - echo: Tool.make({ + const tools = yield* Tool.Service + yield* tools.transform((draft) => + draft.add({ + name: "echo", description: "Echo text", input: Schema.Struct({ text: Schema.String }), output: Schema.String, execute: ({ text }) => Effect.succeed({ output: text }), - }), }), ) - const materialized = yield* codeMode.materialize() - expect(materialized.tool).toBeDefined() - expect(materialized.catalog).toStrictEqual([ + const snapshot = yield* tools.snapshot() + expect(snapshot.definitions.some((tool) => tool.name === "execute")).toBe(true) + expect(snapshot.codeModeCatalog).toStrictEqual([ { path: "echo", description: "Echo text", signature: "tools.echo(input: {\n text: string,\n}): Promise", }, ]) - }).pipe(Effect.scoped, Effect.provide(AppNodeBuilder.build(CodeMode.node))), + }).pipe(Effect.scoped, Effect.provide(AppNodeBuilder.build(Tool.node))), ) }) diff --git a/packages/core/test/codemode/instructions.test.ts b/packages/core/test/codemode/instructions.test.ts index 44f46a9837af..fcb6fe730a58 100644 --- a/packages/core/test/codemode/instructions.test.ts +++ b/packages/core/test/codemode/instructions.test.ts @@ -1,9 +1,8 @@ import { describe, expect } from "bun:test" -import { CodeMode } from "@opencode-ai/core/codemode" import { CodeModeCatalog } from "@opencode-ai/core/codemode/catalog" import { CodeModeInstructions } from "@opencode-ai/core/codemode/instructions" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { Tool } from "@opencode-ai/core/tool/tool" +import { Tool } from "@opencode-ai/core/tool" import { Effect, Schema } from "effect" import { it } from "../lib/effect" import { readInitial, readUpdate } from "../lib/instructions" @@ -66,32 +65,45 @@ describe("CodeModeInstructions", () => { ) it.effect("stores a canonical sorted snapshot so registration order does not churn history", () => { - const alpha = Tool.make({ + const alpha = ({ + name: "alpha", description: "Alpha tool", input: Schema.Struct({}), output: Schema.String, execute: () => Effect.succeed({ output: "alpha" }), }) - const zeta = Tool.make({ + const zeta = ({ + name: "zeta", description: "Zeta tool", input: Schema.Struct({}), output: Schema.String, execute: () => Effect.succeed({ output: "zeta" }), }) - const layer = AppNodeBuilder.build(CodeMode.node) + const layer = AppNodeBuilder.build(Tool.node) return Effect.gen(function* () { - const codeMode = yield* CodeMode.Service + const tools = yield* Tool.Service const initialized = yield* Effect.scoped( Effect.gen(function* () { - yield* codeMode.register(Tool.registrationEntries({ zeta, alpha }, { namespace: "tools" })) - return yield* readInitial(CodeModeInstructions.make((yield* codeMode.materialize()).catalog)) + yield* tools.transform((draft) => { + draft.add({ ...zeta, options: { namespace: "tools" } }) + draft.add({ ...alpha, options: { namespace: "tools" } }) + }) + return yield* readInitial( + CodeModeInstructions.make((yield* tools.snapshot()).codeModeCatalog), + ) }), ) const reordered = yield* Effect.scoped( Effect.gen(function* () { - yield* codeMode.register(Tool.registrationEntries({ alpha, zeta }, { namespace: "tools" })) - return yield* readUpdate(CodeModeInstructions.make((yield* codeMode.materialize()).catalog), initialized) + yield* tools.transform((draft) => { + draft.add({ ...alpha, options: { namespace: "tools" } }) + draft.add({ ...zeta, options: { namespace: "tools" } }) + }) + return yield* readUpdate( + CodeModeInstructions.make((yield* tools.snapshot()).codeModeCatalog), + initialized, + ) }), ) diff --git a/packages/core/test/command.test.ts b/packages/core/test/command.test.ts index 8fb243213923..11ac774b9736 100644 --- a/packages/core/test/command.test.ts +++ b/packages/core/test/command.test.ts @@ -1,27 +1,27 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" -import { CommandV2 } from "@opencode-ai/core/command" +import { Command } from "@opencode-ai/core/command" import { Config } from "@opencode-ai/core/config" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Location } from "@opencode-ai/core/location" import { MCP } from "@opencode-ai/core/mcp/index" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { emptyConfigLayer, emptyMcpLayer, testLocationLayer } from "./fixture/mcp" import { testEffect } from "./lib/effect" const it = testEffect( - AppNodeBuilder.build(CommandV2.node, [ + AppNodeBuilder.build(Command.node, [ [MCP.node, emptyMcpLayer], [Config.node, emptyConfigLayer], [Location.node, testLocationLayer], ]), ) -describe("CommandV2", () => { +describe("Command", () => { it.effect("applies command transforms and preserves later overrides", () => Effect.gen(function* () { - const command = yield* CommandV2.Service + const command = yield* Command.Service yield* command.transform((editor) => { editor.update("review", (command) => { command.template = "First" @@ -30,34 +30,34 @@ describe("CommandV2", () => { editor.update("review", (command) => { command.template = "Second" command.model = { - id: ModelV2.ID.make("claude"), - providerID: ProviderV2.ID.make("anthropic"), - variant: ModelV2.VariantID.make("high"), + id: Model.ID.make("claude"), + providerID: Provider.ID.make("anthropic"), + variant: Model.VariantID.make("high"), } }) }) expect(yield* command.get("review")).toEqual( - CommandV2.Info.make({ + Command.Info.make({ name: "review", template: "Second", description: "Review code", model: { - id: ModelV2.ID.make("claude"), - providerID: ProviderV2.ID.make("anthropic"), - variant: ModelV2.VariantID.make("high"), + id: Model.ID.make("claude"), + providerID: Provider.ID.make("anthropic"), + variant: Model.VariantID.make("high"), }, }), ) expect(yield* command.list()).toEqual([ - CommandV2.Info.make({ + Command.Info.make({ name: "review", template: "Second", description: "Review code", model: { - id: ModelV2.ID.make("claude"), - providerID: ProviderV2.ID.make("anthropic"), - variant: ModelV2.VariantID.make("high"), + id: Model.ID.make("claude"), + providerID: Provider.ID.make("anthropic"), + variant: Model.VariantID.make("high"), }, }), ]) @@ -66,7 +66,7 @@ describe("CommandV2", () => { it.effect("evaluates command template shell blocks", () => Effect.gen(function* () { - const command = yield* CommandV2.Service + const command = yield* Command.Service yield* command.transform((editor) => { editor.update("review", (command) => { command.template = "Output: !`echo command-output`" diff --git a/packages/core/test/config/agent.test.ts b/packages/core/test/config/agent.test.ts index 855ea370e2f7..a8b393bfb3d2 100644 --- a/packages/core/test/config/agent.test.ts +++ b/packages/core/test/config/agent.test.ts @@ -2,26 +2,26 @@ import { describe, expect, test } from "bun:test" import fs from "fs/promises" import path from "path" import { Effect, Schema } from "effect" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Config } from "@opencode-ai/core/config" import { ConfigAgentPlugin } from "@opencode-ai/core/config/plugin/agent" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { FSUtil } from "@opencode-ai/util/fs-util" import { Global } from "@opencode-ai/util/global" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { AbsolutePath } from "@opencode-ai/core/schema" import { ConfigMigrateV1 } from "@opencode-ai/core/v1/config/migrate" import { tmpdir } from "../fixture/tmpdir" import { testEffect } from "../lib/effect" import { agentHost, host } from "../plugin/host" -const it = testEffect(AppNodeBuilder.build(LayerNode.group([AgentV2.node, FSUtil.node, Global.node]))) +const it = testEffect(AppNodeBuilder.build(LayerNode.group([Agent.node, FSUtil.node, Global.node]))) const decode = Schema.decodeUnknownSync(Config.Info) const defaultPermissions = [ { action: "*", resource: "*", effect: "allow" }, { action: "external_directory", resource: "*", effect: "ask" }, -] satisfies PermissionV2.Ruleset +] satisfies Permission.Ruleset test("rejects named agent color tokens", () => { expect(() => decode({ agents: { reviewer: { color: "warning" } } })).toThrow() @@ -31,15 +31,15 @@ describe("ConfigAgentPlugin.Plugin", () => { it.effect("matches POSIX paths against home-relative permissions", () => Effect.gen(function* () { const permissions = yield* loadHomePermissions("/home/test") - expect(PermissionV2.evaluate("external_directory", "/home/test/p/opencode/src/*", permissions).effect).toBe( + expect(Permission.evaluate("external_directory", "/home/test/p/opencode/src/*", permissions).effect).toBe( "allow", ) - expect(PermissionV2.evaluate("external_directory", "/home/test/cache/files/*", permissions).effect).toBe("deny") - expect(PermissionV2.evaluate("external_directory", "/some/~/path", permissions).effect).toBe("deny") - expect(PermissionV2.evaluate("external_directory", "$HOMELESS/private/*", permissions).effect).toBe("deny") + expect(Permission.evaluate("external_directory", "/home/test/cache/files/*", permissions).effect).toBe("deny") + expect(Permission.evaluate("external_directory", "/some/~/path", permissions).effect).toBe("deny") + expect(Permission.evaluate("external_directory", "$HOMELESS/private/*", permissions).effect).toBe("deny") expect(permissions).toContainEqual({ action: "shell", resource: "$HOME/private/**", effect: "deny" }) expect(permissions).not.toContainEqual({ action: "shell", resource: "/home/test/private/**", effect: "deny" }) - expect(PermissionV2.evaluate("shell", "$HOME/private/key", permissions).effect).toBe("deny") + expect(Permission.evaluate("shell", "$HOME/private/key", permissions).effect).toBe("deny") }), ) @@ -47,9 +47,9 @@ describe("ConfigAgentPlugin.Plugin", () => { Effect.gen(function* () { const permissions = yield* loadHomePermissions("C:\\Users\\test") expect( - PermissionV2.evaluate("external_directory", "C:\\Users\\test\\p\\opencode\\src\\*", permissions).effect, + Permission.evaluate("external_directory", "C:\\Users\\test\\p\\opencode\\src\\*", permissions).effect, ).toBe("allow") - expect(PermissionV2.evaluate("external_directory", "C:\\Users\\test\\cache\\files\\*", permissions).effect).toBe( + expect(Permission.evaluate("external_directory", "C:\\Users\\test\\cache\\files\\*", permissions).effect).toBe( "deny", ) }), @@ -57,8 +57,8 @@ describe("ConfigAgentPlugin.Plugin", () => { it.effect("applies all global permissions before agent-specific permissions", () => Effect.gen(function* () { - const agents = yield* AgentV2.Service - const build = AgentV2.ID.make("build") + const agents = yield* Agent.Service + const build = Agent.ID.make("build") yield* agents.transform((editor) => editor.update(build, (agent) => { agent.mode = "primary" @@ -119,10 +119,10 @@ describe("ConfigAgentPlugin.Plugin", () => { { action: "read", resource: "*", effect: "allow" }, { action: "bash", resource: "git *", effect: "allow" }, ]) - expect(PermissionV2.evaluate("bash", "git status", buildAgent.permissions).effect).toBe("allow") - expect(PermissionV2.evaluate("bash", "bun test", buildAgent.permissions).effect).toBe("ask") + expect(Permission.evaluate("bash", "git status", buildAgent.permissions).effect).toBe("allow") + expect(Permission.evaluate("bash", "bun test", buildAgent.permissions).effect).toBe("ask") - const reviewer = yield* agents.get(AgentV2.ID.make("reviewer")) + const reviewer = yield* agents.get(Agent.ID.make("reviewer")) if (!reviewer) throw new Error("expected configured reviewer agent") expect(reviewer).toMatchObject({ description: "Review changes", @@ -137,20 +137,20 @@ describe("ConfigAgentPlugin.Plugin", () => { { action: "edit", resource: "*", effect: "deny" }, { action: "read", resource: "*", effect: "deny" }, ]) - expect(PermissionV2.evaluate("read", "README.md", reviewer.permissions).effect).toBe("deny") - expect((yield* agents.get(AgentV2.ID.make("late")))?.permissions).toEqual([ + expect(Permission.evaluate("read", "README.md", reviewer.permissions).effect).toBe("deny") + expect((yield* agents.get(Agent.ID.make("late")))?.permissions).toEqual([ ...defaultPermissions, { action: "bash", resource: "*", effect: "ask" }, { action: "read", resource: "*", effect: "allow" }, { action: "edit", resource: "*", effect: "allow" }, ]) - expect(yield* agents.get(AgentV2.ID.make("removed"))).toBeUndefined() + expect(yield* agents.get(Agent.ID.make("removed"))).toBeUndefined() }), ) it.effect("maps configured agent fields and preserves an unspecified model variant", () => Effect.gen(function* () { - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const config = Config.Service.of({ entries: () => Effect.succeed([ @@ -194,7 +194,7 @@ describe("ConfigAgentPlugin.Plugin", () => { Effect.provideService(Config.Service, config), ) - const reviewer = yield* agents.get(AgentV2.ID.make("reviewer")) + const reviewer = yield* agents.get(Agent.ID.make("reviewer")) if (!reviewer) throw new Error("expected configured reviewer agent") expect(reviewer).toMatchObject({ system: "Review carefully.", @@ -215,8 +215,8 @@ describe("ConfigAgentPlugin.Plugin", () => { it.effect("removes a built-in agent disabled by configuration", () => Effect.gen(function* () { - const agents = yield* AgentV2.Service - const build = AgentV2.ID.make("build") + const agents = yield* Agent.Service + const build = Agent.ID.make("build") yield* agents.transform((editor) => editor.update(build, () => {})) const config = Config.Service.of({ @@ -277,7 +277,7 @@ Use native v2 fields.`, await fs.writeFile(path.join(tmp.path, "agents", "disabled.md"), "---\ndisabled: true\n---\nDisabled") await fs.writeFile(path.join(tmp.path, "modes", "plan.md"), "Make a plan.") }) - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const config = Config.Service.of({ entries: () => Effect.succeed([ @@ -293,21 +293,21 @@ Use native v2 fields.`, Effect.provideService(Config.Service, config), ) - expect(yield* agents.get(AgentV2.ID.make("reviewer"))).toMatchObject({ + expect(yield* agents.get(Agent.ID.make("reviewer"))).toMatchObject({ model: { providerID: "openrouter", id: "openai/gpt-5" }, system: "Review carefully.", description: "Markdown description", request: { body: { temperature: 0.5 } }, permissions: [...defaultPermissions, { action: "edit", resource: "*", effect: "deny" }], }) - expect(yield* agents.get(AgentV2.ID.make("team/helper"))).toMatchObject({ system: "Help the team." }) - expect(yield* agents.get(AgentV2.ID.make("native"))).toMatchObject({ + expect(yield* agents.get(Agent.ID.make("team/helper"))).toMatchObject({ system: "Help the team." }) + expect(yield* agents.get(Agent.ID.make("native"))).toMatchObject({ system: "Use native v2 fields.", request: { headers: { "x-agent": "native" }, body: { effort: "high" } }, permissions: [...defaultPermissions, { action: "edit", resource: "*", effect: "deny" }], }) - expect(yield* agents.get(AgentV2.ID.make("disabled"))).toBeUndefined() - expect(yield* agents.get(AgentV2.ID.make("plan"))).toMatchObject({ system: "Make a plan.", mode: "primary" }) + expect(yield* agents.get(Agent.ID.make("disabled"))).toBeUndefined() + expect(yield* agents.get(Agent.ID.make("plan"))).toMatchObject({ system: "Make a plan.", mode: "primary" }) }), ), ), @@ -316,8 +316,8 @@ Use native v2 fields.`, function loadHomePermissions(home: string) { return Effect.gen(function* () { - const agents = yield* AgentV2.Service - const build = AgentV2.ID.make("build") + const agents = yield* Agent.Service + const build = Agent.ID.make("build") yield* agents.transform((editor) => editor.update(build, () => {})) const config = Config.Service.of({ entries: () => diff --git a/packages/core/test/config/command.test.ts b/packages/core/test/config/command.test.ts index fced5f83dbe6..aa3cf4914509 100644 --- a/packages/core/test/config/command.test.ts +++ b/packages/core/test/config/command.test.ts @@ -3,18 +3,18 @@ import path from "path" import { describe, expect } from "bun:test" import { Effect, PubSub, Schema, Stream } from "effect" import { Config as ConfigSchema } from "@opencode-ai/schema/config" -import { CommandV2 } from "@opencode-ai/core/command" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Command } from "@opencode-ai/core/command" +import { Agent } from "@opencode-ai/core/agent" import { Config } from "@opencode-ai/core/config" import { ConfigCommandPlugin } from "@opencode-ai/core/config/plugin/command" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { FSUtil } from "@opencode-ai/util/fs-util" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Location } from "@opencode-ai/core/location" import { MCP } from "@opencode-ai/core/mcp/index" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" import { emptyConfigLayer, emptyMcpLayer, testLocationLayer } from "../fixture/mcp" import { tmpdir } from "../fixture/tmpdir" @@ -22,7 +22,7 @@ import { testEffect } from "../lib/effect" import { host } from "../plugin/host" const it = testEffect( - AppNodeBuilder.build(LayerNode.group([CommandV2.node, EventV2.node, FSUtil.node]), [ + AppNodeBuilder.build(LayerNode.group([Command.node, Bus.node, FSUtil.node]), [ [MCP.node, emptyMcpLayer], [Config.node, emptyConfigLayer], [Location.node, testLocationLayer], @@ -54,9 +54,9 @@ Review files`, await fs.writeFile(path.join(tmp.path, "commands", "empty.md"), "") }) - const command = yield* CommandV2.Service - const events = yield* EventV2.Service - const update = yield* events.publish(ConfigSchema.Event.Updated, {}) + const command = yield* Command.Service + const bus = yield* Bus.Service + const update = yield* bus.publish(ConfigSchema.Event.Updated, {}) const updates = yield* PubSub.unbounded() yield* ConfigCommandPlugin.Plugin.effect( host({ @@ -84,20 +84,20 @@ Review files`, ) expect(yield* command.list()).toEqual([ - CommandV2.Info.make({ + Command.Info.make({ name: "review", template: "Review files", description: "File review", - agent: AgentV2.ID.make("reviewer"), + agent: Agent.ID.make("reviewer"), model: { - providerID: ProviderV2.ID.make("anthropic"), - id: ModelV2.ID.make("claude"), - variant: ModelV2.VariantID.make("high"), + providerID: Provider.ID.make("anthropic"), + id: Model.ID.make("claude"), + variant: Model.VariantID.make("high"), }, subtask: true, }), - CommandV2.Info.make({ name: "empty", template: "" }), - CommandV2.Info.make({ name: "nested/docs", template: "Write docs" }), + Command.Info.make({ name: "empty", template: "" }), + Command.Info.make({ name: "nested/docs", template: "Write docs" }), ]) yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "commands", "review.md"), "Review again")) diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index b826da794d14..5c94cc59ea66 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -15,11 +15,11 @@ import { ConfigMigrateV1 } from "@opencode-ai/core/v1/config/migrate" import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { FSUtil } from "@opencode-ai/util/fs-util" import { Watcher } from "@opencode-ai/core/filesystem/watcher" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Global } from "@opencode-ai/util/global" import { Location } from "@opencode-ai/core/location" import { Project } from "@opencode-ai/core/project" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" import { WellKnown } from "@opencode-ai/core/wellknown" import { Integration } from "@opencode-ai/schema/integration" @@ -81,7 +81,7 @@ function testLayer( ), ), ) - return AppNodeBuilder.build(LayerNode.group([Config.node, EventV2.node]), [ + return AppNodeBuilder.build(LayerNode.group([Config.node, Bus.node]), [ [Config.node, Config.configured(options)], [Location.node, locationLayer], [Global.node, Global.layerWith({ config: globalDirectory, home: path.join(globalDirectory, "home") })], @@ -194,8 +194,8 @@ describe("Config", () => { return yield* Effect.gen(function* () { const config = yield* Config.Service - const events = yield* EventV2.Service - const changed = yield* events + const bus = yield* Bus.Service + const changed = yield* bus .subscribe(ConfigSchema.Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.sleep("10 millis") @@ -298,14 +298,14 @@ describe("Config", () => { return yield* Effect.gen(function* () { const config = yield* Config.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service expect(Config.latest(yield* config.entries(), "shell")).toBe("secret") - const updated = yield* events + const updated = yield* bus .subscribe(ConfigSchema.Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow key = "next" - yield* events.publish(Integration.Event.ConnectionUpdated, { integrationID }) + yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID }) expect(yield* Fiber.join(updated)).toHaveLength(1) expect(Config.latest(yield* config.entries(), "shell")).toBe("next") }).pipe( @@ -331,7 +331,7 @@ describe("Config", () => { // V1 lists servers directly under `mcp`, so a file with only `$schema` + `mcp` still migrates. expect(ConfigMigrateV1.isV1({ mcp: { context7: { type: "local", command: ["npx"] } } })).toBe(true) expect(ConfigMigrateV1.isV1({ $schema: "x", mcp: { executor: { type: "remote", url: "https://x" } } })).toBe(true) - // V2 nests under `mcp.servers`, so it must not be misdetected and re-migrated. + // Current config nests under `mcp.servers`, so it must not be misdetected and re-migrated. expect(ConfigMigrateV1.isV1({ mcp: { servers: { context7: { type: "local", command: ["npx"] } } } })).toBe(false) expect(ConfigMigrateV1.isV1({ mcp: {} })).toBe(false) expect(ConfigMigrateV1.isV1({ mcp: { timeout: { execution: 1000 } } })).toBe(false) @@ -396,7 +396,7 @@ describe("Config", () => { }) expect(migrated.providers?.bedrock).toMatchObject({ - package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock"), + package: Provider.aisdk("@ai-sdk/amazon-bedrock"), settings: { region: "us-east-1", profile: "dev" }, headers: { "x-test": "1" }, body: { trace: true }, @@ -1041,7 +1041,7 @@ describe("Config", () => { }, }) expect(documents[0]?.info.providers?.openai).toMatchObject({ - package: ProviderV2.aisdk("@ai-sdk/openai"), + package: Provider.aisdk("@ai-sdk/openai"), settings: { apiKey: "secret", organization: "org" }, models: { model: { @@ -1051,7 +1051,7 @@ describe("Config", () => { }, }) expect(documents[0]?.info.providers?.anthropic).toMatchObject({ - package: ProviderV2.aisdk("@ai-sdk/anthropic"), + package: Provider.aisdk("@ai-sdk/anthropic"), models: { model: { settings: { diff --git a/packages/core/test/config/fixtures/plugin/directory-plugin.ts b/packages/core/test/config/fixtures/plugin/directory-plugin.ts index f5e15c2c00a2..a9d7628e47d5 100644 --- a/packages/core/test/config/fixtures/plugin/directory-plugin.ts +++ b/packages/core/test/config/fixtures/plugin/directory-plugin.ts @@ -1,4 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin/v2" +import { Plugin } from "@opencode-ai/plugin" export default Plugin.define({ id: "directory-plugin", diff --git a/packages/core/test/config/plugin.test.ts b/packages/core/test/config/plugin.test.ts index 2a9dfa81bf0d..c558dd869e81 100644 --- a/packages/core/test/config/plugin.test.ts +++ b/packages/core/test/config/plugin.test.ts @@ -2,21 +2,20 @@ import fs from "fs/promises" import path from "path" import { pathToFileURL } from "url" import { describe, expect } from "bun:test" -import { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect" +import { Plugin as EffectPlugin } from "@opencode-ai/plugin/effect" import { Config as ConfigSchema } from "@opencode-ai/schema/config" -import { Plugin } from "@opencode-ai/schema/plugin" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Catalog } from "@opencode-ai/core/catalog" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Location } from "@opencode-ai/core/location" import { LocationServiceMap } from "@opencode-ai/core/location-services" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Plugin } from "@opencode-ai/core/plugin" import { SdkPlugins } from "@opencode-ai/core/plugin/sdk" import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" import { Effect, Logger } from "effect" import { Database } from "../../src/database/database" @@ -24,7 +23,7 @@ import { tmpdir } from "../fixture/tmpdir" import { testEffect } from "../lib/effect" const it = testEffect( - AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SdkPlugins.node, LocationServiceMap.node])), + AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SdkPlugins.node, LocationServiceMap.node])), ) describe("PluginSupervisor config", () => { @@ -32,7 +31,7 @@ describe("PluginSupervisor config", () => { withLocation( { plugins: ["-opencode.provider.*", "opencode.provider.openai"] }, Effect.gen(function* () { - const plugins = yield* PluginV2.Service + const plugins = yield* Plugin.Service yield* ready() expect( (yield* plugins.list()).map((plugin) => plugin.id).filter((id) => id.startsWith("opencode.provider.")), @@ -54,8 +53,8 @@ describe("PluginSupervisor config", () => { }, Effect.gen(function* () { yield* ready() - const agents = yield* AgentV2.Service - expect(yield* agents.get(AgentV2.ID.make("configured"))).toMatchObject({ + const agents = yield* Agent.Service + expect(yield* agents.get(Agent.ID.make("configured"))).toMatchObject({ description: "Loaded from config", mode: "subagent", }) @@ -69,10 +68,10 @@ describe("PluginSupervisor config", () => { { plugins: [plugin, "-config-promise-plugin"] }, Effect.gen(function* () { yield* ready() - const plugins = yield* PluginV2.Service - const agents = yield* AgentV2.Service + const plugins = yield* Plugin.Service + const agents = yield* Agent.Service expect((yield* plugins.list()).map((item) => String(item.id))).not.toContain("config-promise-plugin") - expect(yield* agents.get(AgentV2.ID.make("configured"))).toBeUndefined() + expect(yield* agents.get(Agent.ID.make("configured"))).toBeUndefined() }), ) }) @@ -83,7 +82,7 @@ describe("PluginSupervisor config", () => { { plugins: [plugin, `-${plugin}`] }, Effect.gen(function* () { yield* ready() - const plugins = yield* PluginV2.Service + const plugins = yield* Plugin.Service expect((yield* plugins.list()).map((item) => String(item.id))).toContain("config-promise-plugin") }), ) @@ -102,8 +101,8 @@ describe("PluginSupervisor config", () => { }, Effect.gen(function* () { yield* ready() - const agents = yield* AgentV2.Service - expect(yield* agents.get(AgentV2.ID.make("effect-configured"))).toMatchObject({ + const agents = yield* Agent.Service + expect(yield* agents.get(Agent.ID.make("effect-configured"))).toMatchObject({ description: "Effect plugin from config", mode: "subagent", }) @@ -133,8 +132,8 @@ describe("PluginSupervisor config", () => { }, Effect.gen(function* () { yield* ready() - const agents = yield* AgentV2.Service - expect(yield* agents.get(AgentV2.ID.make("configured"))).toMatchObject({ + const agents = yield* Agent.Service + expect(yield* agents.get(Agent.ID.make("configured"))).toMatchObject({ description: "Loaded after invalid plugins", }) expect(output).toEqual([ @@ -150,8 +149,8 @@ describe("PluginSupervisor config", () => { undefined, Effect.gen(function* () { yield* ready() - const agents = yield* AgentV2.Service - expect(yield* agents.get(AgentV2.ID.make("directory"))).toMatchObject({ + const agents = yield* Agent.Service + expect(yield* agents.get(Agent.ID.make("directory"))).toMatchObject({ description: "Loaded from plugin directory", }) }), @@ -164,26 +163,26 @@ describe("PluginSupervisor config", () => { undefined, Effect.gen(function* () { yield* ready() - const agents = yield* AgentV2.Service - const events = yield* EventV2.Service + const agents = yield* Agent.Service + const bus = yield* Bus.Service const location = yield* Location.Service - const plugins = yield* PluginV2.Service + const plugins = yield* Plugin.Service const file = path.join(location.directory, ".opencode", "plugin", "mutable.ts") const first = (yield* plugins.list()).find((plugin) => plugin.id === "mutable-plugin")?.id expect(first).toBeDefined() - expect((yield* agents.get(AgentV2.ID.make("mutable")))?.description).toBe("first") + expect((yield* agents.get(Agent.ID.make("mutable")))?.description).toBe("first") yield* Effect.promise(async () => { await fs.writeFile(file, mutablePlugin("second")) const modified = new Date(Date.now() + 5_000) await fs.utimes(file, modified, modified) }) - yield* events.publish(ConfigSchema.Event.Updated, {}) + yield* bus.publish(ConfigSchema.Event.Updated, {}) yield* waitUntil( Effect.gen(function* () { const current = (yield* plugins.list()).find((plugin) => plugin.id === "mutable-plugin")?.id - return current === first && (yield* agents.get(AgentV2.ID.make("mutable")))?.description === "second" + return current === first && (yield* agents.get(Agent.ID.make("mutable")))?.description === "second" }), ) }), @@ -201,8 +200,8 @@ describe("PluginSupervisor config", () => { { plugins: ["-*"] }, Effect.gen(function* () { yield* ready() - const agents = yield* AgentV2.Service - expect(yield* agents.get(AgentV2.ID.make("directory"))).toBeUndefined() + const agents = yield* Agent.Service + expect(yield* agents.get(Agent.ID.make("directory"))).toBeUndefined() }), true, ), @@ -221,7 +220,7 @@ describe("PluginSupervisor config", () => { }, Effect.gen(function* () { yield* ready() - const registry = yield* PluginV2.Service + const registry = yield* Plugin.Service const ids = (yield* registry.list()).map((plugin) => String(plugin.id)) expect(ids.indexOf("opencode.agent")).toBeLessThan(ids.indexOf("sdk-order")) expect(ids.indexOf("sdk-order")).toBeLessThan(ids.indexOf("config-promise-plugin")) @@ -231,7 +230,7 @@ describe("PluginSupervisor config", () => { const catalog = yield* Catalog.Service expect( - (yield* catalog.model.get(ProviderV2.ID.make("configured"), ModelV2.ID.make("glm-5.2")))?.variants, + (yield* catalog.model.get(Provider.ID.make("configured"), Model.ID.make("glm-5.2")))?.variants, ).toEqual([ expect.objectContaining({ id: "high", headers: { custom: "true" } }), expect.objectContaining({ id: "max", settings: { reasoningEffort: "max" } }), @@ -248,12 +247,12 @@ describe("PluginSupervisor config", () => { }, Effect.gen(function* () { yield* ready() - const registry = yield* PluginV2.Service + const registry = yield* Plugin.Service expect((yield* registry.list()).map((plugin) => String(plugin.id))).not.toContain("opencode.variant") const catalog = yield* Catalog.Service expect( - (yield* catalog.model.get(ProviderV2.ID.make("configured"), ModelV2.ID.make("glm-5.2")))?.variants, + (yield* catalog.model.get(Provider.ID.make("configured"), Model.ID.make("glm-5.2")))?.variants, ).toEqual([expect.objectContaining({ id: "high", headers: { custom: "true" } })]) }), ), @@ -304,7 +303,7 @@ function withLocation( } function mutablePlugin(description: string) { - const plugin = pathToFileURL(path.join(import.meta.dir, "../../../plugin/src/v2/promise/index.ts")).href + const plugin = pathToFileURL(path.join(import.meta.dir, "../../../plugin/src/index.ts")).href return ` import { Plugin } from ${JSON.stringify(plugin)} diff --git a/packages/core/test/config/policy.test.ts b/packages/core/test/config/policy.test.ts index 810f20ffd9d4..a90d64ed99d5 100644 --- a/packages/core/test/config/policy.test.ts +++ b/packages/core/test/config/policy.test.ts @@ -3,10 +3,10 @@ import { Config as ConfigSchema } from "@opencode-ai/schema/config" import { Catalog } from "@opencode-ai/core/catalog" import { Config } from "@opencode-ai/core/config" import { ConfigPolicyPlugin } from "@opencode-ai/core/config/plugin/policy" -import { EventV2 } from "@opencode-ai/core/event" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Bus } from "@opencode-ai/core/bus" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { Effect, Schema } from "effect" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "../plugin/fixture" @@ -25,7 +25,7 @@ const policies = (...items: { effect: "allow" | "deny"; resource: string }[]) => }) const addPlugin = Effect.fn(function* (entries: () => Config.Entry[]) { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) yield* ConfigPolicyPlugin.Plugin.effect(host).pipe( Effect.provideService(Config.Service, Config.Service.of({ entries: () => Effect.sync(entries) })), @@ -37,9 +37,9 @@ describe("ConfigPolicyPlugin.Plugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.openai, () => {}) - catalog.provider.update(ProviderV2.ID.anthropic, () => {}) - catalog.provider.update(ProviderV2.ID.make("company-internal"), () => {}) + catalog.provider.update(Provider.ID.openai, () => {}) + catalog.provider.update(Provider.ID.anthropic, () => {}) + catalog.provider.update(Provider.ID.make("company-internal"), () => {}) }) yield* addPlugin(() => [ policies( @@ -49,38 +49,38 @@ describe("ConfigPolicyPlugin.Plugin", () => { ), ]) - expect(yield* catalog.provider.get(ProviderV2.ID.openai)).toBeUndefined() - expect(yield* catalog.provider.get(ProviderV2.ID.anthropic)).toBeDefined() - expect(yield* catalog.provider.get(ProviderV2.ID.make("company-internal"))).toBeDefined() + expect(yield* catalog.provider.get(Provider.ID.openai)).toBeUndefined() + expect(yield* catalog.provider.get(Provider.ID.anthropic)).toBeDefined() + expect(yield* catalog.provider.get(Provider.ID.make("company-internal"))).toBeDefined() }), ) it.effect("prevents project policy from overriding user-global policy", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.openai, () => {})) + yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.openai, () => {})) yield* addPlugin(() => [ policies({ effect: "deny", resource: "openai" }), policies({ effect: "allow", resource: "openai" }), ]) - expect(yield* catalog.provider.get(ProviderV2.ID.openai)).toBeUndefined() + expect(yield* catalog.provider.get(Provider.ID.openai)).toBeUndefined() }), ) it.live("reloads changed policies", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service let entries: Config.Entry[] = [policies({ effect: "deny", resource: "openai" })] - yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.openai, () => {})) + yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.openai, () => {})) yield* addPlugin(() => entries) - expect(yield* catalog.provider.get(ProviderV2.ID.openai)).toBeUndefined() + expect(yield* catalog.provider.get(Provider.ID.openai)).toBeUndefined() entries = [policies({ effect: "allow", resource: "openai" })] - yield* events.publish(ConfigSchema.Event.Updated, {}) + yield* bus.publish(ConfigSchema.Event.Updated, {}) yield* waitUntil( - catalog.provider.get(ProviderV2.ID.openai).pipe(Effect.map((provider) => provider !== undefined)), + catalog.provider.get(Provider.ID.openai).pipe(Effect.map((provider) => provider !== undefined)), ) }), ) diff --git a/packages/core/test/config/provider.test.ts b/packages/core/test/config/provider.test.ts index 60597836f2df..152b34faff0b 100644 --- a/packages/core/test/config/provider.test.ts +++ b/packages/core/test/config/provider.test.ts @@ -5,17 +5,17 @@ import { Catalog } from "@opencode-ai/core/catalog" import { Config } from "@opencode-ai/core/config" import { ConfigProviderPlugin } from "@opencode-ai/core/config/plugin/provider" import { Integration } from "@opencode-ai/core/integration" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "../plugin/fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* (config: Config.Interface) { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) yield* ConfigProviderPlugin.Plugin.effect(host).pipe(Effect.provideService(Config.Service, config)) }) @@ -52,8 +52,8 @@ describe("ConfigProviderPlugin.Plugin", () => { it.effect("defaults custom models to agent capabilities", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.make("custom") - const modelID = ModelV2.ID.make("chat") + const providerID = Provider.ID.make("custom") + const modelID = Model.ID.make("chat") const config = Config.Service.of({ entries: () => Effect.succeed([ @@ -81,9 +81,9 @@ describe("ConfigProviderPlugin.Plugin", () => { it.effect("preserves catalog capabilities unless config overrides them", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.make("custom") - const inheritedID = ModelV2.ID.make("inherited") - const overriddenID = ModelV2.ID.make("overridden") + const providerID = Provider.ID.make("custom") + const inheritedID = Model.ID.make("inherited") + const overriddenID = Model.ID.make("overridden") yield* catalog.transform((draft) => { draft.model.update(providerID, inheritedID, (model) => { model.capabilities = { tools: false, input: ["text"], output: ["text"] } @@ -132,8 +132,8 @@ describe("ConfigProviderPlugin.Plugin", () => { it.effect("keeps configured model variant bodies unchanged", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.opencode - const modelID = ModelV2.ID.make("alpha-gpt-next") + const providerID = Provider.ID.opencode + const modelID = Model.ID.make("alpha-gpt-next") const config = Config.Service.of({ entries: () => Effect.succeed([ @@ -184,8 +184,8 @@ describe("ConfigProviderPlugin.Plugin", () => { it.effect("keeps layered model variant bodies unchanged", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.opencode - const modelID = ModelV2.ID.make("alpha-gpt-next") + const providerID = Provider.ID.opencode + const modelID = Model.ID.make("alpha-gpt-next") const config = Config.Service.of({ entries: () => Effect.succeed([ @@ -232,8 +232,8 @@ describe("ConfigProviderPlugin.Plugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service const integrations = yield* Integration.Service - const providerID = ProviderV2.ID.make("custom") - const modelID = ModelV2.ID.make("chat") + const providerID = Provider.ID.make("custom") + const modelID = Model.ID.make("chat") const config = Config.Service.of({ entries: () => Effect.succeed([ @@ -318,7 +318,7 @@ describe("ConfigProviderPlugin.Plugin", () => { const provider = required(yield* catalog.provider.get(providerID)) const model = required(yield* catalog.model.get(providerID, modelID)) - expect((yield* catalog.model.default())?.id).toBe(ModelV2.ID.make("default")) + expect((yield* catalog.model.default())?.id).toBe(Model.ID.make("default")) expect(provider.name).toBe("Renamed") expect((yield* integrations.get(Integration.ID.make("custom")))?.methods).toContainEqual({ type: "env", @@ -330,7 +330,7 @@ describe("ConfigProviderPlugin.Plugin", () => { expect(provider.settings).toEqual({ baseURL: "https://example.test" }) expect(provider.headers).toEqual({ first: "first", shared: "last", last: "last" }) expect(model.id).toBe(modelID) - expect(model.modelID).toBe(ModelV2.ID.make("api-chat")) + expect(model.modelID).toBe(Model.ID.make("api-chat")) expect(model.name).toBe("Last") expect(model.compatibility).toEqual({ reasoningField: "vendor_reasoning" }) expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] }) @@ -350,8 +350,8 @@ describe("ConfigProviderPlugin.Plugin", () => { expect(model.settings).toEqual({ baseURL: "https://example.test", retained: true }) expect(model.headers).toEqual({ first: "first", shared: "last", last: "last" }) expect(model.variants?.map((variant) => variant.id)).toEqual([ - ModelV2.VariantID.make("fast"), - ModelV2.VariantID.make("slow"), + Model.VariantID.make("fast"), + Model.VariantID.make("slow"), ]) expect(model.variants?.[0]?.headers).toEqual({ first: "first", shared: "last", last: "last" }) expect(model.variants?.[1]?.headers).toEqual({ slow: "slow" }) diff --git a/packages/core/test/config/reload.test.ts b/packages/core/test/config/reload.test.ts index d917821facb9..70d21a693b1b 100644 --- a/packages/core/test/config/reload.test.ts +++ b/packages/core/test/config/reload.test.ts @@ -1,22 +1,22 @@ import path from "path" import { describe, expect } from "bun:test" import { Config as ConfigSchema } from "@opencode-ai/schema/config" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Catalog } from "@opencode-ai/core/catalog" -import { CommandV2 } from "@opencode-ai/core/command" +import { Command } from "@opencode-ai/core/command" import { Config } from "@opencode-ai/core/config" import { ConfigAgentPlugin } from "@opencode-ai/core/config/plugin/agent" import { ConfigCommandPlugin } from "@opencode-ai/core/config/plugin/command" import { ConfigProviderPlugin } from "@opencode-ai/core/config/plugin/provider" import { ConfigReferencePlugin } from "@opencode-ai/core/config/plugin/reference" import { ConfigSkillPlugin } from "@opencode-ai/core/config/plugin/skill" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Global } from "@opencode-ai/util/global" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { Reference } from "@opencode-ai/core/reference" -import { SkillV2 } from "@opencode-ai/core/skill" +import { Skill } from "@opencode-ai/core/skill" import { Effect, Schema } from "effect" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "../plugin/fixture" @@ -28,13 +28,13 @@ const document = path.join(import.meta.dir, "opencode.json") describe("config plugin reloads", () => { it.live("reloads config-backed domains without reloading external plugins", () => Effect.gen(function* () { - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const catalog = yield* Catalog.Service - const commands = yield* CommandV2.Service - const events = yield* EventV2.Service - const plugins = yield* PluginV2.Service + const commands = yield* Command.Service + const bus = yield* Bus.Service + const plugins = yield* Plugin.Service const references = yield* Reference.Service - const skills = yield* SkillV2.Service + const skills = yield* Skill.Service const host = yield* PluginHost.make(plugins) let entries: Config.Entry[] = [config("first")] const service = Config.Service.of({ entries: () => Effect.sync(() => entries) }) @@ -47,26 +47,26 @@ describe("config plugin reloads", () => { yield* setup(ConfigReferencePlugin.Plugin.effect(host)) yield* setup(ConfigProviderPlugin.Plugin.effect(host)) - expect((yield* agents.get(AgentV2.ID.make("first")))?.description).toBe("First agent") + expect((yield* agents.get(Agent.ID.make("first")))?.description).toBe("First agent") expect((yield* commands.get("first"))?.description).toBe("First command") expect( (yield* skills.sources()).some((source) => source.type === "directory" && source.path === "/skills/first"), ).toBe(true) expect((yield* references.list()).map((reference) => reference.name)).toEqual(["first"]) - expect(yield* catalog.provider.get(ProviderV2.ID.make("first"))).toBeDefined() + expect(yield* catalog.provider.get(Provider.ID.make("first"))).toBeDefined() entries = [config("second")] - yield* events.publish(ConfigSchema.Event.Updated, {}) + yield* bus.publish(ConfigSchema.Event.Updated, {}) yield* waitUntil( Effect.gen(function* () { return ( - (yield* agents.get(AgentV2.ID.make("first"))) === undefined && - (yield* agents.get(AgentV2.ID.make("second")))?.description === "Second agent" && + (yield* agents.get(Agent.ID.make("first"))) === undefined && + (yield* agents.get(Agent.ID.make("second")))?.description === "Second agent" && (yield* commands.get("first")) === undefined && (yield* commands.get("second"))?.description === "Second command" && (yield* references.list()).some((reference) => reference.name === "second") && - (yield* catalog.provider.get(ProviderV2.ID.make("first"))) === undefined && - (yield* catalog.provider.get(ProviderV2.ID.make("second"))) !== undefined + (yield* catalog.provider.get(Provider.ID.make("first"))) === undefined && + (yield* catalog.provider.get(Provider.ID.make("second"))) !== undefined ) }), ) diff --git a/packages/core/test/config/skill.test.ts b/packages/core/test/config/skill.test.ts index 86df211cf8ed..2553498987bf 100644 --- a/packages/core/test/config/skill.test.ts +++ b/packages/core/test/config/skill.test.ts @@ -6,7 +6,7 @@ import { ConfigSkillPlugin } from "@opencode-ai/core/config/plugin/skill" import { Global } from "@opencode-ai/util/global" import { Location } from "@opencode-ai/core/location" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SkillV2 } from "@opencode-ai/core/skill" +import { Skill } from "@opencode-ai/core/skill" import { location } from "../fixture/location" import { testEffect } from "../lib/effect" import { host } from "../plugin/host" @@ -18,8 +18,8 @@ describe("ConfigSkillPlugin.Plugin", () => { it.effect("registers configured skill directories and URLs", () => Effect.gen(function* () { const directory = AbsolutePath.make("/repo/packages/app") - const sources: SkillV2.Source[] = [] - const transform = Effect.fnUntraced(function* (update: (draft: SkillV2.Draft) => void | Effect.Effect) { + const sources: Skill.Source[] = [] + const transform = Effect.fnUntraced(function* (update: (draft: Skill.Draft) => void | Effect.Effect) { const result = update({ source: (source) => { sources.push(source) @@ -61,29 +61,29 @@ describe("ConfigSkillPlugin.Plugin", () => { ) expect(sources).toEqual([ - SkillV2.DirectorySource.make({ + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join("/repo/.claude", "skills")), }), - SkillV2.DirectorySource.make({ + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join("/repo/.agents", "skills")), }), - SkillV2.DirectorySource.make({ + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join("/repo/.opencode", "skill")), }), - SkillV2.DirectorySource.make({ + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join("/repo/.opencode", "skills")), }), - SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }), - SkillV2.DirectorySource.make({ + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }), + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join("/home/test", "shared-skills")), }), - SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make("/opt/skills") }), - SkillV2.UrlSource.make({ type: "url", url: "https://example.test/skills/" }), + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make("/opt/skills") }), + Skill.UrlSource.make({ type: "url", url: "https://example.test/skills/" }), ]) }), ) diff --git a/packages/core/test/database-migration.test.ts b/packages/core/test/database-migration.test.ts index f98ded68f3a0..f8148b66f8c7 100644 --- a/packages/core/test/database-migration.test.ts +++ b/packages/core/test/database-migration.test.ts @@ -28,8 +28,8 @@ import deleteToolProgressEventsMigration from "@opencode-ai/core/database/migrat import canonicalToolResultsMigration from "@opencode-ai/core/database/migration/20260722170000_canonical_tool_results" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" -import { ProjectV2 } from "@opencode-ai/core/project" +import { Bus } from "@opencode-ai/core/bus" +import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { AbsolutePath } from "@opencode-ai/core/schema" import { SessionSchema } from "@opencode-ai/core/session/schema" @@ -968,13 +968,13 @@ describe("DatabaseMigration", () => { ) const database = Layer.succeed(Database.Service, { db }) - yield* EventV2.Service.use((service) => + yield* Bus.Service.use((service) => service.publish(SessionV1.Event.Updated, { sessionID: SessionSchema.ID.make("session"), info: { id: SessionSchema.ID.make("session"), slug: "session", - projectID: ProjectV2.ID.global, + projectID: Project.ID.global, directory: "/project", title: "After", version: "test", @@ -983,7 +983,7 @@ describe("DatabaseMigration", () => { }), ).pipe( Effect.provide( - AppNodeBuilder.build(LayerNode.group([EventV2.node, SessionProjector.node]), [[Database.node, database]]), + AppNodeBuilder.build(LayerNode.group([Bus.node, SessionProjector.node]), [[Database.node, database]]), ), ) @@ -1280,7 +1280,7 @@ describe("DatabaseMigration", () => { Effect.gen(function* () { const db = yield* makeDb yield* DatabaseMigration.apply(db) - const projectID = ProjectV2.ID.make("codec_project") + const projectID = Project.ID.make("codec_project") const worktree = AbsolutePath.make("C:\\Repo\\Thing") const sandbox = AbsolutePath.make("C:\\Repo\\Thing\\sandbox") const directory = "C:\\Repo\\Thing\\packages\\api" @@ -1291,7 +1291,7 @@ describe("DatabaseMigration", () => { db .insert(ProjectTable) .values({ - id: ProjectV2.ID.make("invalid_path"), + id: Project.ID.make("invalid_path"), worktree: AbsolutePath.make("not-absolute"), sandboxes: [], time_created: 1, diff --git a/packages/core/test/event-logger.test.ts b/packages/core/test/event-logger.test.ts index ab7aca7d5b9e..9678d0e0d472 100644 --- a/packages/core/test/event-logger.test.ts +++ b/packages/core/test/event-logger.test.ts @@ -3,7 +3,7 @@ import { Effect, Layer, Logger } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Database } from "@opencode-ai/core/database/database" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { EventLogger } from "@opencode-ai/core/event-logger" import { Agent } from "@opencode-ai/schema/agent" import { Catalog } from "@opencode-ai/schema/catalog" @@ -11,7 +11,7 @@ import { Command } from "@opencode-ai/schema/command" import { Config } from "@opencode-ai/schema/config" import { McpEvent } from "@opencode-ai/schema/mcp-event" -const UnlistedUpdated = EventV2.ephemeral({ type: "test.updated", schema: {} }) +const UnlistedUpdated = Bus.ephemeral({ type: "test.updated", schema: {} }) describe("EventLogger", () => { test("logs explicitly listed updated events", async () => { @@ -21,15 +21,15 @@ describe("EventLogger", () => { }) await Effect.gen(function* () { - const events = yield* EventV2.Service - yield* events.publish(Agent.Event.Updated, {}) - yield* events.publish(Catalog.Event.Updated, {}) - yield* events.publish(Command.Event.Updated, {}) - yield* events.publish(Config.Event.Updated, {}) - yield* events.publish(McpEvent.StatusChanged, { server: "example" }) - yield* events.publish(UnlistedUpdated, {}) + const bus = yield* Bus.Service + yield* bus.publish(Agent.Event.Updated, {}) + yield* bus.publish(Catalog.Event.Updated, {}) + yield* bus.publish(Command.Event.Updated, {}) + yield* bus.publish(Config.Event.Updated, {}) + yield* bus.publish(McpEvent.StatusChanged, { server: "example" }) + yield* bus.publish(UnlistedUpdated, {}) }).pipe( - Effect.provide(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, EventLogger.node]))), + Effect.provide(AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, EventLogger.node]))), Effect.provide(Logger.layer([logger])), Effect.scoped, Effect.runPromise, diff --git a/packages/core/test/filesystem/watcher.test.ts b/packages/core/test/filesystem/watcher.test.ts index 5c597d2db534..e475a8b16650 100644 --- a/packages/core/test/filesystem/watcher.test.ts +++ b/packages/core/test/filesystem/watcher.test.ts @@ -6,7 +6,7 @@ import { Deferred, Duration, Effect, Fiber, Layer, Option, Schedule, Stream } fr import { Config } from "@opencode-ai/core/config" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { FSUtil } from "@opencode-ai/util/fs-util" import { LocationWatcher } from "@opencode-ai/core/filesystem/location-watcher" import { Watcher } from "@opencode-ai/core/filesystem/watcher" @@ -21,7 +21,7 @@ const describeWatcher = Watcher.hasNativeBinding() && !process.env.CI ? describe type WatcherEvent = { file: string; event: "add" | "change" | "unlink" } -const it = testEffect(AppNodeBuilder.build(LayerNode.group([FSUtil.node, EventV2.node]))) +const it = testEffect(AppNodeBuilder.build(LayerNode.group([FSUtil.node, Bus.node]))) const configLayer = Layer.succeed( Config.Service, @@ -66,9 +66,9 @@ function withTmp( function wait(check: (event: WatcherEvent) => boolean) { return Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const deferred = yield* Deferred.make() - const fiber = yield* events.subscribe(FileSystem.Event.Changed).pipe( + const fiber = yield* bus.subscribe(FileSystem.Event.Changed).pipe( Stream.runForEach((event) => { if (!check(event.data)) return Effect.void return Deferred.succeed(deferred, event.data).pipe(Effect.asVoid) @@ -217,7 +217,7 @@ describeWatcher("LocationWatcher", () => { it.live("cleanup stops publishing events", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const fs = yield* FSUtil.Service const tmp = yield* Effect.acquireRelease( Effect.promise(() => tmpdir()), @@ -229,9 +229,9 @@ describeWatcher("LocationWatcher", () => { ) const file = path.join(tmp.path, "after-dispose.txt") yield* noUpdate((event) => event.file === file, fs.writeFileString(file, "gone")).pipe( - Effect.provideService(EventV2.Service, events), + Effect.provideService(Bus.Service, bus), ) - }).pipe(Effect.provide(AppNodeBuilder.build(LayerNode.group([FSUtil.node, EventV2.node])))), + }).pipe(Effect.provide(AppNodeBuilder.build(LayerNode.group([FSUtil.node, Bus.node])))), ) it.live("ignores .git/index changes", () => diff --git a/packages/core/test/form.test.ts b/packages/core/test/form.test.ts index 06b50f3643ba..5ee579668be1 100644 --- a/packages/core/test/form.test.ts +++ b/packages/core/test/form.test.ts @@ -2,12 +2,12 @@ import { describe, expect } from "bun:test" import { Deferred, Effect, Exit, Fiber } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Form } from "@opencode-ai/core/form" import { SessionSchema } from "@opencode-ai/core/session/schema" import { testEffect } from "./lib/effect" -const forms = AppNodeBuilder.build(LayerNode.group([EventV2.node, Form.node])) +const forms = AppNodeBuilder.build(LayerNode.group([Bus.node, Form.node])) const it = testEffect(forms) const formID = Form.ID.create("frm_test") @@ -22,9 +22,9 @@ describe("Form", () => { it.effect("returns a terminal cancelled state from ask", () => Effect.gen(function* () { const service = yield* Form.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const created = yield* Deferred.make() - const unsubscribe = yield* events.listen((event) => + const unsubscribe = yield* bus.listen((event) => event.type === Form.Event.Created.type ? Deferred.succeed(created, (event.data as { readonly form: Form.Info }).form).pipe(Effect.asVoid) : Effect.void, @@ -311,8 +311,8 @@ describe("Form", () => { it.effect("cleans up created forms when event publication fails", () => Effect.gen(function* () { const service = yield* Form.Service - const events = yield* EventV2.Service - const unsubscribe = yield* events.listen((event) => + const bus = yield* Bus.Service + const unsubscribe = yield* bus.listen((event) => event.type === Form.Event.Created.type ? Effect.die("create listener failed") : Effect.void, ) yield* Effect.addFinalizer(() => unsubscribe) @@ -328,9 +328,9 @@ describe("Form", () => { it.effect("keeps forms pending when reply event publication fails", () => Effect.gen(function* () { const service = yield* Form.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* service.create(input) - const unsubscribe = yield* events.listen((event) => + const unsubscribe = yield* bus.listen((event) => event.type === Form.Event.Replied.type ? Effect.die("reply listener failed") : Effect.void, ) yield* Effect.addFinalizer(() => unsubscribe) diff --git a/packages/core/test/generate.test.ts b/packages/core/test/generate.test.ts index 575b0199fb9d..b669e60f4833 100644 --- a/packages/core/test/generate.test.ts +++ b/packages/core/test/generate.test.ts @@ -6,15 +6,15 @@ import { Catalog } from "@opencode-ai/core/catalog" import { Generate } from "@opencode-ai/core/generate" import { Integration } from "@opencode-ai/core/integration" import { ModelResolver } from "@opencode-ai/core/model-resolver" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { ID, Info, Ref } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { Npm } from "@opencode-ai/util/npm" import { Effect, Layer, Stream } from "effect" import { testEffect } from "./lib/effect" -const selected = ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("test-provider"), ModelV2.ID.make("gemini")), - package: ProviderV2.aisdk("@ai-sdk/google"), +const selected = Info.make({ + ...Info.default(Provider.ID.make("test-provider"), ID.make("gemini")), + package: Provider.aisdk("@ai-sdk/google"), }) const runtime = Model.make({ id: "gemini", provider: "test-provider", route: OpenAIChat.route }) @@ -89,7 +89,7 @@ it.effect("loads dynamic AI SDK models", () => const generate = yield* Generate.Service const result = yield* generate.text({ prompt: "Return exactly OK", - model: ModelV2.Ref.make({ providerID: selected.providerID, id: selected.id }), + model: Ref.make({ providerID: selected.providerID, id: selected.id }), }) expect(result).toBe("OK") @@ -99,11 +99,11 @@ it.effect("loads dynamic AI SDK models", () => resolverIt.effect("resolves dynamic models with their catalog metadata", () => Effect.gen(function* () { const resolver = yield* ModelResolver.Service - const result = yield* resolver.resolve(ModelV2.Ref.make({ providerID: selected.providerID, id: selected.id })) + const result = yield* resolver.resolve(Ref.make({ providerID: selected.providerID, id: selected.id })) expect(result).toEqual({ model: runtime, - ref: ModelV2.Ref.make({ providerID: selected.providerID, id: selected.id }), + ref: Ref.make({ providerID: selected.providerID, id: selected.id }), capabilities: selected.capabilities, cost: selected.cost, }) diff --git a/packages/core/test/github-copilot/models.test.ts b/packages/core/test/github-copilot/models.test.ts index 9bd07971fdf1..cba3001d691e 100644 --- a/packages/core/test/github-copilot/models.test.ts +++ b/packages/core/test/github-copilot/models.test.ts @@ -1,7 +1,7 @@ import { expect, test } from "bun:test" import { CopilotModels } from "@opencode-ai/core/github-copilot/models" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" test("defensively syncs advertised Copilot models", async () => { const server = Bun.serve({ @@ -48,28 +48,28 @@ test("defensively syncs advertised Copilot models", async () => { }) try { - const existing = ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.githubCopilot, ModelV2.ID.make("gpt-5")), - modelID: ModelV2.ID.make("gpt-5"), + const existing = Model.Info.make({ + ...Model.Info.default(Provider.ID.githubCopilot, Model.ID.make("gpt-5")), + modelID: Model.ID.make("gpt-5"), name: "GPT-5 local", }) - const stale = ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.githubCopilot, ModelV2.ID.make("stale")), - modelID: ModelV2.ID.make("stale"), + const stale = Model.Info.make({ + ...Model.Info.default(Provider.ID.githubCopilot, Model.ID.make("stale")), + modelID: Model.ID.make("stale"), }) const models = await CopilotModels.get(server.url.origin, {}, [existing, stale]) - const model = models.get(ModelV2.ID.make("gpt-5")) + const model = models.get(Model.ID.make("gpt-5")) expect(model?.name).toBe("GPT-5 local") expect(model?.settings).toMatchObject({ baseURL: server.url.origin, endpoint: "responses" }) expect(model?.cost[0]).toMatchObject({ input: 0, output: 0, cache: { read: 0, write: 0 } }) expect(model?.variants.map((variant) => variant.id)).toEqual([ - ModelV2.VariantID.make("low"), - ModelV2.VariantID.make("high"), + Model.VariantID.make("low"), + Model.VariantID.make("high"), ]) - expect(models.get(ModelV2.ID.make("utility"))?.enabled).toBe(false) - expect(models.has(ModelV2.ID.make("stale"))).toBe(false) - expect(models.has(ModelV2.ID.make("incomplete"))).toBe(false) + expect(models.get(Model.ID.make("utility"))?.enabled).toBe(false) + expect(models.has(Model.ID.make("stale"))).toBe(false) + expect(models.has(Model.ID.make("incomplete"))).toBe(false) } finally { await server.stop(true) } diff --git a/packages/core/test/instruction-state.test.ts b/packages/core/test/instruction-state.test.ts index 6557731fd799..4e75340d7838 100644 --- a/packages/core/test/instruction-state.test.ts +++ b/packages/core/test/instruction-state.test.ts @@ -4,7 +4,8 @@ import { Effect, Schema } from "effect" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" import { EventTable } from "@opencode-ai/core/event/sql" import { Instructions } from "@opencode-ai/core/instructions" import { Project } from "@opencode-ai/core/project" @@ -16,7 +17,7 @@ import { SessionSchema } from "@opencode-ai/core/session/schema" import { InstructionBlobTable, InstructionStateTable, SessionTable } from "@opencode-ai/core/session/sql" import { testEffect } from "./lib/effect" -const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SessionProjector.node]))) +const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SessionProjector.node]))) const source = (name: string, read: Effect.Effect) => Instructions.make({ @@ -51,7 +52,7 @@ const setup = (sessionID: SessionSchema.ID) => }) .run() .pipe(Effect.orDie) - return { db, events: yield* EventV2.Service } + return { db, events: yield* Bus.Service } }) const instructionEvents = (db: Database.Interface["db"], sessionID: SessionSchema.ID) => @@ -90,7 +91,7 @@ describe("InstructionState", () => { }), ), ]) - const published: EventV2.Payload[] = [] + const published: Event.Payload[] = [] const unsubscribe = yield* events.listen((event) => Effect.sync(() => { if (event.type === "session.instructions.updated") published.push(event) @@ -146,7 +147,7 @@ describe("InstructionState", () => { }), ), ]) - const published: EventV2.Payload[] = [] + const published: Event.Payload[] = [] const unsubscribe = yield* events.listen((event) => Effect.sync(() => { if (event.type === "session.instructions.updated") published.push(event) diff --git a/packages/core/test/integration.test.ts b/packages/core/test/integration.test.ts index e5d79a53c600..a4553780a62e 100644 --- a/packages/core/test/integration.test.ts +++ b/packages/core/test/integration.test.ts @@ -5,11 +5,11 @@ import { Credential } from "@opencode-ai/core/credential" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Integration } from "@opencode-ai/core/integration" import { testEffect } from "./lib/effect" -const it = testEffect(AppNodeBuilder.build(LayerNode.group([Integration.node, Credential.node, EventV2.node]))) +const it = testEffect(AppNodeBuilder.build(LayerNode.group([Integration.node, Credential.node, Bus.node]))) const failingCredentialNode = makeGlobalNode({ service: Credential.Service, layer: Layer.succeed( @@ -26,7 +26,7 @@ const failingCredentialNode = makeGlobalNode({ deps: [], }) const failingIt = testEffect( - AppNodeBuilder.build(LayerNode.group([Integration.node, EventV2.node]), [[Credential.node, failingCredentialNode]]), + AppNodeBuilder.build(LayerNode.group([Integration.node, Bus.node]), [[Credential.node, failingCredentialNode]]), ) function eventually( @@ -135,7 +135,7 @@ describe("Integration", () => { Effect.gen(function* () { const integrations = yield* Integration.Service const credentials = yield* Credential.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const integrationID = Integration.ID.make("openai") yield* integrations.transform((editor) => editor.method.update({ @@ -143,7 +143,7 @@ describe("Integration", () => { method: { type: "key", label: "API key" }, }), ) - const updated = yield* events + const updated = yield* bus .subscribe(Integration.Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow diff --git a/packages/core/test/lib/image.ts b/packages/core/test/lib/image.ts index b899bc916842..72ac03fdf1e9 100644 --- a/packages/core/test/lib/image.ts +++ b/packages/core/test/lib/image.ts @@ -1,7 +1,7 @@ import { Image } from "@opencode-ai/core/image" import { Effect, Layer } from "effect" -/** Passthrough resizer for tests that build ToolRegistry.node without a Location. */ +/** Passthrough resizer for tests that build Tool.node without a Location. */ export const imagePassthrough = Layer.mock(Image.Service, { normalize: (_resource, content) => Effect.succeed(content), }) diff --git a/packages/core/test/lib/tool.ts b/packages/core/test/lib/tool.ts index 7bf51cd9320f..c99d2001330c 100644 --- a/packages/core/test/lib/tool.ts +++ b/packages/core/test/lib/tool.ts @@ -1,23 +1,23 @@ -import { AgentV2 } from "@opencode-ai/core/agent" -import type { PermissionV2 } from "@opencode-ai/core/permission" +import { Agent } from "@opencode-ai/core/agent" +import type { Permission } from "@opencode-ai/core/permission" import { SessionMessage } from "@opencode-ai/core/session/message" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { Tool } from "@opencode-ai/core/tool/tool" -import { Tools } from "@opencode-ai/core/tool/tools" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import { toSessionError } from "@opencode-ai/core/session/to-session-error" +import type { SessionError } from "@opencode-ai/schema/session-error" +import { Tool } from "@opencode-ai/core/tool" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { Effect, type Scope } from "effect" import { host } from "../plugin/host" export const toolIdentity = { - agent: AgentV2.ID.make("build"), + agent: Agent.ID.make("build"), messageID: SessionMessage.ID.make("msg_tool_test"), } -export const toolDefinitions = (registry: ToolRegistry.Interface, permissions?: PermissionV2.Ruleset) => +export const toolDefinitions = (registry: Tool.Interface, permissions?: Permission.Ruleset) => registry.snapshot(permissions).pipe(Effect.map((toolSet) => toolSet.definitions)) export function waitForTool( - registry: ToolRegistry.Interface, + registry: Tool.Interface, name: string, remaining = 1000, ): Effect.Effect { @@ -33,10 +33,10 @@ export function waitForTool( } export function waitForCodeModeTool( - registry: ToolRegistry.Interface, + registry: Tool.Interface, path: string, remaining = 1000, -): Effect.Effect { +): Effect.Effect { return Effect.gen(function* () { const toolSet = yield* registry.snapshot() if (toolSet.codeModeCatalog?.some((tool) => tool.path === path)) return toolSet @@ -59,9 +59,9 @@ export const registerToolPlugin = ( readonly effect: (context: PluginContext) => Effect.Effect }, overrides: Parameters[0] = {}, -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { - const tools = yield* Tools.Service + const tools = yield* Tool.Service const context = host({ ...overrides, session: { @@ -69,29 +69,31 @@ export const registerToolPlugin = ( }, tool: { transform: (callback) => - Effect.gen(function* () { - const registrations: Array<{ - readonly name: string - readonly tool: Tool.Any - readonly options?: Tool.RegisterOptions - }> = [] - callback({ - add: (name, tool, options) => { - registrations.push({ name, tool, ...(options ? { options } : {}) }) - }, - }) - yield* Effect.forEach( - registrations, - (registration) => tools.register({ [registration.name]: registration.tool }, registration.options), - { discard: true }, - ).pipe(Effect.orDie) - return { dispose: Effect.void } - }), + tools + .transform((draft) => callback({ add: (tool) => draft.add(tool) })) + .pipe(Effect.orDie, Effect.as({ dispose: Effect.void })), hook: () => Effect.die("registerToolPlugin does not support tool hooks"), }, }) yield* plugin.effect(context) }) -export const executeTool = (registry: ToolRegistry.Interface, input: ToolRegistry.ExecuteInput) => - registry.snapshot().pipe(Effect.flatMap((toolSet) => toolSet.execute(input))) +export interface ToolExecution { + readonly status: "completed" | "error" + readonly output?: any + readonly content?: ReadonlyArray + readonly metadata?: Tool.Metadata + readonly error?: SessionError.Error +} + +export const executeTool = ( + registry: Tool.Interface, + input: Parameters[0], +): Effect.Effect => + registry.snapshot().pipe( + Effect.flatMap((tools) => tools.execute(input)), + Effect.map((result) => ({ status: "completed" as const, ...result }) satisfies ToolExecution), + Effect.catchTag("Tool.Error", (error) => + Effect.succeed({ status: "error" as const, error: toSessionError(error) } satisfies ToolExecution), + ), + ) diff --git a/packages/core/test/location-layer.test.ts b/packages/core/test/location-layer.test.ts index 1a198371ece1..bf897a6b1a61 100644 --- a/packages/core/test/location-layer.test.ts +++ b/packages/core/test/location-layer.test.ts @@ -2,36 +2,35 @@ import fs from "fs/promises" import path from "path" import { describe, expect } from "bun:test" import { Config } from "@opencode-ai/schema/config" -import { Plugin } from "@opencode-ai/schema/plugin" import { Money } from "@opencode-ai/schema/money" import { DateTime, Deferred, Effect, Equal, Fiber, Hash, RcMap, Schema, Stream } from "effect" -import { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Plugin as EffectPlugin } from "@opencode-ai/plugin/effect" +import { Agent } from "@opencode-ai/core/agent" import { Catalog } from "@opencode-ai/core/catalog" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { LocationServiceMap } from "@opencode-ai/core/location-services" import { Location } from "@opencode-ai/core/location" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Plugin } from "@opencode-ai/core/plugin" import { SdkPlugins } from "@opencode-ai/core/plugin/sdk" import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProjectV2 } from "@opencode-ai/core/project" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Project } from "@opencode-ai/core/project" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" import { toolDefinitions, waitForTool } from "./lib/tool" import { Database } from "../src/database/database" -import { EventV2 } from "../src/event" +import { Bus } from "../src/bus" import { Reference } from "../src/reference" -import { ToolRegistry } from "../src/tool/registry" +import { Tool } from "../src/tool" -const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, LocationServiceMap.node]))) +const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, LocationServiceMap.node]))) const itWithSdk = testEffect( - AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SdkPlugins.node, LocationServiceMap.node])), + AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SdkPlugins.node, LocationServiceMap.node])), ) describe("LocationServiceMap", () => { @@ -44,7 +43,7 @@ describe("LocationServiceMap", () => { Effect.gen(function* () { const sdk = yield* SdkPlugins.Service const locations = yield* LocationServiceMap.Service - const id = AgentV2.ID.make("persistent-sdk-agent") + const id = Agent.ID.make("persistent-sdk-agent") const plugin = EffectPlugin.define({ id: "persistent-sdk-plugin", effect: (ctx) => ctx.agent.transform((agents) => agents.update(id, () => {})), @@ -55,7 +54,7 @@ describe("LocationServiceMap", () => { const read = Effect.gen(function* () { const supervisor = yield* PluginSupervisor.Service yield* supervisor.flush - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service return yield* agents.get(id) }) @@ -101,7 +100,7 @@ describe("LocationServiceMap", () => { ) const explorer = yield* Effect.gen(function* () { - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service return yield* agents.resolve("explore") }).pipe(Effect.provide(context)) @@ -193,8 +192,8 @@ describe("LocationServiceMap", () => { const context = yield* locations.contextEffect(Location.Ref.make({ directory: AbsolutePath.make(dir.path) })) yield* Deferred.await(firstStarted) - const events = yield* EventV2.Service - const updated = yield* events.subscribe(Config.Event.Updated).pipe( + const bus = yield* Bus.Service + const updated = yield* bus.subscribe(Config.Event.Updated).pipe( Stream.filter((event) => event.location?.directory === dir.path), Stream.runHead, Effect.forkChild({ startImmediately: true }), @@ -235,11 +234,11 @@ describe("LocationServiceMap", () => { Effect.provide(context), Effect.forkChild({ startImmediately: true }), ) - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* Effect.forEach( Array.from({ length: 5 }), - () => events.publish(SdkPlugins.Updated, {}).pipe(Effect.andThen(Effect.sleep("50 millis"))), + () => bus.publish(SdkPlugins.Updated, {}).pipe(Effect.andThen(Effect.sleep("50 millis"))), { discard: true }, ) expect(flushFiber.pollUnsafe()).toBeUndefined() @@ -270,7 +269,7 @@ describe("LocationServiceMap", () => { yield* PluginSupervisor.Service.use((supervisor) => supervisor.flush).pipe(Effect.provide(context)) expect(activations.count).toBe(1) - yield* EventV2.Service.use((events) => events.publish(Config.Event.Updated, {})).pipe(Effect.provide(context)) + yield* Bus.Service.use((bus) => bus.publish(Config.Event.Updated, {})).pipe(Effect.provide(context)) yield* Effect.sleep("200 millis") expect(activations.count).toBe(1) @@ -370,7 +369,7 @@ describe("LocationServiceMap", () => { fs.writeFile(path.join(dir.path, "opencode.json"), JSON.stringify({ plugins: ["-*", "opencode.agent"] })), ) const plugins = yield* Effect.gen(function* () { - const plugins = yield* PluginV2.Service + const plugins = yield* Plugin.Service yield* (yield* PluginSupervisor.Service).flush return yield* plugins.list() }).pipe( @@ -396,7 +395,7 @@ describe("LocationServiceMap", () => { const file = path.join(dir.path, "opencode.json") yield* Effect.promise(() => fs.writeFile(file, JSON.stringify({ plugins: ["-*", "opencode.agent"] }))) yield* Effect.gen(function* () { - const registry = yield* PluginV2.Service + const registry = yield* Plugin.Service const supervisor = yield* PluginSupervisor.Service yield* supervisor.flush expect((yield* registry.list()).map((plugin) => String(plugin.id))).toEqual(["opencode.agent"]) @@ -449,25 +448,25 @@ describe("LocationServiceMap", () => { Effect.scoped( Effect.gen(function* () { const locations = yield* LocationServiceMap.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const firstRef = Location.Ref.make({ directory: AbsolutePath.make(first.path) }) const secondRef = Location.Ref.make({ directory: AbsolutePath.make(second.path) }) const firstContext = yield* locations.contextEffect(firstRef) const secondContext = yield* locations.contextEffect(secondRef) const received = { first: 0, second: 0 } - yield* events.subscribe(Config.Event.Updated).pipe( + yield* bus.subscribe(Config.Event.Updated).pipe( Stream.runForEach(() => Effect.sync(() => received.first++)), Effect.provideContext(firstContext), Effect.forkScoped({ startImmediately: true }), ) - yield* events.subscribe(Config.Event.Updated).pipe( + yield* bus.subscribe(Config.Event.Updated).pipe( Stream.runForEach(() => Effect.sync(() => received.second++)), Effect.provideContext(secondContext), Effect.forkScoped({ startImmediately: true }), ) yield* Effect.sleep("10 millis") - yield* events.publish(Config.Event.Updated, {}, { location: firstRef }) + yield* bus.publish(Config.Event.Updated, {}, { location: firstRef }) yield* Effect.sleep("10 millis") expect(received).toEqual({ first: 1, second: 0 }) @@ -538,12 +537,12 @@ describe("LocationServiceMap", () => { ).pipe( Effect.flatMap(([blocked, allowed]) => Effect.gen(function* () { - const update = (directory: string, providerID: ProviderV2.ID) => + const update = (directory: string, providerID: Provider.ID) => Effect.gen(function* () { yield* Reference.Service const catalog = yield* Catalog.Service yield* catalog.transform((editor) => editor.provider.update(providerID, () => {})) - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service // Tool plugins register during the forked PluginSupervisor boot; wait for // every expected tool rather than relying on batch ordering. yield* Effect.forEach( @@ -573,8 +572,8 @@ describe("LocationServiceMap", () => { ), ) - const blockedID = ProviderV2.ID.make("blocked-location") - const allowedID = ProviderV2.ID.make("allowed-location") + const blockedID = Provider.ID.make("blocked-location") + const allowedID = Provider.ID.make("allowed-location") const blockedState = yield* update(blocked.path, blockedID) expect(blockedState.providers.some((provider) => provider.id === blockedID)).toBe(true) expect(blockedState.providers.some((provider) => provider.id === allowedID)).toBe(false) @@ -641,13 +640,13 @@ describe("LocationServiceMap", () => { ) const failure = yield* SessionRunnerModel.Service.use((models) => models.resolve( - SessionV2.Info.make({ - id: SessionV2.ID.make("ses_unavailable_model"), - projectID: ProjectV2.ID.global, + Session.Info.make({ + id: Session.ID.make("ses_unavailable_model"), + projectID: Project.ID.global, title: "test", model: { - id: ModelV2.ID.make("chat"), - providerID: ProviderV2.ID.make("unavailable"), + id: Model.ID.make("chat"), + providerID: Provider.ID.make("unavailable"), }, cost: Money.USD.zero, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, @@ -678,25 +677,25 @@ describe("LocationServiceMap", () => { const resolved = yield* Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((editor) => { - editor.provider.update(ProviderV2.ID.make("aliased"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai") + editor.provider.update(Provider.ID.make("aliased"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai") }) - editor.model.update(ProviderV2.ID.make("aliased"), ModelV2.ID.make("fast"), (model) => { + editor.model.update(Provider.ID.make("aliased"), Model.ID.make("fast"), (model) => { // Catalog id and package model id intentionally differ, like gpt-5.5-fast -> gpt-5.5. - model.modelID = ModelV2.ID.make("base") - model.variants = [{ id: ModelV2.VariantID.make("high") }] + model.modelID = Model.ID.make("base") + model.variants = [{ id: Model.VariantID.make("high") }] }) }) const models = yield* SessionRunnerModel.Service return yield* models.resolve( - SessionV2.Info.make({ - id: SessionV2.ID.make("ses_aliased_model"), - projectID: ProjectV2.ID.global, + Session.Info.make({ + id: Session.ID.make("ses_aliased_model"), + projectID: Project.ID.global, title: "test", model: { - id: ModelV2.ID.make("fast"), - providerID: ProviderV2.ID.make("aliased"), - variant: ModelV2.VariantID.make("high"), + id: Model.ID.make("fast"), + providerID: Provider.ID.make("aliased"), + variant: Model.VariantID.make("high"), }, cost: Money.USD.zero, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, @@ -707,10 +706,10 @@ describe("LocationServiceMap", () => { }).pipe(Effect.provide(LocationServiceMap.Service.get(location))) expect(resolved.ref).toEqual( - ModelV2.Ref.make({ - id: ModelV2.ID.make("fast"), - providerID: ProviderV2.ID.make("aliased"), - variant: ModelV2.VariantID.make("high"), + Model.Ref.make({ + id: Model.ID.make("fast"), + providerID: Provider.ID.make("aliased"), + variant: Model.VariantID.make("high"), }), ) expect(String(resolved.model.id)).toBe("base") @@ -726,7 +725,7 @@ describe("LocationServiceMap", () => { ).pipe( Effect.flatMap((dir) => Effect.gen(function* () { - const plugins = yield* PluginV2.Service + const plugins = yield* Plugin.Service const reviewer = EffectPlugin.define({ id: "reviewer", effect: (ctx) => @@ -741,7 +740,7 @@ describe("LocationServiceMap", () => { }) yield* plugins.activate([{ ...reviewer, version: "1" }]) - expect(yield* (yield* AgentV2.Service).get(AgentV2.ID.make("reviewer"))).toMatchObject({ + expect(yield* (yield* Agent.Service).get(Agent.ID.make("reviewer"))).toMatchObject({ description: "Reviews code", mode: "subagent", }) diff --git a/packages/core/test/location.test.ts b/packages/core/test/location.test.ts index d012a0463f96..b51c3c5bd83c 100644 --- a/packages/core/test/location.test.ts +++ b/packages/core/test/location.test.ts @@ -4,10 +4,10 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Location } from "@opencode-ai/core/location" import { Project } from "@opencode-ai/core/project" import { AbsolutePath } from "@opencode-ai/core/schema" -import { WorkspaceV2 } from "@opencode-ai/core/workspace" +import { Workspace } from "@opencode-ai/core/workspace" import { testEffect } from "./lib/effect" -const workspaceID = WorkspaceV2.ID.make("wrk_test") +const workspaceID = Workspace.ID.make("wrk_test") const ref = { directory: AbsolutePath.make("/repo/packages/app"), workspaceID } const projectLayer = Layer.succeed( Project.Service, diff --git a/packages/core/test/mcp-instructions.test.ts b/packages/core/test/mcp-instructions.test.ts index 77b493635482..44efd76b2418 100644 --- a/packages/core/test/mcp-instructions.test.ts +++ b/packages/core/test/mcp-instructions.test.ts @@ -1,18 +1,18 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { MCP } from "@opencode-ai/core/mcp/index" import { McpInstructions } from "@opencode-ai/core/mcp/instructions" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { McpTool } from "@opencode-ai/core/tool/mcp" import { it } from "./lib/effect" import { readInitial, readUpdate } from "./lib/instructions" -const build = AgentV2.ID.make("build") +const build = Agent.ID.make("build") -const selection = (permissions: PermissionV2.Ruleset = []) => { - const info = AgentV2.Info.make({ ...AgentV2.Info.empty(build), permissions }) +const selection = (permissions: Permission.Ruleset = []) => { + const info = Agent.Info.make({ ...Agent.Info.empty(build), permissions }) return { id: info.id, info } } diff --git a/packages/core/test/mcp.test.ts b/packages/core/test/mcp.test.ts index 8b948c9cc9bf..b33fc860d752 100644 --- a/packages/core/test/mcp.test.ts +++ b/packages/core/test/mcp.test.ts @@ -16,18 +16,18 @@ import { Config } from "@opencode-ai/core/config" import { Credential } from "@opencode-ai/core/credential" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" import { Form } from "@opencode-ai/core/form" import { Integration } from "@opencode-ai/core/integration" import { Location } from "@opencode-ai/core/location" import { MCP } from "@opencode-ai/core/mcp/index" import { MCPClient } from "@opencode-ai/core/mcp/client" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { McpTool } from "@opencode-ai/core/tool/mcp" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { Tool } from "@opencode-ai/core/tool" import { Deferred, Effect, Exit, Fiber, Layer, Schema, Stream } from "effect" import { Image } from "@opencode-ai/core/image" import { testEffect } from "./lib/effect" @@ -35,8 +35,8 @@ import { imagePassthrough } from "./lib/image" import { location } from "./fixture/location" import { executeTool, toolDefinitions, toolIdentity, waitForCodeModeTool, waitForTool } from "./lib/tool" -let assertion: Deferred.Deferred | undefined -let decision: Effect.Effect = Effect.void +let assertion: Deferred.Deferred | undefined +let decision: Effect.Effect = Effect.void let calls = 0 type ResourcePage = { @@ -182,14 +182,14 @@ function resourceMcpLayer( }), ), Layer.succeed(Location.Service, Location.Service.of(location({ directory }))), - Layer.mock(EventV2.Service, { + Layer.mock(Bus.Service, { subscribe: () => Stream.never, publish: (definition, data) => { const event = { - id: EventV2.ID.create(), + id: Event.ID.create(), type: definition.type, data, - } as EventV2.Payload + } as Event.Payload if (event.type !== Form.Event.Created.type || !onFormCreated) return Effect.succeed(event) return onFormCreated(Schema.decodeUnknownSync(Form.Event.Created.data)(data).form).pipe(Effect.as(event)) }, @@ -285,7 +285,7 @@ const mcp = Layer.mock(MCP.Service, { }) }), }) -const permissions = Layer.mock(PermissionV2.Service, { +const permissions = Layer.mock(Permission.Service, { assert: (input) => Effect.gen(function* () { if (!assertion) return yield* Effect.die("Permission test is not initialized") @@ -293,13 +293,12 @@ const permissions = Layer.mock(PermissionV2.Service, { yield* decision }), }) -const events = Layer.mock(EventV2.Service, { subscribe: () => Stream.never }) +const events = Layer.mock(Bus.Service, { subscribe: () => Stream.never }) const it = testEffect( - AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, McpTool.node]), [ + AppNodeBuilder.build(LayerNode.group([Tool.node, McpTool.node]), [ [MCP.node, mcp], - [PermissionV2.node, permissions], - [EventV2.node, events], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + [Permission.node, permissions], + [Bus.node, events], [Image.node, imagePassthrough], ]), ) @@ -801,7 +800,7 @@ test("serializes concurrent MCP lifecycle operations", async () => { it.effect("advertises MCP output schemas to Code Mode", () => Effect.gen(function* () { - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const toolSet = yield* waitForCodeModeTool(registry, "demo.search") const execute = toolSet.definitions.find((tool) => tool.name === "execute") @@ -818,7 +817,7 @@ it.effect("advertises MCP output schemas to Code Mode", () => it.effect("advertises MCP tools directly when Code Mode is disabled for the server", () => Effect.gen(function* () { - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service yield* waitForTool(registry, "direct_lookup") const definitions = yield* toolDefinitions(registry) const execute = definitions.find((tool) => tool.name === "execute") @@ -832,38 +831,35 @@ it.effect("advertises MCP tools directly when Code Mode is disabled for the serv // success whose text happens to describe an error. it.effect("fails the call when MCP reports isError", () => Effect.gen(function* () { - assertion = yield* Deferred.make() + assertion = yield* Deferred.make() decision = Effect.void - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service yield* waitForTool(registry, "direct_fail") const execution = yield* executeTool(registry, { - sessionID: SessionV2.ID.make("ses_mcp_is_error"), + sessionID: Session.ID.make("ses_mcp_is_error"), ...toolIdentity, call: { type: "tool-call", id: "call_mcp_is_error", name: "direct_fail", input: {} }, }) expect(execution).toMatchObject({ status: "error", error: { message: "search index unavailable" } }) - expect(execution.content).toBeUndefined() }), ) // Baseline (PLAN.md step 1): mixed MCP text and media content must reach the model intact. it.effect("preserves MCP text and media content for the model", () => Effect.gen(function* () { - assertion = yield* Deferred.make() + assertion = yield* Deferred.make() decision = Effect.void - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service yield* waitForTool(registry, "direct_media") const execution = yield* executeTool(registry, { - sessionID: SessionV2.ID.make("ses_mcp_media"), + sessionID: Session.ID.make("ses_mcp_media"), ...toolIdentity, call: { type: "tool-call", id: "call_mcp_media", name: "direct_media", input: {} }, }) - expect(execution.status).toBe("completed") - if (execution.status !== "completed") return expect(execution.output).toBe("rendered chart") expect(execution.content).toMatchObject([ { type: "text", text: "rendered chart" }, @@ -875,14 +871,14 @@ it.effect("preserves MCP text and media content for the model", () => it.effect("waits for permission before calling an MCP tool", () => Effect.gen(function* () { calls = 0 - assertion = yield* Deferred.make() + assertion = yield* Deferred.make() const permission = yield* Deferred.make() decision = Deferred.await(permission) - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const toolSet = yield* waitForCodeModeTool(registry, "demo.search") const fiber = yield* toolSet.execute({ - sessionID: SessionV2.ID.make("ses_mcp_permission"), + sessionID: Session.ID.make("ses_mcp_permission"), ...toolIdentity, call: { type: "tool-call", @@ -896,7 +892,7 @@ it.effect("waits for permission before calling an MCP tool", () => resources: ["*"], save: ["*"], metadata: {}, - sessionID: SessionV2.ID.make("ses_mcp_permission"), + sessionID: Session.ID.make("ses_mcp_permission"), agent: toolIdentity.agent, source: { type: "tool", @@ -915,13 +911,13 @@ it.effect("waits for permission before calling an MCP tool", () => it.effect("does not call MCP when permission is blocked", () => Effect.gen(function* () { calls = 0 - assertion = yield* Deferred.make() - decision = Effect.fail(new PermissionV2.BlockedError({ rules: [], permission: "demo_search", resources: ["*"] })) - const registry = yield* ToolRegistry.Service + assertion = yield* Deferred.make() + decision = Effect.fail(new Permission.BlockedError({ rules: [], permission: "demo_search", resources: ["*"] })) + const registry = yield* Tool.Service const toolSet = yield* waitForCodeModeTool(registry, "demo.search") const execution = yield* toolSet.execute({ - sessionID: SessionV2.ID.make("ses_mcp_blocked"), + sessionID: Session.ID.make("ses_mcp_blocked"), ...toolIdentity, call: { type: "tool-call", @@ -930,7 +926,6 @@ it.effect("does not call MCP when permission is blocked", () => input: { code: "return await tools.demo.search({})" }, }, }) - expect(execution.status).toBe("completed") expect(execution.content).toEqual([{ type: "text", text: "Unable to execute demo_search" }]) expect(execution.metadata).toEqual({ toolCalls: [{ tool: "demo.search", status: "error" }], diff --git a/packages/core/test/model-resolver.test.ts b/packages/core/test/model-resolver.test.ts index 9eedc4265b64..547893a088f4 100644 --- a/packages/core/test/model-resolver.test.ts +++ b/packages/core/test/model-resolver.test.ts @@ -5,25 +5,25 @@ import { Effect } from "effect" import { Headers } from "effect/unstable/http" import { Credential } from "@opencode-ai/core/credential" import { Integration } from "@opencode-ai/core/integration" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Compatibility, ID, Info, VariantID } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { ModelResolver } from "@opencode-ai/core/model-resolver" import { it } from "./lib/effect" interface ModelOptions { readonly modelID?: string - readonly compatibility?: ModelV2.Compatibility - readonly settings?: ModelV2.Info["settings"] - readonly headers?: ModelV2.Info["headers"] - readonly body?: ModelV2.Info["body"] - readonly variants?: ModelV2.Info["variants"] + readonly compatibility?: Compatibility + readonly settings?: Info["settings"] + readonly headers?: Info["headers"] + readonly body?: Info["body"] + readonly variants?: Info["variants"] } const model = (packageName: string | undefined, options: ModelOptions = {}) => - ModelV2.Info.make({ - id: ModelV2.ID.make("test-model"), - modelID: ModelV2.ID.make(options.modelID ?? "api-test-model"), - providerID: ProviderV2.ID.make("test-provider"), + Info.make({ + id: ID.make("test-model"), + modelID: ID.make(options.modelID ?? "api-test-model"), + providerID: Provider.ID.make("test-provider"), name: "Test model", compatibility: options.compatibility, package: packageName, @@ -42,12 +42,12 @@ const model = (packageName: string | undefined, options: ModelOptions = {}) => describe("ModelResolver", () => { it.effect("uses the API modelID instead of the catalog ID for native OpenAI routes", () => Effect.gen(function* () { - const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), { + const catalog = model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }) const resolved = yield* ModelResolver.fromCatalogModel(catalog) - expect(catalog.id).toBe(ModelV2.ID.make("test-model")) + expect(catalog.id).toBe(ID.make("test-model")) expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" }) expect(resolved.route).toMatchObject({ id: "openai-responses", @@ -65,7 +65,7 @@ describe("ModelResolver", () => { it.effect("keeps catalog apiKey credentials out of provider JSON", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { apiKey: "secret", baseURL: "https://openai.example/v1" }, }), ) @@ -79,7 +79,7 @@ describe("ModelResolver", () => { it.effect("treats an empty configured API key as omitted", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { apiKey: "", baseURL: "https://openai.example/v1" }, }), ) @@ -98,7 +98,7 @@ describe("ModelResolver", () => { it.effect("uses merged API settings for OpenAI-compatible auth and request defaults", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), { + model(Provider.aisdk("@ai-sdk/openai-compatible"), { compatibility: { reasoningField: "vendor_reasoning" }, settings: { apiKey: "settings-secret", @@ -128,11 +128,11 @@ describe("ModelResolver", () => { it.effect("overlays selected OpenAI variant settings and bodies", () => Effect.gen(function* () { - const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), { + const catalog = model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, variants: [ { - id: ModelV2.VariantID.make("high"), + id: VariantID.make("high"), settings: { reasoningEffort: "high" }, headers: { "x-variant": "high" }, body: { @@ -143,7 +143,7 @@ describe("ModelResolver", () => { }, ], }) - const resolved = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("high")) + const resolved = yield* ModelResolver.resolveModel(catalog, VariantID.make("high")) expect(resolved.route.defaults.headers).toMatchObject({ "x-test": "header", "x-variant": "high" }) expect(resolved.route.defaults.http?.body).toEqual({ @@ -160,18 +160,18 @@ describe("ModelResolver", () => { it.effect("overlays selected OpenAI-compatible variant bodies", () => Effect.gen(function* () { - const catalog = model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), { + const catalog = model(Provider.aisdk("@ai-sdk/openai-compatible"), { settings: { baseURL: "https://compatible.example/v1" }, variants: [ { - id: ModelV2.VariantID.make("high"), + id: VariantID.make("high"), settings: {}, headers: {}, body: { store: false, reasoning_effort: "high" }, }, ], }) - const resolved = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("high")) + const resolved = yield* ModelResolver.resolveModel(catalog, VariantID.make("high")) expect(resolved.route.defaults.http?.body).toEqual({ custom_extension: { enabled: true }, @@ -183,10 +183,10 @@ describe("ModelResolver", () => { it.effect("rejects an explicit unavailable variant during model resolution", () => Effect.gen(function* () { - const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), { + const catalog = model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }) - const failure = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("unknown")).pipe(Effect.flip) + const failure = yield* ModelResolver.resolveModel(catalog, VariantID.make("unknown")).pipe(Effect.flip) expect(failure).toMatchObject({ _tag: "SessionRunnerModel.VariantUnavailableError", @@ -200,18 +200,18 @@ describe("ModelResolver", () => { it.effect("overlays selected Anthropic variant settings", () => Effect.gen(function* () { - const catalog = model(ProviderV2.aisdk("@ai-sdk/anthropic"), { + const catalog = model(Provider.aisdk("@ai-sdk/anthropic"), { settings: { baseURL: "https://anthropic.example/v1" }, variants: [ { - id: ModelV2.VariantID.make("high"), + id: VariantID.make("high"), settings: { thinking: { type: "enabled", budgetTokens: 12000 } }, headers: {}, body: {}, }, ], }) - const resolved = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("high")) + const resolved = yield* ModelResolver.resolveModel(catalog, VariantID.make("high")) expect(resolved.route.defaults.http?.body).toEqual({ custom_extension: { enabled: true }, @@ -225,7 +225,7 @@ describe("ModelResolver", () => { it.effect("maps catalog Anthropic AI SDK models into native routes", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/anthropic"), { + model(Provider.aisdk("@ai-sdk/anthropic"), { settings: { baseURL: "https://anthropic.example/v1" }, }), ) @@ -241,7 +241,7 @@ describe("ModelResolver", () => { it.effect("uses resolved credentials for bearer auth", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, headers: {}, body: {}, @@ -265,7 +265,7 @@ describe("ModelResolver", () => { Effect.gen(function* () { const credential = Credential.Key.make({ type: "key", key: "stored-secret", metadata: { tenant: "work" } }) const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { apiKey: "configured-secret", baseURL: "https://openai.example/v1" }, headers: {}, body: {}, @@ -288,7 +288,7 @@ describe("ModelResolver", () => { it.effect("does not project OAuth account metadata into the request body", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, headers: {}, body: {}, @@ -310,7 +310,7 @@ describe("ModelResolver", () => { it.effect("routes ChatGPT OAuth credentials to the codex backend", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, headers: {}, body: {}, @@ -395,7 +395,7 @@ describe("ModelResolver", () => { it.effect("maps legacy OpenAI organization and project settings to headers", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { organization: "org_123", project: "proj_123" }, }), ) @@ -410,7 +410,7 @@ describe("ModelResolver", () => { it.effect("routes ChatGPT OAuth credentials without an account id to the codex backend", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, headers: {}, body: {}, @@ -441,7 +441,7 @@ describe("ModelResolver", () => { it.effect("keeps non-ChatGPT OAuth credentials on the configured endpoint", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, headers: {}, body: {}, @@ -473,7 +473,7 @@ describe("ModelResolver", () => { it.effect("loads dynamic native provider packages through the injected package loader", () => Effect.gen(function* () { const native = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }), ) @@ -510,7 +510,7 @@ describe("ModelResolver", () => { it.effect("maps OAuth credentials to native provider auth settings", () => Effect.gen(function* () { const native = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }), ) @@ -549,12 +549,12 @@ describe("ModelResolver", () => { it.effect("loads arbitrary AISDK packages through the injected AISDK loader", () => Effect.gen(function* () { const native = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }), ) const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/google"), { + model(Provider.aisdk("@ai-sdk/google"), { modelID: "gemini-api-model", settings: { project: "test" }, headers: { "x-aisdk": "header" }, @@ -568,7 +568,7 @@ describe("ModelResolver", () => { id: "test-model", modelID: "gemini-api-model", providerID: "test-provider", - package: ProviderV2.aisdk("@ai-sdk/google"), + package: Provider.aisdk("@ai-sdk/google"), settings: { project: "test", apiKey: "fallback-secret" }, headers: { "x-aisdk": "header" }, body: { custom: true }, @@ -589,7 +589,7 @@ describe("ModelResolver", () => { it.effect("rejects AISDK packages without an available loader", () => Effect.gen(function* () { const failure = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/google"), { + model(Provider.aisdk("@ai-sdk/google"), { settings: { baseURL: "https://google.example/v1" }, }), ).pipe(Effect.flip) @@ -607,12 +607,12 @@ describe("ModelResolver", () => { it.effect("drops an empty API key before loading an AISDK package", () => Effect.gen(function* () { const native = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }), ) yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/google"), { + model(Provider.aisdk("@ai-sdk/google"), { settings: { apiKey: "", baseURL: "https://google.example/v1" }, }), undefined, @@ -629,7 +629,7 @@ describe("ModelResolver", () => { it.effect("reports whether a catalog model declares a provider package", () => Effect.sync(() => { - expect(ModelResolver.supported(model(ProviderV2.aisdk("@ai-sdk/openai")))).toBe(true) + expect(ModelResolver.supported(model(Provider.aisdk("@ai-sdk/openai")))).toBe(true) expect(ModelResolver.supported(model("@opencode-ai/ai/providers/custom"))).toBe(true) expect(ModelResolver.supported(model(undefined))).toBe(false) }), diff --git a/packages/core/test/model.test.ts b/packages/core/test/model.test.ts index fe97acc25aad..0c545eeeefdb 100644 --- a/packages/core/test/model.test.ts +++ b/packages/core/test/model.test.ts @@ -1,23 +1,23 @@ import { describe, expect, test } from "bun:test" import { Schema } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" -const decode = Schema.decodeUnknownSync(ModelV2.Ref) +const decode = Schema.decodeUnknownSync(Model.Ref) -describe("ModelV2.Ref", () => { +describe("Model.Ref", () => { test("accepts a model selection without a variant", () => { expect(decode({ id: "claude-sonnet", providerID: "anthropic" })).toEqual({ - id: ModelV2.ID.make("claude-sonnet"), - providerID: ProviderV2.ID.make("anthropic"), + id: Model.ID.make("claude-sonnet"), + providerID: Provider.ID.make("anthropic"), }) }) test("preserves an explicit model variant", () => { expect(decode({ id: "claude-sonnet", providerID: "anthropic", variant: "high" })).toEqual({ - id: ModelV2.ID.make("claude-sonnet"), - providerID: ProviderV2.ID.make("anthropic"), - variant: ModelV2.VariantID.make("high"), + id: Model.ID.make("claude-sonnet"), + providerID: Provider.ID.make("anthropic"), + variant: Model.VariantID.make("high"), }) }) }) diff --git a/packages/core/test/models.test.ts b/packages/core/test/models.test.ts index 1d8bd4ae184c..e848f65c98d6 100644 --- a/packages/core/test/models.test.ts +++ b/packages/core/test/models.test.ts @@ -6,9 +6,9 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNodePlatform } from "@opencode-ai/util/effect/app-node-platform" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Global } from "@opencode-ai/util/global" -import { ModelV2 } from "@opencode-ai/core/model" +import { Model } from "@opencode-ai/core/model" import { ModelsDev } from "@opencode-ai/core/models-dev" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { it } from "./lib/effect" import { readFile, rm, writeFile, utimes, mkdir } from "fs/promises" import path from "path" @@ -16,10 +16,10 @@ import path from "path" const cacheFile = path.join(Global.Path.cache, "models.json") test("normalizes permissive interleaved values to compatibility", () => { - expect(ModelV2.compatibility("reasoning_text")).toEqual({ reasoningField: "reasoning_text" }) - expect(ModelV2.compatibility({ field: "vendor_reasoning" })).toEqual({ reasoningField: "vendor_reasoning" }) - expect(ModelV2.compatibility(true)).toBeUndefined() - expect(ModelV2.compatibility(false)).toBeUndefined() + expect(Model.compatibility("reasoning_text")).toEqual({ reasoningField: "reasoning_text" }) + expect(Model.compatibility({ field: "vendor_reasoning" })).toEqual({ reasoningField: "vendor_reasoning" }) + expect(Model.compatibility(true)).toBeUndefined() + expect(Model.compatibility(false)).toBeUndefined() }) const fixture = { @@ -47,15 +47,15 @@ const fixture = { const fixtureSnapshot = [ { info: { - id: ProviderV2.ID.make("acme"), + id: Provider.ID.make("acme"), name: "Acme", - package: ProviderV2.aisdk("@ai-sdk/openai-compatible"), + package: Provider.aisdk("@ai-sdk/openai-compatible"), }, models: [ { - id: ModelV2.ID.make("acme-1"), - modelID: ModelV2.ID.make("acme-1"), - providerID: ProviderV2.ID.make("acme"), + id: Model.ID.make("acme-1"), + modelID: Model.ID.make("acme-1"), + providerID: Provider.ID.make("acme"), name: "Acme One", compatibility: { reasoningField: "vendor_reasoning" }, family: undefined, @@ -109,15 +109,15 @@ const fixture2 = { const fixture2Snapshot = [ { info: { - id: ProviderV2.ID.make("beta"), + id: Provider.ID.make("beta"), name: "Beta", - package: ProviderV2.aisdk("@ai-sdk/openai-compatible"), + package: Provider.aisdk("@ai-sdk/openai-compatible"), }, models: [ { - id: ModelV2.ID.make("beta-1"), - modelID: ModelV2.ID.make("beta-1"), - providerID: ProviderV2.ID.make("beta"), + id: Model.ID.make("beta-1"), + modelID: Model.ID.make("beta-1"), + providerID: Provider.ID.make("beta"), name: "Beta One", family: undefined, package: undefined, diff --git a/packages/core/test/move-session.test.ts b/packages/core/test/move-session.test.ts index 952bd292413a..9a37f143b16a 100644 --- a/packages/core/test/move-session.test.ts +++ b/packages/core/test/move-session.test.ts @@ -8,13 +8,13 @@ import { MoveSession } from "@opencode-ai/core/control-plane/move-session" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Job } from "@opencode-ai/core/job" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { ProjectDirectories } from "@opencode-ai/core/project/directories" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionExecution } from "@opencode-ai/core/session/execution" import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionTable } from "@opencode-ai/core/session/sql" @@ -40,10 +40,10 @@ const it = testEffect( LayerNode.group([ MoveSession.node, Database.node, - EventV2.node, + Bus.node, ProjectDirectories.node, Project.node, - SessionV2.node, + Session.node, SessionProjector.node, SessionStore.node, ]), @@ -86,7 +86,7 @@ describe("MoveSession", () => { yield* Effect.promise(() => fs.writeFile(path.join(source, "untracked.txt"), "new\n")) const projectID = (yield* Project.Service.use((service) => service.resolve(source))).id - const sessionID = SessionV2.ID.make("ses_move") + const sessionID = Session.ID.make("ses_move") const { db } = yield* Database.Service yield* db .insert(ProjectTable) @@ -142,7 +142,7 @@ describe("MoveSession", () => { yield* Effect.promise(() => fs.writeFile(path.join(source, "untracked.txt"), "new\n")) const projectID = (yield* Project.Service.use((service) => service.resolve(source))).id - const sessionID = SessionV2.ID.make("ses_move_nested") + const sessionID = Session.ID.make("ses_move_nested") const { db } = yield* Database.Service yield* db .insert(ProjectTable) @@ -164,7 +164,7 @@ describe("MoveSession", () => { .run() .pipe(Effect.orDie) - const missing = yield* SessionV2.Service.use((service) => + const missing = yield* Session.Service.use((service) => service.move({ sessionID, directory: abs("packages") }).pipe(Effect.flip), ) expect(missing._tag).toBe("Session.DestinationNotFoundError") @@ -202,7 +202,7 @@ describe("MoveSession", () => { const projectID = (yield* Project.Service.use((service) => service.resolve(source))).id const destinationProjectID = (yield* Project.Service.use((service) => service.resolve(destination))).id - const sessionID = SessionV2.ID.make("ses_move_project") + const sessionID = Session.ID.make("ses_move_project") const { db } = yield* Database.Service yield* db .insert(ProjectTable) @@ -224,7 +224,7 @@ describe("MoveSession", () => { .run() .pipe(Effect.orDie) - yield* SessionV2.Service.use((service) => + yield* Session.Service.use((service) => service.move({ sessionID, directory: destination }), ) @@ -266,7 +266,7 @@ describe("MoveSession", () => { yield* Effect.promise(() => fs.writeFile(path.join(source, "untracked.txt"), "unrelated\n")) const projectID = (yield* Project.Service.use((service) => service.resolve(source))).id - const sessionID = SessionV2.ID.make("ses_move_nested_checkout") + const sessionID = Session.ID.make("ses_move_nested_checkout") const { db } = yield* Database.Service yield* db .insert(ProjectTable) diff --git a/packages/core/test/permission.test.ts b/packages/core/test/permission.test.ts index 6abf9c47ca62..7e7ac0392c6d 100644 --- a/packages/core/test/permission.test.ts +++ b/packages/core/test/permission.test.ts @@ -1,19 +1,19 @@ import { describe, expect } from "bun:test" import { Cause, Deferred, Effect, Fiber, Layer } from "effect" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Job } from "@opencode-ai/core/job" import { Location } from "@opencode-ai/core/location" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { PermissionTable } from "@opencode-ai/core/permission/sql" import { PermissionSaved } from "@opencode-ai/core/permission/saved" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionTable } from "@opencode-ai/core/session/sql" import { SessionStore } from "@opencode-ai/core/session/store" import { eq } from "drizzle-orm" @@ -28,17 +28,17 @@ const it = testEffect( AppNodeBuilder.build( LayerNode.group([ Database.node, - EventV2.node, + Bus.node, SessionStore.node, PermissionSaved.node, - AgentV2.node, - PermissionV2.node, + Agent.node, + Permission.node, ]), [[Location.node, current]], ), ) -function setup(rules: PermissionV2.Ruleset = []) { +function setup(rules: Permission.Ruleset = []) { return Effect.gen(function* () { const { db } = yield* Database.Service yield* db @@ -50,7 +50,7 @@ function setup(rules: PermissionV2.Ruleset = []) { yield* db .insert(SessionTable) .values({ - id: SessionV2.ID.make("ses_test"), + id: Session.ID.make("ses_test"), project_id: Project.ID.global, slug: "test", directory: "/project", @@ -65,35 +65,35 @@ function setup(rules: PermissionV2.Ruleset = []) { }) } -function setRules(rules: PermissionV2.Ruleset) { +function setRules(rules: Permission.Ruleset) { return Effect.gen(function* () { - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service yield* agents.transform((editor) => - editor.update(AgentV2.ID.make("test"), (agent) => { + editor.update(Agent.ID.make("test"), (agent) => { agent.permissions = [...rules] }), ) }) } -function assertion(input: Partial = {}) { +function assertion(input: Partial = {}) { return { - id: PermissionV2.ID.create("per_test"), - sessionID: SessionV2.ID.make("ses_test"), + id: Permission.ID.create("per_test"), + sessionID: Session.ID.make("ses_test"), action: "read", resources: ["src/index.ts"], ...input, - } satisfies PermissionV2.AssertInput + } satisfies Permission.AssertInput } function waitForRequest() { return Effect.gen(function* () { - const service = yield* PermissionV2.Service - const events = yield* EventV2.Service - const asked = yield* Deferred.make() - const unsubscribe = yield* events.listen((event) => - event.type === PermissionV2.Event.Asked.type - ? Deferred.succeed(asked, event.data as PermissionV2.Request).pipe(Effect.asVoid) + const service = yield* Permission.Service + const bus = yield* Bus.Service + const asked = yield* Deferred.make() + const unsubscribe = yield* bus.listen((event) => + event.type === Permission.Event.Asked.type + ? Deferred.succeed(asked, event.data as Permission.Request).pipe(Effect.asVoid) : Effect.void, ) yield* Effect.addFinalizer(() => unsubscribe) @@ -103,53 +103,53 @@ function waitForRequest() { }) } -describe("PermissionV2", () => { +describe("Permission", () => { it.effect("returns the evaluated effect and only queues prompts", () => Effect.gen(function* () { yield* setup([{ action: "read", resource: "*", effect: "allow" }]) - const service = yield* PermissionV2.Service - expect(yield* service.ask(assertion())).toEqual({ id: PermissionV2.ID.create("per_test"), effect: "allow" }) + const service = yield* Permission.Service + expect(yield* service.ask(assertion())).toEqual({ id: Permission.ID.create("per_test"), effect: "allow" }) expect(yield* service.list()).toEqual([]) yield* setRules([{ action: "read", resource: "*", effect: "deny" }]) - expect(yield* service.ask(assertion())).toEqual({ id: PermissionV2.ID.create("per_test"), effect: "deny" }) + expect(yield* service.ask(assertion())).toEqual({ id: Permission.ID.create("per_test"), effect: "deny" }) expect(yield* service.list()).toEqual([]) yield* setRules([]) - expect(yield* service.ask(assertion())).toEqual({ id: PermissionV2.ID.create("per_test"), effect: "ask" }) - expect(yield* service.get(PermissionV2.ID.create("per_test"))).toBeDefined() + expect(yield* service.ask(assertion())).toEqual({ id: Permission.ID.create("per_test"), effect: "ask" }) + expect(yield* service.get(Permission.ID.create("per_test"))).toBeDefined() }), ) it.effect("evaluates against an explicit provider-turn agent", () => Effect.gen(function* () { yield* setup([{ action: "read", resource: "*", effect: "allow" }]) - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service yield* agents.transform((editor) => - editor.update(AgentV2.ID.make("reviewer"), (agent) => { + editor.update(Agent.ID.make("reviewer"), (agent) => { agent.permissions.push({ action: "read", resource: "*", effect: "deny" }) }), ) - const service = yield* PermissionV2.Service + const service = yield* Permission.Service expect(yield* service.ask(assertion())).toMatchObject({ effect: "allow" }) - expect(yield* service.ask(assertion({ agent: AgentV2.ID.make("reviewer") }))).toMatchObject({ effect: "deny" }) + expect(yield* service.ask(assertion({ agent: Agent.ID.make("reviewer") }))).toMatchObject({ effect: "deny" }) yield* agents.transform((editor) => - editor.update(AgentV2.ID.make("reviewer"), (agent) => { + editor.update(Agent.ID.make("reviewer"), (agent) => { agent.permissions = [] }), ) - expect(yield* service.ask(assertion({ agent: AgentV2.ID.make("reviewer") }))).toMatchObject({ effect: "ask" }) - expect(yield* service.get(PermissionV2.ID.create("per_test"))).not.toHaveProperty("agent") + expect(yield* service.ask(assertion({ agent: Agent.ID.make("reviewer") }))).toMatchObject({ effect: "ask" }) + expect(yield* service.get(Permission.ID.create("per_test"))).not.toHaveProperty("agent") }), ) it.effect("allows and denies from explicit rules without asking", () => Effect.gen(function* () { yield* setup([{ action: "read", resource: "*", effect: "allow" }]) - const service = yield* PermissionV2.Service + const service = yield* Permission.Service yield* service.assert(assertion()) yield* setRules([{ action: "read", resource: "*", effect: "deny" }]) const blocked = yield* service.assert(assertion()).pipe(Effect.flip) - expect(blocked).toBeInstanceOf(PermissionV2.BlockedError) + expect(blocked).toBeInstanceOf(Permission.BlockedError) expect(yield* service.list()).toEqual([]) }), ) @@ -160,7 +160,7 @@ describe("PermissionV2", () => { { action: "*", resource: "*", effect: "deny" }, { action: "read", resource: "*", effect: "allow" }, ]) - const service = yield* PermissionV2.Service + const service = yield* Permission.Service expect(yield* service.ask(assertion({ resources: ["tool_123"] }))).toMatchObject({ effect: "allow" }) expect( @@ -176,19 +176,19 @@ describe("PermissionV2", () => { yield* db .update(SessionTable) .set({ agent: null }) - .where(eq(SessionTable.id, SessionV2.ID.make("ses_test"))) + .where(eq(SessionTable.id, Session.ID.make("ses_test"))) .run() .pipe(Effect.orDie) - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service yield* agents.transform((editor) => - editor.update(AgentV2.ID.make("build"), (agent) => { + editor.update(Agent.ID.make("build"), (agent) => { agent.permissions = [{ action: "custom", resource: "*", effect: "allow" }] }), ) - const service = yield* PermissionV2.Service + const service = yield* Permission.Service expect(yield* service.ask(assertion({ action: "custom", resources: ["*"] }))).toEqual({ - id: PermissionV2.ID.create("per_test"), + id: Permission.ID.create("per_test"), effect: "allow", }) expect(yield* service.list()).toEqual([]) @@ -202,17 +202,17 @@ describe("PermissionV2", () => { yield* db .update(SessionTable) .set({ agent: null }) - .where(eq(SessionTable.id, SessionV2.ID.make("ses_test"))) + .where(eq(SessionTable.id, Session.ID.make("ses_test"))) .run() .pipe(Effect.orDie) - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service yield* agents.transform((editor) => { - editor.remove(AgentV2.ID.make("test")) - editor.remove(AgentV2.ID.make("build")) + editor.remove(Agent.ID.make("test")) + editor.remove(Agent.ID.make("build")) }) - const service = yield* PermissionV2.Service - expect(yield* service.ask(assertion())).toEqual({ id: PermissionV2.ID.create("per_test"), effect: "deny" }) + const service = yield* Permission.Service + expect(yield* service.ask(assertion())).toEqual({ id: Permission.ID.create("per_test"), effect: "deny" }) expect(yield* service.list()).toEqual([]) }), ) @@ -220,13 +220,13 @@ describe("PermissionV2", () => { it.effect("evaluates bash with the normal configured-rule semantics", () => Effect.gen(function* () { yield* setup([{ action: "*", resource: "*", effect: "allow" }]) - const service = yield* PermissionV2.Service + const service = yield* Permission.Service const bash = assertion({ action: "bash", resources: ["pwd"] }) - expect(yield* service.ask(bash)).toEqual({ id: PermissionV2.ID.create("per_test"), effect: "allow" }) + expect(yield* service.ask(bash)).toEqual({ id: Permission.ID.create("per_test"), effect: "allow" }) yield* setRules([]) - expect(yield* service.ask(bash)).toEqual({ id: PermissionV2.ID.create("per_test"), effect: "ask" }) - expect(yield* service.get(PermissionV2.ID.create("per_test"))).toBeDefined() + expect(yield* service.ask(bash)).toEqual({ id: Permission.ID.create("per_test"), effect: "ask" }) + expect(yield* service.get(Permission.ID.create("per_test"))).toBeDefined() }), ) @@ -236,16 +236,16 @@ describe("PermissionV2", () => { const saved = yield* PermissionSaved.Service yield* saved.add({ projectID: Project.ID.global, action: "bash", resources: ["pwd"] }) - const service = yield* PermissionV2.Service + const service = yield* Permission.Service expect(yield* service.ask(assertion({ action: "bash", resources: ["pwd"] }))).toEqual({ - id: PermissionV2.ID.create("per_test"), + id: Permission.ID.create("per_test"), effect: "allow", }) expect(yield* service.list()).toEqual([]) yield* setRules([{ action: "bash", resource: "*", effect: "deny" }]) expect(yield* service.ask(assertion({ action: "bash", resources: ["pwd"] }))).toEqual({ - id: PermissionV2.ID.create("per_test"), + id: Permission.ID.create("per_test"), effect: "deny", }) }), @@ -257,7 +257,7 @@ describe("PermissionV2", () => { const { service, fiber, request } = yield* waitForRequest() expect(yield* service.list()).toEqual([request]) expect(yield* service.forSession(request.sessionID)).toEqual([request]) - expect(yield* service.forSession(SessionV2.ID.make("ses_other"))).toEqual([]) + expect(yield* service.forSession(Session.ID.make("ses_other"))).toEqual([]) expect(yield* service.get(request.id)).toEqual(request) yield* service.reply({ requestID: request.id, reply: "once" }) yield* Fiber.join(fiber) @@ -277,7 +277,7 @@ describe("PermissionV2", () => { if (exit._tag === "Failure") expect( exit.cause.reasons.some( - (reason) => Cause.isDieReason(reason) && reason.defect instanceof PermissionV2.DeclinedError, + (reason) => Cause.isDieReason(reason) && reason.defect instanceof Permission.DeclinedError, ), ).toBe(true) expect(yield* service.list()).toEqual([]) @@ -287,12 +287,12 @@ describe("PermissionV2", () => { it.effect("stores and removes saved resources for a project", () => Effect.gen(function* () { yield* setup() - const service = yield* PermissionV2.Service - const asked = yield* Deferred.make() - const events = yield* EventV2.Service - const unsubscribe = yield* events.listen((event) => - event.type === PermissionV2.Event.Asked.type - ? Deferred.succeed(asked, event.data as PermissionV2.Request).pipe(Effect.asVoid) + const service = yield* Permission.Service + const asked = yield* Deferred.make() + const bus = yield* Bus.Service + const unsubscribe = yield* bus.listen((event) => + event.type === Permission.Event.Asked.type + ? Deferred.succeed(asked, event.data as Permission.Request).pipe(Effect.asVoid) : Effect.void, ) yield* Effect.addFinalizer(() => unsubscribe) @@ -308,7 +308,7 @@ describe("PermissionV2", () => { const saved = yield* PermissionSaved.Service const id = (yield* saved.list())[0]!.id expect(yield* saved.list()).toEqual([{ id, projectID: Project.ID.global, action: "read", resource: "src/*" }]) - yield* service.assert(assertion({ id: PermissionV2.ID.create("per_next"), resources: ["src/next.ts"] })) + yield* service.assert(assertion({ id: Permission.ID.create("per_next"), resources: ["src/next.ts"] })) yield* saved.remove(id) expect(yield* saved.list()).toEqual([]) }), diff --git a/packages/core/test/plugin.test.ts b/packages/core/test/plugin.test.ts index ca191a8b56da..e2f0b88a94f9 100644 --- a/packages/core/test/plugin.test.ts +++ b/packages/core/test/plugin.test.ts @@ -1,16 +1,14 @@ import { describe, expect } from "bun:test" import { Context, Effect, Exit, Fiber, Schema, Stream } from "effect" -import { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect" +import { Plugin as EffectPlugin } from "@opencode-ai/plugin/effect" import { Config as ConfigSchema } from "@opencode-ai/schema/config" -import { Plugin } from "@opencode-ai/schema/plugin" -import { AgentV2 } from "@opencode-ai/core/agent" -import { EventV2 } from "@opencode-ai/core/event" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Agent } from "@opencode-ai/core/agent" +import { Bus } from "@opencode-ai/core/bus" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionMessage } from "@opencode-ai/core/session/message" -import { Tool } from "@opencode-ai/core/tool/tool" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" +import { Tool } from "@opencode-ai/core/tool" import { testEffect } from "./lib/effect" import { PluginTestLayer } from "./plugin/fixture" @@ -20,11 +18,11 @@ class Secret extends Context.Service()("@opencode/test/PluginSec const versioned = (plugin: EffectPlugin.Plugin, version = "1") => ({ ...plugin, version }) -describe("PluginV2", () => { +describe("Plugin", () => { it.live("exposes public events through the plugin context", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service - const events = yield* EventV2.Service + const plugins = yield* Plugin.Service + const bus = yield* Bus.Service const host = yield* PluginHost.make(plugins) const received = yield* host.event.subscribe().pipe( Stream.filter((event) => event.type === "config.updated"), @@ -33,7 +31,7 @@ describe("PluginV2", () => { ) yield* Effect.sleep("10 millis") - yield* events.publish(ConfigSchema.Event.Updated, {}) + yield* bus.publish(ConfigSchema.Event.Updated, {}) expect((yield* Fiber.join(received)).valueOrUndefined?.type).toBe("config.updated") }), @@ -41,12 +39,12 @@ describe("PluginV2", () => { it.effect("replaces plugins by ID and version", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service - const agents = yield* AgentV2.Service - const events = yield* EventV2.Service + const plugins = yield* Plugin.Service + const agents = yield* Agent.Service + const bus = yield* Bus.Service let description = "first" let updates = 0 - const unsubscribe = yield* events.listen((event) => + const unsubscribe = yield* bus.listen((event) => Effect.sync(() => { if (event.type === Plugin.Event.Updated.type) updates++ }), @@ -67,19 +65,19 @@ describe("PluginV2", () => { yield* plugins.activate([versioned(managed(), "1")]) - expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("first") + expect((yield* agents.get(Agent.ID.make("configured")))?.description).toBe("first") description = "second" yield* plugins.activate([versioned(managed(), "2")]) - expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("second") + expect((yield* agents.get(Agent.ID.make("configured")))?.description).toBe("second") description = "third" yield* plugins.activate([versioned(managed(), "2")]) expect(updates).toBe(2) - expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("second") + expect((yield* agents.get(Agent.ID.make("configured")))?.description).toBe("second") yield* plugins.activate([]) - expect(yield* agents.get(AgentV2.ID.make("configured"))).toBeUndefined() + expect(yield* agents.get(Agent.ID.make("configured"))).toBeUndefined() expect(updates).toBe(3) yield* unsubscribe }), @@ -87,7 +85,7 @@ describe("PluginV2", () => { it.effect("rejects duplicate IDs before replacing active plugins", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service + const plugins = yield* Plugin.Service const active = Plugin.ID.make("active") const duplicate = "duplicate" yield* plugins.activate([{ id: active, version: "1", effect: () => Effect.void }]) @@ -106,8 +104,8 @@ describe("PluginV2", () => { it.effect("skips failed plugins and loads the rest", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service - const agents = yield* AgentV2.Service + const plugins = yield* Plugin.Service + const agents = yield* Agent.Service let fail = true const good = EffectPlugin.define({ id: "good", @@ -130,7 +128,7 @@ describe("PluginV2", () => { yield* plugins.activate([versioned(good), versioned(bad)]) expect(yield* plugins.list()).toEqual([{ id: Plugin.ID.make("good") }]) - expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("loaded") + expect((yield* agents.get(Agent.ID.make("configured")))?.description).toBe("loaded") fail = false yield* plugins.activate([versioned(good), versioned(bad, "2")]) @@ -140,8 +138,8 @@ describe("PluginV2", () => { it.effect("restores the previous plugin when its replacement fails", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service - const agents = yield* AgentV2.Service + const plugins = yield* Plugin.Service + const agents = yield* Agent.Service const previous = EffectPlugin.define({ id: "managed", effect: (ctx) => @@ -170,14 +168,14 @@ describe("PluginV2", () => { yield* plugins.activate([versioned(replacement, "2")]) expect(yield* plugins.list()).toEqual([{ id: Plugin.ID.make("managed") }]) - expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("previous") + expect((yield* agents.get(Agent.ID.make("configured")))?.description).toBe("previous") }), ) it.effect("deactivates a plugin when replacement and restoration fail", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service - const agents = yield* AgentV2.Service + const plugins = yield* Plugin.Service + const agents = yield* Agent.Service let loads = 0 const previous = EffectPlugin.define({ id: "managed", @@ -202,13 +200,13 @@ describe("PluginV2", () => { yield* plugins.activate([versioned(replacement, "2")]) expect(yield* plugins.list()).toEqual([]) - expect(yield* agents.get(AgentV2.ID.make("configured"))).toBeUndefined() + expect(yield* agents.get(Agent.ID.make("configured"))).toBeUndefined() }), ) it.effect("closes the previous generation in reverse order", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service + const plugins = yield* Plugin.Service const closed: string[] = [] yield* plugins.activate( ["first", "second"].map((id) => ({ @@ -226,7 +224,7 @@ describe("PluginV2", () => { it.effect("isolates plugins from ambient services", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service + const plugins = yield* Plugin.Service let visible = true const plugin = EffectPlugin.define({ id: "isolated", @@ -245,22 +243,22 @@ describe("PluginV2", () => { it.effect("registers location tools through the plugin context", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service - const registry = yield* ToolRegistry.Service + const plugins = yield* Plugin.Service + const registry = yield* Tool.Service const plugin = EffectPlugin.define({ id: "tool-plugin", effect: (ctx) => ctx.tool .transform((draft) => draft.add( - "plugin_tool", - Tool.make({ + ({ + name: "plugin_tool", + options: { codemode: false }, description: "Plugin tool", input: Schema.Struct({}), output: Schema.Struct({ ok: Schema.Boolean }), execute: () => Effect.succeed({ output: { ok: true } }), }), - { codemode: false }, ), ) .pipe(Effect.orDie), @@ -276,10 +274,12 @@ describe("PluginV2", () => { it.effect("namespaces tool names and routes codemode registrations through execute", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service - const registry = yield* ToolRegistry.Service - const tool = (description: string) => - Tool.make({ + const plugins = yield* Plugin.Service + const registry = yield* Tool.Service + const tool = (name: string, description: string, options?: Tool.Options) => + ({ + name, + options, description, input: Schema.Struct({}), output: Schema.Struct({ ok: Schema.Boolean }), @@ -290,9 +290,9 @@ describe("PluginV2", () => { effect: (ctx) => ctx.tool .transform((draft) => { - draft.add("plain", tool("Plain"), { codemode: false }) - draft.add("look/up", tool("Lookup"), { namespace: "context7", codemode: false }) - draft.add("search", tool("Search"), { namespace: "context7" }) + draft.add(tool("plain", "Plain", { codemode: false })) + draft.add(tool("look/up", "Lookup", { namespace: "context7", codemode: false })) + draft.add(tool("search", "Search", { namespace: "context7" })) }) .pipe(Effect.orDie), }) @@ -309,8 +309,8 @@ describe("PluginV2", () => { it.effect("fires before/after tool hooks with mutable events around execution", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service - const registry = yield* ToolRegistry.Service + const plugins = yield* Plugin.Service + const registry = yield* Tool.Service const executed: unknown[] = [] const seen: { before?: unknown @@ -324,15 +324,15 @@ describe("PluginV2", () => { yield* ctx.tool .transform((draft) => draft.add( - "echo", - Tool.make({ + ({ + name: "echo", + options: { codemode: false }, description: "Echo", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), execute: ({ text }) => Effect.sync(() => executed.push({ text })).pipe(Effect.as({ output: { text } })), }), - { codemode: false }, ), ) .pipe(Effect.orDie) @@ -352,12 +352,15 @@ describe("PluginV2", () => { seen.after = { input: event.input, status: event.status, - content: event.content, - metadata: event.metadata, + content: event.status === "completed" ? event.result.content : undefined, + metadata: event.status === "completed" ? event.result.metadata : event.error.metadata, } if (event.status !== "completed") return - event.content = [{ type: "text", text: "after-mutated" }] - event.metadata = { rewritten: true } + event.result = { + ...event.result, + content: [{ type: "text", text: "after-mutated" }], + metadata: { rewritten: true }, + } }), ) .pipe(Effect.asVoid) @@ -365,7 +368,7 @@ describe("PluginV2", () => { yield* ctx.tool .hook("execute.after", (event) => Effect.sync(() => { - if (event.status === "completed") (event.content as unknown as unknown[]).splice(0) + if (event.status === "completed" && Array.isArray(event.result.content)) event.result.content.splice(0) }), ) .pipe(Effect.asVoid) @@ -376,8 +379,8 @@ describe("PluginV2", () => { const toolSet = yield* registry.snapshot() const execution = yield* toolSet.execute({ - sessionID: SessionV2.ID.make("ses_hooks"), - agent: AgentV2.ID.make("build"), + sessionID: Session.ID.make("ses_hooks"), + agent: Agent.ID.make("build"), messageID: SessionMessage.ID.make("msg_hooks"), call: { type: "tool-call", id: "call-hooks", name: "echo", input: { text: "original" } }, }) diff --git a/packages/core/test/plugin/command.test.ts b/packages/core/test/plugin/command.test.ts index 65848dbd722e..f5dc96a8ceae 100644 --- a/packages/core/test/plugin/command.test.ts +++ b/packages/core/test/plugin/command.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" -import { CommandV2 } from "@opencode-ai/core/command" +import { Command } from "@opencode-ai/core/command" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Location } from "@opencode-ai/core/location" import { CommandPlugin } from "@opencode-ai/core/plugin/command" @@ -15,12 +15,12 @@ const locationLayer = Layer.succeed( Location.Service, Location.Service.of(location({ directory }, { projectDirectory: project })), ) -const it = testEffect(AppNodeBuilder.build(CommandV2.node, [[Location.node, locationLayer]])) +const it = testEffect(AppNodeBuilder.build(Command.node, [[Location.node, locationLayer]])) describe("CommandPlugin.Plugin", () => { it.effect("registers built-in init and review commands", () => Effect.gen(function* () { - const command = yield* CommandV2.Service + const command = yield* Command.Service yield* CommandPlugin.Plugin.effect( host({ command: { diff --git a/packages/core/test/plugin/fixture.ts b/packages/core/test/plugin/fixture.ts index fc56071558d6..82ba8419d6f1 100644 --- a/packages/core/test/plugin/fixture.ts +++ b/packages/core/test/plugin/fixture.ts @@ -1,26 +1,25 @@ -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { AISDK } from "@opencode-ai/core/aisdk" import { Catalog } from "@opencode-ai/core/catalog" -import { CommandV2 } from "@opencode-ai/core/command" +import { Command } from "@opencode-ai/core/command" import { Config } from "@opencode-ai/core/config" import { Credential } from "@opencode-ai/core/credential" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNodePlatform } from "@opencode-ai/util/effect/app-node-platform" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { FileSystem } from "@opencode-ai/core/filesystem" import { FSUtil } from "@opencode-ai/util/fs-util" import { Form } from "@opencode-ai/core/form" import { Integration } from "@opencode-ai/core/integration" import { Location } from "@opencode-ai/core/location" import { Npm } from "@opencode-ai/util/npm" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHooks } from "@opencode-ai/core/plugin/hooks" import { PluginRuntime } from "@opencode-ai/core/plugin/runtime" import { Reference } from "@opencode-ai/core/reference" -import { SkillV2 } from "@opencode-ai/core/skill" -import { ToolHooks } from "@opencode-ai/core/tool/hooks" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" +import { Skill } from "@opencode-ai/core/skill" +import { Tool } from "@opencode-ai/core/tool" import { WebSearch } from "@opencode-ai/core/websearch" import { Effect, Layer } from "effect" import { tempLocationLayer } from "../fixture/location" @@ -41,21 +40,21 @@ export const PluginTestLayer = AppNodeBuilder.build( Location.node, Npm.node, Credential.node, - EventV2.node, + Bus.node, Form.node, LayerNodePlatform.httpClient, - PluginV2.node, - AgentV2.node, + Plugin.node, + Agent.node, AISDK.node, Catalog.node, - CommandV2.node, + Command.node, Integration.node, PluginRuntime.node, PluginHooks.node, Reference.node, - SkillV2.node, - ToolHooks.node, - ToolRegistry.toolsNode, + Skill.node, + PluginHooks.node, + Tool.node, WebSearch.node, ]), [ diff --git a/packages/core/test/plugin/fixtures/config-effect-plugin.ts b/packages/core/test/plugin/fixtures/config-effect-plugin.ts index e607b658e35d..37546379b1b6 100644 --- a/packages/core/test/plugin/fixtures/config-effect-plugin.ts +++ b/packages/core/test/plugin/fixtures/config-effect-plugin.ts @@ -1,4 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin/v2/effect" +import { Plugin } from "@opencode-ai/plugin/effect" import { Effect } from "effect" export default Plugin.define({ diff --git a/packages/core/test/plugin/fixtures/config-promise-plugin.ts b/packages/core/test/plugin/fixtures/config-promise-plugin.ts index 0fef8fbc655d..91f4a1b176e1 100644 --- a/packages/core/test/plugin/fixtures/config-promise-plugin.ts +++ b/packages/core/test/plugin/fixtures/config-promise-plugin.ts @@ -1,4 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin/v2" +import { Plugin } from "@opencode-ai/plugin" export default Plugin.define({ id: "config-promise-plugin", diff --git a/packages/core/test/plugin/fixtures/failing-plugin.ts b/packages/core/test/plugin/fixtures/failing-plugin.ts index dac49b410d74..f6db2fb411dc 100644 --- a/packages/core/test/plugin/fixtures/failing-plugin.ts +++ b/packages/core/test/plugin/fixtures/failing-plugin.ts @@ -1,4 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin/v2/effect" +import { Plugin } from "@opencode-ai/plugin/effect" import { Effect } from "effect" export default Plugin.define({ diff --git a/packages/core/test/plugin/fixtures/variant-source-plugin.ts b/packages/core/test/plugin/fixtures/variant-source-plugin.ts index 5dae699f6e5b..08045a29000d 100644 --- a/packages/core/test/plugin/fixtures/variant-source-plugin.ts +++ b/packages/core/test/plugin/fixtures/variant-source-plugin.ts @@ -1,5 +1,6 @@ -import { Plugin } from "@opencode-ai/plugin/v2/effect" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Plugin } from "@opencode-ai/plugin/effect" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { Effect } from "effect" export default Plugin.define({ @@ -8,14 +9,14 @@ export default Plugin.define({ ctx.catalog .transform((catalog) => { catalog.provider.update("configured", (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") }) catalog.model.update("configured", "glm-5.2", (model) => { - model.modelID = "glm-5.2" - model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + model.modelID = Model.ID.make("glm-5.2") + model.package = Provider.aisdk("@ai-sdk/openai-compatible") model.variants = [ { - id: "high", + id: Model.VariantID.make("high"), settings: {}, headers: { custom: "true" }, body: {}, diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index b7218ae690b0..4087a38f9cd4 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -1,21 +1,15 @@ -import { Plugin } from "@opencode-ai/plugin/v2/effect" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Plugin } from "@opencode-ai/plugin/effect" +import type { IntegrationMethodRegistration } from "@opencode-ai/plugin/effect/integration" +import { Agent } from "@opencode-ai/core/agent" import { Catalog } from "@opencode-ai/core/catalog" import { Credential } from "@opencode-ai/core/credential" import { Integration } from "@opencode-ai/core/integration" import { Location } from "@opencode-ai/core/location" -import { ModelV2 } from "@opencode-ai/core/model" +import { Model } from "@opencode-ai/core/model" import { Project } from "@opencode-ai/core/project" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" import { WebSearch } from "@opencode-ai/core/websearch" -import type { - CredentialOAuth, - IntegrationCommandMethod, - IntegrationEnvMethod, - IntegrationKeyMethod, - IntegrationOAuthMethod, -} from "@opencode-ai/sdk/v2/types" import { Effect, Stream } from "effect" type Overrides = Partial> & { @@ -41,7 +35,6 @@ export function host(overrides: Overrides = {}): Plugin.Context { get: () => Effect.die("unused catalog.provider.get"), }, model: { - get: () => Effect.die("unused catalog.model.get"), list: () => Effect.die("unused catalog.model.list"), default: () => Effect.die("unused catalog.model.default"), }, @@ -116,9 +109,22 @@ export function host(overrides: Overrides = {}): Plugin.Context { } } -export function agentHost(agent: AgentV2.Interface): Plugin.Context["agent"] { +export function agentHost(agent: Agent.Interface): Plugin.Context["agent"] { return { - get: (id) => agent.get(AgentV2.ID.make(id)).pipe(Effect.map((value) => value && agentInfo(value))), + get: (input) => + agent.get(input.agentID).pipe( + Effect.flatMap((value) => + value + ? Effect.succeed({ + location: new Location.Info({ + directory: AbsolutePath.make("/"), + project: { id: Project.ID.make("test"), directory: AbsolutePath.make("/") }, + }), + data: agentInfo(value), + }) + : Effect.fail(new Error(`Agent not found: ${input.agentID}`)), + ), + ), list: () => Effect.die("unused agent.list"), reload: agent.reload, transform: (callback) => @@ -126,17 +132,17 @@ export function agentHost(agent: AgentV2.Interface): Plugin.Context["agent"] { callback({ list: () => draft.list().map(agentInfo), get: (id) => { - const value = draft.get(AgentV2.ID.make(id)) + const value = draft.get(Agent.ID.make(id)) return value && agentInfo(value) }, - default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)), + default: (id) => draft.default(id === undefined ? undefined : Agent.ID.make(id)), update: (id, update) => - draft.update(AgentV2.ID.make(id), (value) => { + draft.update(Agent.ID.make(id), (value) => { const current = agentInfo(value) update(current) - Object.assign(value, current, { id: AgentV2.ID.make(current.id) }) + Object.assign(value, current, { id: Agent.ID.make(current.id) }) }), - remove: (id) => draft.remove(AgentV2.ID.make(id)), + remove: (id) => draft.remove(Agent.ID.make(id)), }), ), } @@ -149,10 +155,6 @@ export function catalogHost(catalog: Catalog.Interface): Plugin.Context["catalog get: () => Effect.die("unused catalog.provider.get"), }, model: { - get: (providerID, modelID) => - catalog.model - .get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)) - .pipe(Effect.map((value) => value && modelInfo(value))), list: () => Effect.die("unused catalog.model.list"), default: () => Effect.die("unused catalog.model.default"), }, @@ -167,7 +169,7 @@ export function catalogHost(catalog: Catalog.Interface): Plugin.Context["catalog models: new Map(Array.from(value.models, ([id, model]) => [id, modelInfo(model)])), })), get: (id) => { - const value = draft.provider.get(ProviderV2.ID.make(id)) + const value = draft.provider.get(Provider.ID.make(id)) return ( value && { provider: providerInfo(value.provider), @@ -176,41 +178,41 @@ export function catalogHost(catalog: Catalog.Interface): Plugin.Context["catalog ) }, update: (id, update) => - draft.provider.update(ProviderV2.ID.make(id), (value) => { + draft.provider.update(Provider.ID.make(id), (value) => { const current = providerInfo(value) update(current) - Object.assign(value, current, { id: ProviderV2.ID.make(current.id) }) + Object.assign(value, current, { id: Provider.ID.make(current.id) }) }), - remove: (id) => draft.provider.remove(ProviderV2.ID.make(id)), + remove: (id) => draft.provider.remove(Provider.ID.make(id)), }, model: { get: (providerID, modelID) => { - const value = draft.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)) + const value = draft.model.get(Provider.ID.make(providerID), Model.ID.make(modelID)) return value && modelInfo(value) }, update: (providerID, modelID, update) => - draft.model.update(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID), (value) => { + draft.model.update(Provider.ID.make(providerID), Model.ID.make(modelID), (value) => { const current = modelInfo(value) update(current) Object.assign(value, current, { - id: ModelV2.ID.make(current.id), - providerID: ProviderV2.ID.make(current.providerID), - family: current.family === undefined ? undefined : ModelV2.Family.make(current.family), + id: Model.ID.make(current.id), + providerID: Provider.ID.make(current.providerID), + family: current.family === undefined ? undefined : Model.Family.make(current.family), variants: current.variants?.map((variant) => ({ ...variant, - id: ModelV2.VariantID.make(variant.id), + id: Model.VariantID.make(variant.id), })), }) }), remove: (providerID, modelID) => - draft.model.remove(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)), + draft.model.remove(Provider.ID.make(providerID), Model.ID.make(modelID)), default: { get: () => { const value = draft.model.default.get() return value && { providerID: value.providerID, modelID: value.modelID } }, set: (providerID, modelID) => - draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)), + draft.model.default.set(Provider.ID.make(providerID), Model.ID.make(modelID)), }, }, }), @@ -370,7 +372,7 @@ export function webSearchHost(websearch: WebSearch.Interface): Plugin.Context["w } } -function oauthCredential(value: CredentialOAuth) { +function oauthCredential(value: Credential.OAuth) { return Credential.OAuth.make({ ...value, methodID: Integration.MethodID.make(value.methodID) }) } @@ -390,7 +392,7 @@ function method(value: Integration.Method) { } function internalMethod( - value: IntegrationOAuthMethod | IntegrationCommandMethod | IntegrationKeyMethod | IntegrationEnvMethod, + value: IntegrationMethodRegistration["method"], ): Integration.Method { if (value.type === "env") return value if (value.type === "key") return value @@ -407,7 +409,7 @@ function internalMethod( } } -function agentInfo(value: AgentV2.Info) { +function agentInfo(value: Agent.Info) { return { ...value, model: value.model && { ...value.model }, @@ -420,7 +422,7 @@ function agentInfo(value: AgentV2.Info) { } } -function providerInfo(value: ProviderV2.MutableInfo) { +function providerInfo(value: Provider.MutableInfo) { return { ...value, settings: value.settings && { ...value.settings }, @@ -429,7 +431,7 @@ function providerInfo(value: ProviderV2.MutableInfo) { } } -function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) { +function modelInfo(value: Model.Info | Model.MutableInfo) { return { ...value, settings: value.settings && { ...value.settings }, diff --git a/packages/core/test/plugin/models-dev.test.ts b/packages/core/test/plugin/models-dev.test.ts index b08e6792c276..80e929bf3c7d 100644 --- a/packages/core/test/plugin/models-dev.test.ts +++ b/packages/core/test/plugin/models-dev.test.ts @@ -6,12 +6,12 @@ import { Catalog } from "@opencode-ai/core/catalog" import { Integration } from "@opencode-ai/core/integration" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Location } from "@opencode-ai/core/location" -import { ModelV2 } from "@opencode-ai/core/model" +import { Model } from "@opencode-ai/core/model" import { ModelsDev } from "@opencode-ai/core/models-dev" import { ModelsDevPlugin } from "@opencode-ai/core/plugin/models-dev" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" import { location } from "../fixture/location" import { testEffect } from "../lib/effect" @@ -21,7 +21,7 @@ const locationLayer = Layer.succeed( Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })), ) -const layer = AppNodeBuilder.build(LayerNode.group([Catalog.node, Integration.node, EventV2.node]), [ +const layer = AppNodeBuilder.build(LayerNode.group([Catalog.node, Integration.node, Bus.node]), [ [Location.node, locationLayer], ]) const it = testEffect(layer) @@ -33,8 +33,8 @@ describe("ModelsDevPlugin", () => { Effect.gen(function* () { const integrations = yield* Integration.Service const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.make("acme") - const modelID = ModelV2.ID.make("gpt-5.4") + const providerID = Provider.ID.make("acme") + const modelID = Model.ID.make("gpt-5.4") const models = ModelsDev.Service.of({ get: () => Effect.succeed([ @@ -42,7 +42,7 @@ describe("ModelsDevPlugin", () => { info: { id: providerID, name: "Acme", - package: ProviderV2.aisdk("@ai-sdk/openai-compatible"), + package: Provider.aisdk("@ai-sdk/openai-compatible"), settings: { baseURL: "https://api.acme.test/v1" }, }, environment: [], @@ -52,7 +52,7 @@ describe("ModelsDevPlugin", () => { modelID, providerID, name: "GPT-5.4", - family: ModelV2.Family.make("gpt"), + family: Model.Family.make("gpt"), capabilities: { tools: true, input: [], output: [] }, variants: [], time: { released: Date.parse("2026-01-01") }, @@ -89,12 +89,12 @@ describe("ModelsDevPlugin", () => { limit: { context: 1_050_000, input: 922_000, output: 128_000 }, }, { - id: ModelV2.ID.make("gpt-5.4-fast"), + id: Model.ID.make("gpt-5.4-fast"), modelID, providerID, name: "GPT-5.4 Fast", - family: ModelV2.Family.make("gpt"), - package: ProviderV2.aisdk("@ai-sdk/openai-compatible"), + family: Model.Family.make("gpt"), + package: Provider.aisdk("@ai-sdk/openai-compatible"), settings: { baseURL: "https://api.acme.test/v1" }, headers: { "x-mode": "fast" }, body: { service_tier: "priority" }, @@ -146,8 +146,8 @@ describe("ModelsDevPlugin", () => { }), ).pipe(Effect.provideService(ModelsDev.Service, models)) - const base = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4")) - const fast = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4-fast")) + const base = yield* catalog.model.get(providerID, Model.ID.make("gpt-5.4")) + const fast = yield* catalog.model.get(providerID, Model.ID.make("gpt-5.4-fast")) expect(base?.variants).toEqual([]) expect(base?.body).toBeUndefined() @@ -156,7 +156,7 @@ describe("ModelsDevPlugin", () => { modelID: "gpt-5.4", providerID: "acme", name: "GPT-5.4 Fast", - package: ProviderV2.aisdk("@ai-sdk/openai-compatible"), + package: Provider.aisdk("@ai-sdk/openai-compatible"), settings: { baseURL: "https://api.acme.test/v1" }, headers: { "x-mode": "fast" }, body: { service_tier: "priority" }, @@ -231,13 +231,13 @@ describe("ModelsDevPlugin", () => { }), ) - const model = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning")) + const model = yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-reasoning")) expect(model?.variants?.map((variant) => variant.id)).toEqual([ - ModelV2.VariantID.make("low"), - ModelV2.VariantID.make("high"), + Model.VariantID.make("low"), + Model.VariantID.make("high"), ]) expect(model?.variants).toContainEqual({ - id: ModelV2.VariantID.make("low"), + id: Model.VariantID.make("low"), settings: { reasoningEffort: "low", reasoningSummary: "auto", @@ -245,7 +245,7 @@ describe("ModelsDevPlugin", () => { }, }) expect(model?.variants).toContainEqual({ - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { reasoningEffort: "high", reasoningSummary: "auto", @@ -253,7 +253,7 @@ describe("ModelsDevPlugin", () => { }, }) - const mode = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning-high")) + const mode = yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-reasoning-high")) expect(mode).toMatchObject({ id: "gpt-reasoning-high", name: "GPT Reasoning High", @@ -261,229 +261,229 @@ describe("ModelsDevPlugin", () => { body: { service_tier: "priority" }, }) expect(mode?.variants?.map((variant) => variant.id)).toEqual([ - ModelV2.VariantID.make("low"), - ModelV2.VariantID.make("high"), + Model.VariantID.make("low"), + Model.VariantID.make("high"), ]) - const pro = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning-pro")) + const pro = yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-reasoning-pro")) expect(pro).toMatchObject({ id: "gpt-reasoning-pro", body: { reasoning: { mode: "pro" } }, }) - const budgetModel = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-budget")) + const budgetModel = yield* catalog.model.get(Provider.ID.anthropic, Model.ID.make("claude-budget")) expect(budgetModel?.variants).toContainEqual({ - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { thinking: { type: "enabled", budgetTokens: 16000 } }, }) expect(budgetModel?.variants).toContainEqual({ - id: ModelV2.VariantID.make("max"), + id: Model.VariantID.make("max"), settings: { thinking: { type: "enabled", budgetTokens: 31999 } }, }) - const anthropicEffortModel = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-opus-4.7")) + const anthropicEffortModel = yield* catalog.model.get(Provider.ID.anthropic, Model.ID.make("claude-opus-4.7")) expect(anthropicEffortModel?.variants).toEqual([ - { id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } }, + { id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } }, { - id: ModelV2.VariantID.make("low"), + id: Model.VariantID.make("low"), settings: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" }, }, ]) - const anthropicToggleModel = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-toggle")) + const anthropicToggleModel = yield* catalog.model.get(Provider.ID.anthropic, Model.ID.make("claude-toggle")) expect(anthropicToggleModel?.variants).toEqual([ - { id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } }, + { id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } }, { - id: ModelV2.VariantID.make("thinking"), + id: Model.VariantID.make("thinking"), settings: { thinking: { type: "adaptive", display: "summarized" } }, }, ]) - const opus45 = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-opus-4-5")) + const opus45 = yield* catalog.model.get(Provider.ID.anthropic, Model.ID.make("claude-opus-4-5")) expect(opus45?.variants).toEqual([ - { id: ModelV2.VariantID.make("low"), settings: { effort: "low" } }, - { id: ModelV2.VariantID.make("high"), settings: { effort: "high" } }, + { id: Model.VariantID.make("low"), settings: { effort: "low" } }, + { id: Model.VariantID.make("high"), settings: { effort: "high" } }, ]) - const grok = yield* catalog.model.get(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4.5")) + const grok = yield* catalog.model.get(Provider.ID.make("xai"), Model.ID.make("grok-4.5")) expect(grok?.variants).toEqual( ["low", "medium", "high"].map((id) => ({ - id: ModelV2.VariantID.make(id), + id: Model.VariantID.make(id), settings: { reasoningEffort: id }, })), ) - const minimax = yield* catalog.model.get(ProviderV2.ID.make("opencode-go"), ModelV2.ID.make("minimax-m3")) + const minimax = yield* catalog.model.get(Provider.ID.make("opencode-go"), Model.ID.make("minimax-m3")) expect(minimax?.variants).toEqual([ - { id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } }, + { id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } }, { - id: ModelV2.VariantID.make("thinking"), + id: Model.VariantID.make("thinking"), settings: { thinking: { type: "adaptive", display: "summarized" } }, }, ]) - const toggle = yield* catalog.model.get(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("toggle-only")) + const toggle = yield* catalog.model.get(Provider.ID.make("alibaba"), Model.ID.make("toggle-only")) expect(toggle?.variants).toEqual([ - { id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } }, - { id: ModelV2.VariantID.make("thinking"), settings: { enableThinking: true } }, + { id: Model.VariantID.make("none"), settings: { enableThinking: false } }, + { id: Model.VariantID.make("thinking"), settings: { enableThinking: true } }, ]) - const combined = yield* catalog.model.get(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("toggle-budget")) + const combined = yield* catalog.model.get(Provider.ID.make("alibaba"), Model.ID.make("toggle-budget")) expect(combined?.variants).toEqual([ - { id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } }, + { id: Model.VariantID.make("none"), settings: { enableThinking: false } }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { enableThinking: true, thinkingBudget: 8000 }, }, { - id: ModelV2.VariantID.make("max"), + id: Model.VariantID.make("max"), settings: { enableThinking: true, thinkingBudget: 16000 }, }, ]) - const gateway = yield* catalog.model.get(ProviderV2.ID.make("vercel"), ModelV2.ID.make("alibaba/qwen-toggle")) + const gateway = yield* catalog.model.get(Provider.ID.make("vercel"), Model.ID.make("alibaba/qwen-toggle")) expect(gateway?.variants).toEqual([ - { id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } }, + { id: Model.VariantID.make("none"), settings: { enableThinking: false } }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { enableThinking: true, thinkingBudget: 8000 }, }, { - id: ModelV2.VariantID.make("max"), + id: Model.VariantID.make("max"), settings: { enableThinking: true, thinkingBudget: 16000 }, }, ]) - const gatewayNova = yield* catalog.model.get(ProviderV2.ID.make("vercel"), ModelV2.ID.make("amazon/nova-2-lite")) + const gatewayNova = yield* catalog.model.get(Provider.ID.make("vercel"), Model.ID.make("amazon/nova-2-lite")) expect(gatewayNova?.variants).toEqual([ { - id: ModelV2.VariantID.make("none"), + id: Model.VariantID.make("none"), settings: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } }, }, { - id: ModelV2.VariantID.make("low"), + id: Model.VariantID.make("low"), settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } }, }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } }, }, ]) const gatewayFallback = yield* catalog.model.get( - ProviderV2.ID.make("vercel"), - ModelV2.ID.make("deepseek/deepseek-toggle"), + Provider.ID.make("vercel"), + Model.ID.make("deepseek/deepseek-toggle"), ) expect(gatewayFallback?.variants).toEqual([ { - id: ModelV2.VariantID.make("none"), + id: Model.VariantID.make("none"), settings: { reasoning: { enabled: false } }, }, { - id: ModelV2.VariantID.make("low"), + id: Model.VariantID.make("low"), settings: { reasoningEffort: "low" }, }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { reasoningEffort: "high" }, }, ]) const openrouter = yield* catalog.model.get( - ProviderV2.ID.make("openrouter"), - ModelV2.ID.make("openrouter-toggle"), + Provider.ID.make("openrouter"), + Model.ID.make("openrouter-toggle"), ) expect(openrouter?.variants).toEqual([ - { id: ModelV2.VariantID.make("none"), settings: { reasoning: { enabled: false } } }, - { id: ModelV2.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } }, + { id: Model.VariantID.make("none"), settings: { reasoning: { enabled: false } } }, + { id: Model.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } }, ]) - const google = yield* catalog.model.get(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini-2.5-flash")) + const google = yield* catalog.model.get(Provider.ID.make("google"), Model.ID.make("gemini-2.5-flash")) expect(google?.variants).toEqual([ { - id: ModelV2.VariantID.make("none"), + id: Model.VariantID.make("none"), settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } }, }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } }, }, { - id: ModelV2.VariantID.make("max"), + id: Model.VariantID.make("max"), settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } }, }, ]) const vertex = yield* catalog.model.get( - ProviderV2.ID.make("google-vertex"), - ModelV2.ID.make("gemini-2.5-flash-lite"), + Provider.ID.make("google-vertex"), + Model.ID.make("gemini-2.5-flash-lite"), ) expect(vertex?.variants).toEqual([ { - id: ModelV2.VariantID.make("none"), + id: Model.VariantID.make("none"), settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } }, }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } }, }, { - id: ModelV2.VariantID.make("max"), + id: Model.VariantID.make("max"), settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } }, }, ]) const bedrock = yield* catalog.model.get( - ProviderV2.ID.make("amazon-bedrock"), - ModelV2.ID.make("amazon.nova-2-lite-v1:0"), + Provider.ID.make("amazon-bedrock"), + Model.ID.make("amazon.nova-2-lite-v1:0"), ) expect(bedrock?.variants).toEqual([ { - id: ModelV2.VariantID.make("none"), + id: Model.VariantID.make("none"), settings: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } }, }, { - id: ModelV2.VariantID.make("low"), + id: Model.VariantID.make("low"), settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } }, }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } }, }, ]) - const sapGemini = yield* catalog.model.get(ProviderV2.ID.make("sap-ai-core"), ModelV2.ID.make("gemini-2.5-flash")) + const sapGemini = yield* catalog.model.get(Provider.ID.make("sap-ai-core"), Model.ID.make("gemini-2.5-flash")) expect(sapGemini?.variants).toEqual([ { - id: ModelV2.VariantID.make("none"), + id: Model.VariantID.make("none"), settings: { modelParams: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } }, }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } } }, }, { - id: ModelV2.VariantID.make("max"), + id: Model.VariantID.make("max"), settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } } }, }, ]) - const sapNova = yield* catalog.model.get(ProviderV2.ID.make("sap-ai-core"), ModelV2.ID.make("amazon--nova-lite")) + const sapNova = yield* catalog.model.get(Provider.ID.make("sap-ai-core"), Model.ID.make("amazon--nova-lite")) expect(sapNova?.variants).toEqual([ { - id: ModelV2.VariantID.make("none"), + id: Model.VariantID.make("none"), settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } }, }, }, { - id: ModelV2.VariantID.make("low"), + id: Model.VariantID.make("low"), settings: { modelParams: { additionalModelRequestFields: { output_config: { effort: "low" } } }, }, }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { modelParams: { additionalModelRequestFields: { output_config: { effort: "high" } } }, }, @@ -491,31 +491,31 @@ describe("ModelsDevPlugin", () => { ]) const sapCohere = yield* catalog.model.get( - ProviderV2.ID.make("sap-ai-core"), - ModelV2.ID.make("cohere--command-a-reasoning"), + Provider.ID.make("sap-ai-core"), + Model.ID.make("cohere--command-a-reasoning"), ) expect(sapCohere?.variants).toEqual([ { - id: ModelV2.VariantID.make("none"), + id: Model.VariantID.make("none"), settings: { modelParams: { thinking: { type: "disabled" } } }, }, { - id: ModelV2.VariantID.make("low"), + id: Model.VariantID.make("low"), settings: { modelParams: { reasoning_effort: "low" } }, }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { modelParams: { reasoning_effort: "high" } }, }, ]) const sapAnthropicEffort = yield* catalog.model.get( - ProviderV2.ID.make("sap-ai-core"), - ModelV2.ID.make("anthropic--claude-4.7-opus"), + Provider.ID.make("sap-ai-core"), + Model.ID.make("anthropic--claude-4.7-opus"), ) expect(sapAnthropicEffort?.variants).toEqual([ { - id: ModelV2.VariantID.make("low"), + id: Model.VariantID.make("low"), settings: { modelParams: { additionalModelRequestFields: { @@ -528,12 +528,12 @@ describe("ModelsDevPlugin", () => { ]) const sapAnthropicBudget = yield* catalog.model.get( - ProviderV2.ID.make("sap-ai-core"), - ModelV2.ID.make("anthropic--claude-4-sonnet"), + Provider.ID.make("sap-ai-core"), + Model.ID.make("anthropic--claude-4-sonnet"), ) expect(sapAnthropicBudget?.variants).toEqual([ { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: 8000 } }, @@ -541,7 +541,7 @@ describe("ModelsDevPlugin", () => { }, }, { - id: ModelV2.VariantID.make("max"), + id: Model.VariantID.make("max"), settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: 16000 } }, diff --git a/packages/core/test/plugin/promise.test.ts b/packages/core/test/plugin/promise.test.ts index 97e30386f2be..2730680ccd4c 100644 --- a/packages/core/test/plugin/promise.test.ts +++ b/packages/core/test/plugin/promise.test.ts @@ -1,24 +1,21 @@ import { describe, expect } from "bun:test" import { Message, SystemPart } from "@opencode-ai/ai" import { DateTime, Effect, Schema } from "effect" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHooks } from "@opencode-ai/core/plugin/hooks" import { PluginHost } from "@opencode-ai/core/plugin/host" import { PluginPromise } from "@opencode-ai/core/plugin/promise" import { WebSearch } from "@opencode-ai/core/websearch" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionPending } from "@opencode-ai/core/session/pending" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { Plugin } from "@opencode-ai/plugin/v2" -import { Tool } from "@opencode-ai/plugin/v2/tool" -import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session" -import { Model } from "@opencode-ai/schema/model" -import { Provider } from "@opencode-ai/schema/provider" +import { Tool } from "@opencode-ai/core/tool" +import { Provider } from "@opencode-ai/core/provider" +import { define } from "@opencode-ai/plugin/promise/plugin" +import type { SessionHooks } from "@opencode-ai/plugin/effect/session" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" import { host as testHost } from "./host" @@ -35,7 +32,7 @@ describe("fromPromise", () => { }) yield* PluginPromise.fromPromise( - Plugin.define({ + define({ id: "promise-session-generate", setup: async (ctx) => { expect(await ctx.session.generate({ sessionID: "ses_generate", prompt: "Summarize" })).toEqual({ @@ -67,7 +64,7 @@ describe("fromPromise", () => { SessionPending.Synthetic.make({ admittedSeq: 1, id: SessionMessage.ID.make(input.id), - sessionID: SessionV2.ID.make(input.sessionID), + sessionID: Session.ID.make(input.sessionID), timeCreated: DateTime.makeUnsafe(0), type: "synthetic", data: { @@ -82,7 +79,7 @@ describe("fromPromise", () => { }) yield* PluginPromise.fromPromise( - Plugin.define({ + define({ id: "promise-session-synthetic", setup: async (ctx) => { await ctx.session.synthetic(input) @@ -101,10 +98,10 @@ describe("fromPromise", () => { it.effect("forwards standard client reads", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) const seen: string[] = [] - const promisePlugin = Plugin.define({ + const promisePlugin = define({ id: "promise-client-reads", setup: async (ctx) => { const results = await Promise.all([ @@ -128,31 +125,36 @@ describe("fromPromise", () => { }), ) - it.effect("forwards direct agent and model reads", () => + it.effect("forwards direct agent and model list reads", () => Effect.gen(function* () { - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const catalog = yield* Catalog.Service - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) yield* agents.transform((draft) => - draft.update(AgentV2.ID.make("reviewer"), (agent) => { + draft.update(Agent.ID.make("reviewer"), (agent) => { agent.description = "Reviews code" }), ) yield* catalog.transform((draft) => - draft.model.update(ProviderV2.ID.make("test"), ModelV2.ID.make("alias"), (model) => { - model.modelID = ModelV2.ID.make("gpt-5") + draft.model.update(Provider.ID.make("test"), Model.ID.make("alias"), (model) => { + model.modelID = Model.ID.make("gpt-5") }), ) yield* PluginPromise.fromPromise( - Plugin.define({ + define({ id: "promise-direct-reads", setup: async (ctx) => { - expect(await ctx.agent.get("reviewer")).toMatchObject({ description: "Reviews code" }) - expect(await ctx.agent.get("missing")).toBeUndefined() - expect(await ctx.catalog.model.get("test", "alias")).toMatchObject({ modelID: "gpt-5" }) - expect(await ctx.catalog.model.get("test", "missing")).toBeUndefined() + expect((await ctx.agent.get({ agentID: Agent.ID.make("reviewer") })).data).toMatchObject({ + description: "Reviews code", + }) + await expect(ctx.agent.get({ agentID: Agent.ID.make("missing") })).rejects.toThrow("Agent not found: missing") + const models = (await ctx.catalog.model.list()).data + expect(models.find((model) => model.providerID === "test" && model.id === "alias")).toMatchObject({ + modelID: "gpt-5", + }) + expect(models.find((model) => model.providerID === "test" && model.id === "missing")).toBeUndefined() }, }), ).effect(host) @@ -161,11 +163,11 @@ describe("fromPromise", () => { it.effect("loads a promise plugin and registers a transform hook", () => Effect.gen(function* () { - const agents = yield* AgentV2.Service - const plugin = yield* PluginV2.Service + const agents = yield* Agent.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) - const promisePlugin = Plugin.define({ + const promisePlugin = define({ id: "promise-example", setup: async (ctx) => { expect(ctx.options.mode).toBe("strict") @@ -181,7 +183,7 @@ describe("fromPromise", () => { const adapted = PluginPromise.fromPromise(promisePlugin) yield* adapted.effect({ ...host, options: { mode: "strict" } }) - expect(yield* agents.get(AgentV2.ID.make("reviewer"))).toMatchObject({ + expect(yield* agents.get(Agent.ID.make("reviewer"))).toMatchObject({ description: "Reviews code", mode: "subagent", }) @@ -190,11 +192,11 @@ describe("fromPromise", () => { it.effect("forwards session context hooks", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const hooks = yield* PluginHooks.Service const host = yield* PluginHost.make(plugin) yield* PluginPromise.fromPromise( - Plugin.define({ + define({ id: "promise-session-context", setup: async (ctx) => { await ctx.session.hook("context", (event) => { @@ -205,8 +207,8 @@ describe("fromPromise", () => { }), ).effect(host) const event: SessionHooks["context"] = { - sessionID: SessionV2.ID.make("ses_promise_session_context"), - agent: AgentV2.ID.make("build"), + sessionID: Session.ID.make("ses_promise_session_context"), + agent: Agent.ID.make("build"), model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") }), system: [SystemPart.make("Initial")], messages: [Message.user("Hello")], @@ -222,11 +224,11 @@ describe("fromPromise", () => { it.effect("disposes a hook registration on request", () => Effect.gen(function* () { - const agents = yield* AgentV2.Service - const plugin = yield* PluginV2.Service + const agents = yield* Agent.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) - const promisePlugin = Plugin.define({ + const promisePlugin = define({ id: "promise-dispose", setup: async (ctx) => { const registration = await ctx.agent.transform((draft) => { @@ -241,16 +243,16 @@ describe("fromPromise", () => { const adapted = PluginPromise.fromPromise(promisePlugin) yield* adapted.effect(host) - expect(yield* agents.get(AgentV2.ID.make("temp"))).toBeUndefined() + expect(yield* agents.get(Agent.ID.make("temp"))).toBeUndefined() }), ) it.effect("registers a standalone web search provider", () => Effect.gen(function* () { const websearch = yield* WebSearch.Service - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) - const promisePlugin = Plugin.define({ + const promisePlugin = define({ id: "promise-websearch", setup: async (ctx) => { await ctx.websearch.transform((draft) => { @@ -279,10 +281,10 @@ describe("fromPromise", () => { it.effect("runs the setup cleanup when the plugin scope closes", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) const events: string[] = [] - const promisePlugin = Plugin.define({ + const promisePlugin = define({ id: "promise-cleanup", setup: async () => { events.push("setup") @@ -306,17 +308,18 @@ describe("fromPromise", () => { it.effect("constructs plain Promise tool definitions in the host", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service - const registry = yield* ToolRegistry.Service + const plugins = yield* Plugin.Service + const registry = yield* Tool.Service const host = yield* PluginHost.make(plugins) - const progress: ToolRegistry.Progress[] = [] - const promisePlugin = Plugin.define({ + const progress: Tool.Metadata[] = [] + const promisePlugin = define({ id: "promise-tool", setup: async (ctx) => { await ctx.tool.transform((tools) => { tools.add( - "hello", - Tool.make({ + { + name: "hello", + options: { codemode: false }, description: "Hello", input: Schema.Struct({ name: Schema.String }), output: Schema.String, @@ -324,8 +327,7 @@ describe("fromPromise", () => { await context.progress({ phase: "greeting" }) return { output: `Hello, ${name}!` } }, - }), - { codemode: false }, + }, ) }) }, @@ -337,8 +339,8 @@ describe("fromPromise", () => { expect(toolSet.definitions).toContainEqual(expect.objectContaining({ name: "hello", description: "Hello" })) expect( yield* toolSet.execute({ - sessionID: SessionV2.ID.make("ses_promise_tool"), - agent: AgentV2.ID.make("build"), + sessionID: Session.ID.make("ses_promise_tool"), + agent: Agent.ID.make("build"), messageID: SessionMessage.ID.make("msg_promise_tool"), progress: (update) => Effect.sync(() => progress.push(update)), call: { type: "tool-call", id: "call_promise_tool", name: "hello", input: { name: "world" } }, diff --git a/packages/core/test/plugin/provider-amazon-bedrock.test.ts b/packages/core/test/plugin/provider-amazon-bedrock.test.ts index 39f46c49a2c4..44d91b036a48 100644 --- a/packages/core/test/plugin/provider-amazon-bedrock.test.ts +++ b/packages/core/test/plugin/provider-amazon-bedrock.test.ts @@ -3,18 +3,18 @@ import { describe, expect } from "bun:test" import type { LanguageModelV3 } from "@ai-sdk/provider" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { AmazonBedrockPlugin } from "@opencode-ai/core/plugin/provider/amazon-bedrock" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* AmazonBedrockPlugin.effect(host) @@ -83,9 +83,9 @@ describe("AmazonBedrockPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const bedrock = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.amazonBedrock), - package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock"), + const bedrock = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.amazonBedrock), + package: Provider.aisdk("@ai-sdk/amazon-bedrock"), settings: { endpoint: "https://bedrock.example" }, }) catalog.provider.update(bedrock.id, (item) => { @@ -94,8 +94,8 @@ describe("AmazonBedrockPlugin", () => { }) }) yield* addPlugin() - const result = required(yield* catalog.provider.get(ProviderV2.ID.amazonBedrock)) - expect(result.package).toBe(ProviderV2.aisdk("@ai-sdk/amazon-bedrock")) + const result = required(yield* catalog.provider.get(Provider.ID.amazonBedrock)) + expect(result.package).toBe(Provider.aisdk("@ai-sdk/amazon-bedrock")) expect(result.settings).toEqual({ baseURL: "https://bedrock.example" }) }), ) @@ -103,14 +103,14 @@ describe("AmazonBedrockPlugin", () => { it.effect("prefers endpoint over baseURL for SDK base URL", () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/amazon-bedrock", options: { @@ -129,14 +129,14 @@ describe("AmazonBedrockPlugin", () => { it.effect("uses baseURL as SDK base URL", () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/amazon-bedrock", options: { @@ -164,14 +164,14 @@ describe("AmazonBedrockPlugin", () => { }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/amazon-bedrock", options: { name: "amazon-bedrock" }, @@ -185,14 +185,14 @@ describe("AmazonBedrockPlugin", () => { it.effect("uses config region over AWS_REGION for SDK base URL", () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "us-east-1" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/amazon-bedrock", options: { name: "amazon-bedrock", region: "eu-west-1" }, @@ -205,14 +205,14 @@ describe("AmazonBedrockPlugin", () => { it.effect("uses AWS_REGION for SDK base URL when config region is absent", () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "eu-west-1" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/amazon-bedrock", options: { name: "amazon-bedrock" }, @@ -225,14 +225,14 @@ describe("AmazonBedrockPlugin", () => { it.effect("defaults SDK region to us-east-1", () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: undefined }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/amazon-bedrock", options: { name: "amazon-bedrock" }, @@ -245,15 +245,15 @@ describe("AmazonBedrockPlugin", () => { it.effect("loads bearer token option into env and uses bearer auth", () => withEnv({ AWS_ACCESS_KEY_ID: undefined, AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const headers: Array = [] yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/amazon-bedrock", options: { @@ -275,15 +275,15 @@ describe("AmazonBedrockPlugin", () => { it.effect("prefers bearer token env over bearer token option", () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: "env-token" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const headers: Array = [] yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/amazon-bedrock", options: { @@ -305,14 +305,14 @@ describe("AmazonBedrockPlugin", () => { it.effect("creates Mantle SDK with GPT-5 OpenAI base path", () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), - modelID: ModelV2.ID.make("openai.gpt-5.5"), - package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("openai.gpt-5.5")), + modelID: Model.ID.make("openai.gpt-5.5"), + package: Provider.aisdk("@ai-sdk/amazon-bedrock/mantle"), }), package: "@ai-sdk/amazon-bedrock/mantle", options: { @@ -332,24 +332,24 @@ describe("AmazonBedrockPlugin", () => { it.effect("selects Mantle APIs without Bedrock cross-region prefixes", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), - modelID: ModelV2.ID.make("openai.gpt-5.5"), - package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("openai.gpt-5.5")), + modelID: Model.ID.make("openai.gpt-5.5"), + package: Provider.aisdk("@ai-sdk/amazon-bedrock/mantle"), }), sdk: fakeSelectorSdk(calls), options: { baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1", region: "us-east-2" }, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")), - modelID: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"), - package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("openai.gpt-oss-safeguard-120b")), + modelID: Model.ID.make("openai.gpt-oss-safeguard-120b"), + package: Provider.aisdk("@ai-sdk/amazon-bedrock/mantle"), }), sdk: fakeSelectorSdk(calls), options: { region: "us-east-1" }, @@ -360,14 +360,14 @@ describe("AmazonBedrockPlugin", () => { it.effect("ignores other Bedrock provider subpaths", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/anthropic"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("@ai-sdk/amazon-bedrock/anthropic"), }), package: "@ai-sdk/amazon-bedrock/anthropic", options: { name: "amazon-bedrock" }, @@ -387,15 +387,15 @@ describe("AmazonBedrockPlugin", () => { }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const headers: Array = [] yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/amazon-bedrock", options: { @@ -419,51 +419,51 @@ describe("AmazonBedrockPlugin", () => { it.effect("applies legacy cross-region inference prefixes", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, options: {}, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, options: { region: "eu-west-1" }, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("global.anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("global.anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, options: { region: "eu-west-1" }, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, options: { region: "ap-northeast-1" }, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, options: { region: "ap-southeast-2" }, @@ -481,15 +481,15 @@ describe("AmazonBedrockPlugin", () => { it.effect("uses AWS_REGION for language prefixes when region option is absent", () => withEnv({ AWS_REGION: "eu-west-1" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, options: {}, @@ -501,7 +501,7 @@ describe("AmazonBedrockPlugin", () => { it.effect("applies the full legacy cross-region prefix matrix", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] const cases = [ @@ -573,10 +573,10 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() for (const item of cases) { yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)), - modelID: ModelV2.ID.make(item.modelID), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make(item.modelID)), + modelID: Model.ID.make(item.modelID), + package: Provider.aisdk("test-provider"), }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, options: { region: item.region }, @@ -588,15 +588,15 @@ describe("AmazonBedrockPlugin", () => { it.effect("ignores non-Bedrock providers for language selection", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.openai, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, options: { region: "eu-west-1" }, diff --git a/packages/core/test/plugin/provider-anthropic.test.ts b/packages/core/test/plugin/provider-anthropic.test.ts index 0c0bcba461e8..de9008b10f1b 100644 --- a/packages/core/test/plugin/provider-anthropic.test.ts +++ b/packages/core/test/plugin/provider-anthropic.test.ts @@ -2,18 +2,18 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { AnthropicPlugin } from "@opencode-ai/core/plugin/provider/anthropic" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* AnthropicPlugin.effect(host) @@ -29,9 +29,9 @@ describe("AnthropicPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const item = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.anthropic), - package: ProviderV2.aisdk("@ai-sdk/anthropic"), + const item = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.anthropic), + package: Provider.aisdk("@ai-sdk/anthropic"), headers: { Existing: "1" }, }) catalog.provider.update(item.id, (draft) => { @@ -40,32 +40,32 @@ describe("AnthropicPlugin", () => { }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).headers?.["anthropic-beta"]).toBe( + expect(required(yield* catalog.provider.get(Provider.ID.anthropic)).headers?.["anthropic-beta"]).toBe( "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14", ) - expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).headers?.Existing).toBe("1") + expect(required(yield* catalog.provider.get(Provider.ID.anthropic)).headers?.Existing).toBe("1") }), ) it.effect("ignores non-Anthropic providers", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.openai, () => {})) + yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.openai, () => {})) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).headers?.["anthropic-beta"]).toBeUndefined() + expect(required(yield* catalog.provider.get(Provider.ID.openai)).headers?.["anthropic-beta"]).toBeUndefined() }), ) it.effect("creates Anthropic SDKs with the model provider ID as the SDK name", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom-anthropic"), ModelV2.ID.make("claude-sonnet-4-5")), - modelID: ModelV2.ID.make("claude-sonnet-4-5"), - package: ProviderV2.aisdk("@ai-sdk/anthropic"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom-anthropic"), Model.ID.make("claude-sonnet-4-5")), + modelID: Model.ID.make("claude-sonnet-4-5"), + package: Provider.aisdk("@ai-sdk/anthropic"), }), package: "@ai-sdk/anthropic", options: { name: "custom-anthropic", apiKey: "test" }, @@ -76,14 +76,14 @@ describe("AnthropicPlugin", () => { it.effect("uses the Anthropic provider ID as the SDK name for the bundled Anthropic provider", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")), - modelID: ModelV2.ID.make("claude-sonnet-4-5"), - package: ProviderV2.aisdk("@ai-sdk/anthropic"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.anthropic, Model.ID.make("claude-sonnet-4-5")), + modelID: Model.ID.make("claude-sonnet-4-5"), + package: Provider.aisdk("@ai-sdk/anthropic"), }), package: "@ai-sdk/anthropic", options: { name: "anthropic", apiKey: "test" }, diff --git a/packages/core/test/plugin/provider-azure-cognitive-services.test.ts b/packages/core/test/plugin/provider-azure-cognitive-services.test.ts index 5b9ca4bf86f9..c53bb4a524a6 100644 --- a/packages/core/test/plugin/provider-azure-cognitive-services.test.ts +++ b/packages/core/test/plugin/provider-azure-cognitive-services.test.ts @@ -3,18 +3,18 @@ import { describe, expect } from "bun:test" import type { LanguageModelV3 } from "@ai-sdk/provider" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { AzureCognitiveServicesPlugin } from "@opencode-ai/core/plugin/provider/azure" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* AzureCognitiveServicesPlugin.effect(host) @@ -65,12 +65,12 @@ describe("AzureCognitiveServicesPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => { - item.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("azure-cognitive-services"), (item) => { + item.package = Provider.aisdk("@ai-sdk/openai-compatible") }) }) yield* addPlugin() - const result = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services"))) + const result = required(yield* catalog.provider.get(Provider.ID.make("azure-cognitive-services"))) expect(result).toMatchObject({ package: "aisdk:@ai-sdk/openai-compatible", settings: { baseURL: "https://cognitive.cognitiveservices.azure.com/openai" }, @@ -85,12 +85,12 @@ describe("AzureCognitiveServicesPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const azure = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services")), + const azure = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.make("azure-cognitive-services")), package: "aisdk:@ai-sdk/openai-compatible", }) - const openai = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.openai), + const openai = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.openai), package: "aisdk:test-provider", }) catalog.provider.update(azure.id, (item) => { @@ -103,8 +103,8 @@ describe("AzureCognitiveServicesPlugin", () => { }) }) yield* addPlugin() - const azure = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services"))) - const openai = required(yield* catalog.provider.get(ProviderV2.ID.openai)) + const azure = required(yield* catalog.provider.get(Provider.ID.make("azure-cognitive-services"))) + const openai = required(yield* catalog.provider.get(Provider.ID.openai)) expect(azure.settings?.baseURL).toBeUndefined() expect(azure).toMatchObject({ package: "aisdk:@ai-sdk/openai-compatible" }) expect(openai.settings?.baseURL).toBeUndefined() @@ -115,14 +115,14 @@ describe("AzureCognitiveServicesPlugin", () => { it.effect("selects chat only for completion URLs", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")), - modelID: ModelV2.ID.make("deployment"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("azure-cognitive-services"), Model.ID.make("deployment")), + modelID: Model.ID.make("deployment"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), @@ -134,23 +134,23 @@ describe("AzureCognitiveServicesPlugin", () => { it.effect("uses the legacy Azure selector order and provider guard", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")), - modelID: ModelV2.ID.make("deployment"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("azure-cognitive-services"), Model.ID.make("deployment")), + modelID: Model.ID.make("deployment"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) const ignored = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("deployment")), - modelID: ModelV2.ID.make("deployment"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.openai, Model.ID.make("deployment")), + modelID: Model.ID.make("deployment"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), @@ -163,33 +163,33 @@ describe("AzureCognitiveServicesPlugin", () => { it.effect("falls back from responses to messages, chat, then languageModel", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] const sdk = fakeSelectorSdk(calls) yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("messages-deployment")), - modelID: ModelV2.ID.make("messages-deployment"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("azure-cognitive-services"), Model.ID.make("messages-deployment")), + modelID: Model.ID.make("messages-deployment"), package: "aisdk:test-provider", }), sdk: { messages: sdk.messages, chat: sdk.chat, languageModel: sdk.languageModel }, options: {}, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")), - modelID: ModelV2.ID.make("chat-deployment"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("azure-cognitive-services"), Model.ID.make("chat-deployment")), + modelID: Model.ID.make("chat-deployment"), package: "aisdk:test-provider", }), sdk: { chat: sdk.chat, languageModel: sdk.languageModel }, options: {}, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("language-deployment")), - modelID: ModelV2.ID.make("language-deployment"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("azure-cognitive-services"), Model.ID.make("language-deployment")), + modelID: Model.ID.make("language-deployment"), package: "aisdk:test-provider", }), sdk: { languageModel: sdk.languageModel }, diff --git a/packages/core/test/plugin/provider-azure.test.ts b/packages/core/test/plugin/provider-azure.test.ts index 227e3bfa7834..85f405e6cbe6 100644 --- a/packages/core/test/plugin/provider-azure.test.ts +++ b/packages/core/test/plugin/provider-azure.test.ts @@ -3,18 +3,18 @@ import { describe, expect } from "bun:test" import type { LanguageModelV3 } from "@ai-sdk/provider" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { AzurePlugin } from "@opencode-ai/core/plugin/provider/azure" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* AzurePlugin.effect(host) @@ -65,12 +65,12 @@ describe("AzurePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.azure, (item) => { - item.package = ProviderV2.aisdk("@ai-sdk/azure") + catalog.provider.update(Provider.ID.azure, (item) => { + item.package = Provider.aisdk("@ai-sdk/azure") }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env") + expect(required(yield* catalog.provider.get(Provider.ID.azure)).settings?.resourceName).toBe("from-env") }), ), ) @@ -80,20 +80,20 @@ describe("AzurePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const azure = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.azure), - package: ProviderV2.aisdk("@ai-sdk/azure"), + const azure = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.azure), + package: Provider.aisdk("@ai-sdk/azure"), settings: { resourceName: "from-config" }, }) catalog.provider.update(azure.id, (item) => { item.package = azure.package item.settings = { resourceName: "from-config" } }) - catalog.provider.update(ProviderV2.ID.openai, () => {}) + catalog.provider.update(Provider.ID.openai, () => {}) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-config") - expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).settings?.resourceName).toBeUndefined() + expect(required(yield* catalog.provider.get(Provider.ID.azure)).settings?.resourceName).toBe("from-config") + expect(required(yield* catalog.provider.get(Provider.ID.openai)).settings?.resourceName).toBeUndefined() }), ), ) @@ -103,9 +103,9 @@ describe("AzurePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const azure = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.azure), - package: ProviderV2.aisdk("@ai-sdk/azure"), + const azure = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.azure), + package: Provider.aisdk("@ai-sdk/azure"), settings: { resourceName: "" }, }) catalog.provider.update(azure.id, (item) => { @@ -114,7 +114,7 @@ describe("AzurePlugin", () => { }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env") + expect(required(yield* catalog.provider.get(Provider.ID.azure)).settings?.resourceName).toBe("from-env") }), ), ) @@ -124,9 +124,9 @@ describe("AzurePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const azure = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.azure), - package: ProviderV2.aisdk("@ai-sdk/azure"), + const azure = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.azure), + package: Provider.aisdk("@ai-sdk/azure"), settings: { resourceName: " " }, }) catalog.provider.update(azure.id, (item) => { @@ -135,7 +135,7 @@ describe("AzurePlugin", () => { }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env") + expect(required(yield* catalog.provider.get(Provider.ID.azure)).settings?.resourceName).toBe("from-env") }), ), ) @@ -143,14 +143,14 @@ describe("AzurePlugin", () => { it.effect("allows configured baseURL without resourceName", () => withEnv({ AZURE_RESOURCE_NAME: undefined }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), - modelID: ModelV2.ID.make("deployment"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.azure, Model.ID.make("deployment")), + modelID: Model.ID.make("deployment"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/azure", options: { name: "azure", baseURL: "https://proxy.example.com/openai" }, @@ -167,10 +167,10 @@ describe("AzurePlugin", () => { yield* addPlugin() const exit = yield* aisdk .runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), - modelID: ModelV2.ID.make("deployment"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.azure, Model.ID.make("deployment")), + modelID: Model.ID.make("deployment"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/azure", options: { name: "azure" }, @@ -183,15 +183,15 @@ describe("AzurePlugin", () => { it.effect("selects chat only for completion URLs", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), - modelID: ModelV2.ID.make("deployment"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.azure, Model.ID.make("deployment")), + modelID: Model.ID.make("deployment"), + package: Provider.aisdk("test-provider"), }), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true }, @@ -202,15 +202,15 @@ describe("AzurePlugin", () => { it.effect("selects chat from per-call useCompletionUrls", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), - modelID: ModelV2.ID.make("deployment"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.azure, Model.ID.make("deployment")), + modelID: Model.ID.make("deployment"), + package: Provider.aisdk("test-provider"), }), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true }, @@ -221,15 +221,15 @@ describe("AzurePlugin", () => { it.effect("ignores model useCompletionUrls when per-call option is unset", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), - modelID: ModelV2.ID.make("deployment"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.azure, Model.ID.make("deployment")), + modelID: Model.ID.make("deployment"), + package: Provider.aisdk("test-provider"), body: { useCompletionUrls: true }, }), sdk: fakeSelectorSdk(calls), @@ -241,24 +241,24 @@ describe("AzurePlugin", () => { it.effect("uses the legacy Azure selector order and provider guard", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), - modelID: ModelV2.ID.make("deployment"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.azure, Model.ID.make("deployment")), + modelID: Model.ID.make("deployment"), + package: Provider.aisdk("test-provider"), }), sdk: fakeSelectorSdk(calls), options: {}, }) const ignored = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("deployment")), - modelID: ModelV2.ID.make("deployment"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.openai, Model.ID.make("deployment")), + modelID: Model.ID.make("deployment"), + package: Provider.aisdk("test-provider"), }), sdk: fakeSelectorSdk(calls), options: {}, @@ -270,7 +270,7 @@ describe("AzurePlugin", () => { it.effect("falls back through the legacy Azure selector order", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] const make = (method: string) => (id: string) => { @@ -279,19 +279,19 @@ describe("AzurePlugin", () => { } yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")), - modelID: ModelV2.ID.make("messages-deployment"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.azure, Model.ID.make("messages-deployment")), + modelID: Model.ID.make("messages-deployment"), + package: Provider.aisdk("test-provider"), }), sdk: { messages: make("messages"), chat: make("chat"), languageModel: make("languageModel") }, options: {}, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")), - modelID: ModelV2.ID.make("language-deployment"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.azure, Model.ID.make("language-deployment")), + modelID: Model.ID.make("language-deployment"), + package: Provider.aisdk("test-provider"), }), sdk: { languageModel: make("languageModel") }, options: {}, diff --git a/packages/core/test/plugin/provider-cerebras.test.ts b/packages/core/test/plugin/provider-cerebras.test.ts index 6722f48996f5..bf13f452d4dd 100644 --- a/packages/core/test/plugin/provider-cerebras.test.ts +++ b/packages/core/test/plugin/provider-cerebras.test.ts @@ -2,11 +2,11 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect, mock } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { CerebrasPlugin } from "@opencode-ai/core/plugin/provider/cerebras" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" @@ -14,7 +14,7 @@ const cerebrasOptions: Record[] = [] const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* CerebrasPlugin.effect(host) @@ -35,13 +35,13 @@ describe("CerebrasPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => { - item.package = ProviderV2.aisdk("@ai-sdk/cerebras") + catalog.provider.update(Provider.ID.make("cerebras"), (item) => { + item.package = Provider.aisdk("@ai-sdk/cerebras") item.headers = { ...item.headers, Existing: "1" } }) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras")))?.headers).toEqual({ + expect((yield* catalog.provider.get(Provider.ID.make("cerebras")))?.headers).toEqual({ Existing: "1", "X-Cerebras-3rd-Party-Integration": "opencode", }) @@ -51,25 +51,25 @@ describe("CerebrasPlugin", () => { it.effect("ignores non-Cerebras providers", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {})) + yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.make("groq"), () => {})) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("groq")))?.headers).toBeUndefined() + expect((yield* catalog.provider.get(Provider.ID.make("groq")))?.headers).toBeUndefined() }), ) it.effect("creates a bundled Cerebras SDK with the model provider ID as the SDK name", () => Effect.gen(function* () { cerebrasOptions.length = 0 - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default( - ProviderV2.ID.make("custom-cerebras"), - ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), + model: Model.Info.make({ + ...Model.Info.default( + Provider.ID.make("custom-cerebras"), + Model.ID.make("llama-4-scout-17b-16e-instruct"), ), - modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), + modelID: Model.ID.make("llama-4-scout-17b-16e-instruct"), package: "aisdk:test-provider", }), package: "@ai-sdk/cerebras", @@ -83,16 +83,16 @@ describe("CerebrasPlugin", () => { it.effect("preserves an explicit bundled Cerebras SDK name option", () => Effect.gen(function* () { cerebrasOptions.length = 0 - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default( - ProviderV2.ID.make("custom-cerebras"), - ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), + model: Model.Info.make({ + ...Model.Info.default( + Provider.ID.make("custom-cerebras"), + Model.ID.make("llama-4-scout-17b-16e-instruct"), ), - modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), + modelID: Model.ID.make("llama-4-scout-17b-16e-instruct"), package: "aisdk:test-provider", }), package: "@ai-sdk/cerebras", @@ -105,16 +105,16 @@ describe("CerebrasPlugin", () => { it.effect("ignores non-Cerebras SDK packages", () => Effect.gen(function* () { cerebrasOptions.length = 0 - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default( - ProviderV2.ID.make("custom-cerebras"), - ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), + model: Model.Info.make({ + ...Model.Info.default( + Provider.ID.make("custom-cerebras"), + Model.ID.make("llama-4-scout-17b-16e-instruct"), ), - modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), + modelID: Model.ID.make("llama-4-scout-17b-16e-instruct"), package: "aisdk:test-provider", }), package: "@ai-sdk/groq", diff --git a/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts b/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts index bd9d9e80cd27..2dab4d93baa7 100644 --- a/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts @@ -1,18 +1,18 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect, mock } from "bun:test" import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { CloudflareAIGatewayPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-ai-gateway" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* CloudflareAIGatewayPlugin.effect(host) @@ -112,13 +112,13 @@ describe("CloudflareAIGatewayPlugin", () => { }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-ai-gateway"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), package: "ai-gateway-provider", @@ -133,14 +133,14 @@ describe("CloudflareAIGatewayPlugin", () => { withEnv(cloudflareEnv(), () => Effect.gen(function* () { resetCalls() - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-ai-gateway"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), package: "ai-gateway-provider", @@ -178,14 +178,14 @@ describe("CloudflareAIGatewayPlugin", () => { withEnv(cloudflareEnv(), () => Effect.gen(function* () { resetCalls() - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-ai-gateway"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), package: "ai-gateway-provider", @@ -208,14 +208,14 @@ describe("CloudflareAIGatewayPlugin", () => { withEnv(cloudflareEnv(), () => Effect.gen(function* () { resetCalls() - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-ai-gateway"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), package: "ai-gateway-provider", @@ -246,14 +246,14 @@ describe("CloudflareAIGatewayPlugin", () => { () => Effect.gen(function* () { resetCalls() - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-ai-gateway"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), package: "ai-gateway-provider", @@ -278,14 +278,14 @@ describe("CloudflareAIGatewayPlugin", () => { withEnv(cloudflareEnv({ CLOUDFLARE_API_TOKEN: undefined, CF_AIG_TOKEN: "cf-aig-token" }), () => Effect.gen(function* () { resetCalls() - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-ai-gateway"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), package: "ai-gateway-provider", @@ -301,14 +301,14 @@ describe("CloudflareAIGatewayPlugin", () => { withEnv(cloudflareEnv({ CLOUDFLARE_ACCOUNT_ID: undefined, CLOUDFLARE_GATEWAY_ID: undefined }), () => Effect.gen(function* () { resetCalls() - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-ai-gateway"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), package: "ai-gateway-provider", @@ -325,14 +325,14 @@ describe("CloudflareAIGatewayPlugin", () => { withEnv(cloudflareEnv({ CLOUDFLARE_API_TOKEN: undefined, CF_AIG_TOKEN: undefined }), () => Effect.gen(function* () { resetCalls() - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-ai-gateway"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), package: "ai-gateway-provider", @@ -355,14 +355,14 @@ describe("CloudflareAIGatewayPlugin", () => { () => Effect.gen(function* () { resetCalls() - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-ai-gateway"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), package: "ai-gateway-provider", @@ -379,17 +379,17 @@ describe("CloudflareAIGatewayPlugin", () => { withEnv(cloudflareEnv(), () => Effect.gen(function* () { resetCalls() - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default( - ProviderV2.ID.make("cloudflare-ai-gateway"), - ModelV2.ID.make("anthropic/claude-sonnet-4-5"), + model: Model.Info.make({ + ...Model.Info.default( + Provider.ID.make("cloudflare-ai-gateway"), + Model.ID.make("anthropic/claude-sonnet-4-5"), ), - modelID: ModelV2.ID.make("anthropic/claude-sonnet-4-5"), + modelID: Model.ID.make("anthropic/claude-sonnet-4-5"), package: "aisdk:test-provider", }), package: "ai-gateway-provider", @@ -411,14 +411,14 @@ describe("CloudflareAIGatewayPlugin", () => { withEnv(cloudflareEnv(), () => Effect.gen(function* () { resetCalls() - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-ai-gateway"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", diff --git a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts index 5f4e3bc27609..7179786dc721 100644 --- a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts @@ -2,11 +2,11 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { CloudflareWorkersAIPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-workers-ai" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import type { LanguageModelV3 } from "@ai-sdk/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" @@ -14,7 +14,7 @@ import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* CloudflareWorkersAIPlugin.effect(host) @@ -82,20 +82,20 @@ describe("CloudflareWorkersAIPlugin", () => { it.effect("maps account ID to endpoint URL and creates an OpenAI-compatible SDK", () => withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => { - provider.package = ProviderV2.aisdk("test-provider") + catalog.provider.update(Provider.ID.make("cloudflare-workers-ai"), (provider) => { + provider.package = Provider.aisdk("test-provider") }), ) yield* addPlugin() - const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))) + const provider = required(yield* catalog.provider.get(Provider.ID.make("cloudflare-workers-ai"))) const sdk = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), - modelID: ModelV2.ID.make("@cf/model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("@cf/model")), + modelID: Model.ID.make("@cf/model"), package: provider.package, settings: provider.settings, }), @@ -116,13 +116,13 @@ describe("CloudflareWorkersAIPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => { - provider.package = ProviderV2.aisdk("test-provider") + catalog.provider.update(Provider.ID.make("cloudflare-workers-ai"), (provider) => { + provider.package = Provider.aisdk("test-provider") provider.settings = { ...provider.settings, baseURL: "https://proxy.example/v1" } }), ) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))).toMatchObject({ + expect(required(yield* catalog.provider.get(Provider.ID.make("cloudflare-workers-ai")))).toMatchObject({ package: "aisdk:test-provider", settings: { baseURL: "https://proxy.example/v1" }, }) @@ -133,13 +133,13 @@ describe("CloudflareWorkersAIPlugin", () => { it.effect("allows a configured baseURL without account ID", () => withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined, CLOUDFLARE_API_KEY: "key" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), - modelID: ModelV2.ID.make("@cf/model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("@cf/model")), + modelID: Model.ID.make("@cf/model"), package: "aisdk:@ai-sdk/openai-compatible", settings: { baseURL: "https://proxy.example/v1" }, }), @@ -156,13 +156,13 @@ describe("CloudflareWorkersAIPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => { - provider.package = ProviderV2.aisdk("test-provider") + catalog.provider.update(Provider.ID.make("cloudflare-workers-ai"), (provider) => { + provider.package = Provider.aisdk("test-provider") provider.settings = { ...provider.settings, accountId: "configured-acct" } }), ) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))).toMatchObject({ + expect(required(yield* catalog.provider.get(Provider.ID.make("cloudflare-workers-ai")))).toMatchObject({ package: "aisdk:test-provider", settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1" }, }) @@ -173,13 +173,13 @@ describe("CloudflareWorkersAIPlugin", () => { it.effect("uses env API key over auth or configured API key and keeps the Cloudflare User-Agent", () => withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "env-key" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), - modelID: ModelV2.ID.make("@cf/model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("@cf/model")), + modelID: Model.ID.make("@cf/model"), package: "aisdk:@ai-sdk/openai-compatible", settings: { baseURL: "https://proxy.example/v1" }, }), @@ -202,13 +202,13 @@ describe("CloudflareWorkersAIPlugin", () => { it.effect("expands account ID vars in endpoint URLs", () => withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), - modelID: ModelV2.ID.make("@cf/model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("@cf/model")), + modelID: Model.ID.make("@cf/model"), package: "aisdk:@ai-sdk/openai-compatible", settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1" }, }), @@ -227,14 +227,14 @@ describe("CloudflareWorkersAIPlugin", () => { it.effect("selects languageModel with the API model ID", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("@cf/api-model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("alias")), + modelID: Model.ID.make("@cf/api-model"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), @@ -248,13 +248,13 @@ describe("CloudflareWorkersAIPlugin", () => { it.effect("does not create an SDK for non OpenAI-compatible packages", () => withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), - modelID: ModelV2.ID.make("@cf/model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("@cf/model")), + modelID: Model.ID.make("@cf/model"), package: "aisdk:@ai-sdk/anthropic", settings: { baseURL: "https://proxy.example/v1" }, }), diff --git a/packages/core/test/plugin/provider-dynamic.test.ts b/packages/core/test/plugin/provider-dynamic.test.ts index 341878e6ded4..437c64bbf4db 100644 --- a/packages/core/test/plugin/provider-dynamic.test.ts +++ b/packages/core/test/plugin/provider-dynamic.test.ts @@ -7,11 +7,11 @@ import path from "path" import { fileURLToPath } from "url" import { AISDK } from "@opencode-ai/core/aisdk" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { DynamicProviderPlugin } from "@opencode-ai/core/plugin/provider/dynamic" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" @@ -29,7 +29,7 @@ function npmEntrypoint(entrypoint?: string) { } const addPlugin = Effect.fn(function* (npm?: Npm.Interface) { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) yield* DynamicProviderPlugin.effect(host).pipe(Effect.provideService(Npm.Service, npm ?? (yield* Npm.Service))) }) @@ -52,10 +52,10 @@ describe("DynamicProviderPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")), - modelID: ModelV2.ID.make("test-model"), - package: ProviderV2.aisdk(fixtureProvider), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom"), Model.ID.make("test-model")), + modelID: Model.ID.make("test-model"), + package: Provider.aisdk(fixtureProvider), }), package: fixtureProvider, options: { name: "custom", marker: "dynamic" }, @@ -71,10 +71,10 @@ describe("DynamicProviderPlugin", () => { const sdk = { marker: "existing" } yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")), - modelID: ModelV2.ID.make("test-model"), - package: ProviderV2.aisdk(fixtureProvider), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom"), Model.ID.make("test-model")), + modelID: Model.ID.make("test-model"), + package: Provider.aisdk(fixtureProvider), }), package: fixtureProvider, options: { name: "custom", marker: "dynamic" }, @@ -89,10 +89,10 @@ describe("DynamicProviderPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("test-model")), - modelID: ModelV2.ID.make("test-model"), - package: ProviderV2.aisdk(fixtureProvider), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom-provider"), Model.ID.make("test-model")), + modelID: Model.ID.make("test-model"), + package: Provider.aisdk(fixtureProvider), }), package: fixtureProvider, options: { name: "custom-provider", marker: "dynamic" }, @@ -106,9 +106,9 @@ describe("DynamicProviderPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin(npmEntrypoint(fixtureProviderPath)) const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("npm-provider"), ModelV2.ID.make("test-model")), - modelID: ModelV2.ID.make("test-model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("npm-provider"), Model.ID.make("test-model")), + modelID: Model.ID.make("test-model"), package: "aisdk:fixture-provider", }), package: "fixture-provider", @@ -124,9 +124,9 @@ describe("DynamicProviderPlugin", () => { yield* addPlugin(npmEntrypoint()) const exit = yield* aisdk .language( - ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("missing-entrypoint"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("alias"), + Model.Info.make({ + ...Model.Info.default(Provider.ID.make("missing-entrypoint"), Model.ID.make("alias")), + modelID: Model.ID.make("alias"), package: "aisdk:fixture-provider", }), ) @@ -142,9 +142,9 @@ describe("DynamicProviderPlugin", () => { yield* addPlugin() const exit = yield* aisdk .language( - ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("bad-import"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("alias"), + Model.Info.make({ + ...Model.Info.default(Provider.ID.make("bad-import"), Model.ID.make("alias")), + modelID: Model.ID.make("alias"), package: "aisdk:file:///missing/provider-factory.js", }), ) @@ -156,15 +156,15 @@ describe("DynamicProviderPlugin", () => { itWithAISDK.live("wraps missing provider factory exports as AISDK init errors", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const tmp = yield* tempEntrypoint("export const notAProviderFactory = true\n") yield* addPlugin(npmEntrypoint(tmp.entrypoint)) const exit = yield* aisdk .language( - ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("missing-factory"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("alias"), + Model.Info.make({ + ...Model.Info.default(Provider.ID.make("missing-factory"), Model.ID.make("alias")), + modelID: Model.ID.make("alias"), package: "aisdk:fixture-provider", }), ) @@ -176,14 +176,14 @@ describe("DynamicProviderPlugin", () => { itWithAISDK.effect("uses the model modelID for the default language model", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const language = yield* aisdk.language( - ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("test-model-api"), - package: ProviderV2.aisdk(fixtureProvider), + Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom"), Model.ID.make("alias")), + modelID: Model.ID.make("test-model-api"), + package: Provider.aisdk(fixtureProvider), }), ) expect(language).toMatchObject({ modelID: "test-model-api", options: { name: "custom" } }) diff --git a/packages/core/test/plugin/provider-factory.test.ts b/packages/core/test/plugin/provider-factory.test.ts index d51d4900b493..28856ad6d0b9 100644 --- a/packages/core/test/plugin/provider-factory.test.ts +++ b/packages/core/test/plugin/provider-factory.test.ts @@ -1,8 +1,8 @@ import { expect } from "bun:test" import { Effect } from "effect" import { AISDK } from "@opencode-ai/core/aisdk" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { AlibabaPlugin } from "@opencode-ai/core/plugin/provider/alibaba" import { CoherePlugin } from "@opencode-ai/core/plugin/provider/cohere" @@ -13,11 +13,11 @@ import { MistralPlugin } from "@opencode-ai/core/plugin/provider/mistral" import { PerplexityPlugin } from "@opencode-ai/core/plugin/provider/perplexity" import { TogetherAIPlugin } from "@opencode-ai/core/plugin/provider/togetherai" import { VenicePlugin } from "@opencode-ai/core/plugin/provider/venice" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" -const modelID = ModelV2.ID.make("test-model") +const modelID = Model.ID.make("test-model") const options = { name: "custom-provider", apiKey: "test", baseURL: "https://example.test" } const providers = [ { id: "alibaba", plugin: AlibabaPlugin, package: "@ai-sdk/alibaba", provider: "alibaba.chat" }, @@ -36,14 +36,14 @@ const it = testEffect(PluginTestLayer) providers.forEach((item) => it.effect(`${item.id} loads only its exact package`, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* item.plugin.effect(host) - const model = ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make(item.id), modelID), + const model = Model.Info.make({ + ...Model.Info.default(Provider.ID.make(item.id), modelID), modelID, - package: ProviderV2.aisdk(item.package), + package: Provider.aisdk(item.package), }) const matched = yield* aisdk.runSDK({ model, package: item.package, options }) const ignored = yield* aisdk.runSDK({ model, package: `${item.package}/unsupported`, options }) diff --git a/packages/core/test/plugin/provider-github-copilot.test.ts b/packages/core/test/plugin/provider-github-copilot.test.ts index b39e06ac7bef..0f70aa628bf6 100644 --- a/packages/core/test/plugin/provider-github-copilot.test.ts +++ b/packages/core/test/plugin/provider-github-copilot.test.ts @@ -3,11 +3,11 @@ import { App } from "@opencode-ai/core/app" import { describe, expect, test } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { copilotBaseURL, copilotFetch, GithubCopilotPlugin } from "@opencode-ai/core/plugin/provider/github-copilot" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { Integration } from "@opencode-ai/core/integration" import type { LanguageModelV3 } from "@ai-sdk/provider" import { testEffect } from "../lib/effect" @@ -16,7 +16,7 @@ import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* GithubCopilotPlugin.effect(host) @@ -94,22 +94,22 @@ describe("GithubCopilotPlugin", () => { it.effect("creates the bundled Copilot SDK for the GitHub Copilot package", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const ignored = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), - modelID: ModelV2.ID.make("gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5")), + modelID: Model.ID.make("gpt-5"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", options: { name: "github-copilot" }, }) const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), - modelID: ModelV2.ID.make("gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5")), + modelID: Model.ID.make("gpt-5"), package: "aisdk:test-provider", }), package: "@ai-sdk/github-copilot", @@ -122,14 +122,14 @@ describe("GithubCopilotPlugin", () => { it.effect("selects languageModel when responses and chat are absent", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")), - modelID: ModelV2.ID.make("claude-sonnet-4"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("claude-sonnet-4")), + modelID: Model.ID.make("claude-sonnet-4"), package: "aisdk:test-provider", }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, @@ -141,14 +141,14 @@ describe("GithubCopilotPlugin", () => { it.effect("selects languageModel with the API model ID when responses and chat are absent", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("claude-sonnet-4"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("alias")), + modelID: Model.ID.make("claude-sonnet-4"), package: "aisdk:test-provider", }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, @@ -160,50 +160,50 @@ describe("GithubCopilotPlugin", () => { it.effect("uses responses for gpt-5 models except gpt-5-mini", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), - modelID: ModelV2.ID.make("gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5")), + modelID: Model.ID.make("gpt-5"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")), - modelID: ModelV2.ID.make("gpt-5.1-codex"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5.1-codex")), + modelID: Model.ID.make("gpt-5.1-codex"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")), - modelID: ModelV2.ID.make("gpt-4o"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-4o")), + modelID: Model.ID.make("gpt-4o"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")), - modelID: ModelV2.ID.make("gpt-5-mini"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5-mini")), + modelID: Model.ID.make("gpt-5-mini"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini-2025-08-07")), - modelID: ModelV2.ID.make("gpt-5-mini-2025-08-07"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5-mini-2025-08-07")), + modelID: Model.ID.make("gpt-5-mini-2025-08-07"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), @@ -221,14 +221,14 @@ describe("GithubCopilotPlugin", () => { it.effect("uses advertised Copilot endpoint metadata before model ID fallbacks", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("mai-code-1-flash-picker")), - modelID: ModelV2.ID.make("mai-code-1-flash-picker"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("mai-code-1-flash-picker")), + modelID: Model.ID.make("mai-code-1-flash-picker"), package: "aisdk:test-provider", settings: { endpoint: "responses" }, }), @@ -236,9 +236,9 @@ describe("GithubCopilotPlugin", () => { options: { endpoint: "responses" }, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), - modelID: ModelV2.ID.make("gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5")), + modelID: Model.ID.make("gpt-5"), package: "aisdk:test-provider", settings: { endpoint: "chat" }, }), @@ -251,32 +251,32 @@ describe("GithubCopilotPlugin", () => { it.effect("uses the API model ID when selecting responses or chat", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")), - modelID: ModelV2.ID.make("gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("default")), + modelID: Model.ID.make("gpt-5"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")), - modelID: ModelV2.ID.make("gpt-5-mini"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("small")), + modelID: Model.ID.make("gpt-5-mini"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")), - modelID: ModelV2.ID.make("claude-sonnet-4"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("sonnet")), + modelID: Model.ID.make("claude-sonnet-4"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), @@ -290,12 +290,12 @@ describe("GithubCopilotPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("github-copilot"), () => {}) - catalog.model.update(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {}) + catalog.provider.update(Provider.ID.make("github-copilot"), () => {}) + catalog.model.update(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5-chat-latest"), () => {}) }) yield* addPlugin() expect( - required(yield* catalog.model.get(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest"))) + required(yield* catalog.model.get(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5-chat-latest"))) .enabled, ).toBe(false) }), @@ -305,12 +305,12 @@ describe("GithubCopilotPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("custom-copilot"), () => {}) - catalog.model.update(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {}) + catalog.provider.update(Provider.ID.make("custom-copilot"), () => {}) + catalog.model.update(Provider.ID.make("custom-copilot"), Model.ID.make("gpt-5-chat-latest"), () => {}) }) yield* addPlugin() expect( - required(yield* catalog.model.get(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"))) + required(yield* catalog.model.get(Provider.ID.make("custom-copilot"), Model.ID.make("gpt-5-chat-latest"))) .enabled, ).toBe(true) }), @@ -318,14 +318,14 @@ describe("GithubCopilotPlugin", () => { it.effect("ignores non-Copilot providers", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")), - modelID: ModelV2.ID.make("gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("openai"), Model.ID.make("gpt-5")), + modelID: Model.ID.make("gpt-5"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), diff --git a/packages/core/test/plugin/provider-gitlab.test.ts b/packages/core/test/plugin/provider-gitlab.test.ts index ac12ebfc9d60..a549ff18b227 100644 --- a/packages/core/test/plugin/provider-gitlab.test.ts +++ b/packages/core/test/plugin/provider-gitlab.test.ts @@ -2,11 +2,11 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect, mock } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { GitLabPlugin } from "@opencode-ai/core/plugin/provider/gitlab" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" @@ -14,7 +14,7 @@ const gitlabSDKOptions: Record[] = [] const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* GitLabPlugin.effect(host) @@ -64,13 +64,13 @@ describe("GitLabPlugin", () => { () => Effect.gen(function* () { gitlabSDKOptions.length = 0 - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), - modelID: ModelV2.ID.make("claude"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("gitlab"), Model.ID.make("claude")), + modelID: Model.ID.make("claude"), package: "aisdk:test-provider", }), package: "gitlab-ai-provider", @@ -102,13 +102,13 @@ describe("GitLabPlugin", () => { () => Effect.gen(function* () { gitlabSDKOptions.length = 0 - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), - modelID: ModelV2.ID.make("claude"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("gitlab"), Model.ID.make("claude")), + modelID: Model.ID.make("claude"), package: "aisdk:test-provider", }), package: "gitlab-ai-provider", @@ -128,13 +128,13 @@ describe("GitLabPlugin", () => { () => Effect.gen(function* () { gitlabSDKOptions.length = 0 - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), - modelID: ModelV2.ID.make("claude"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("gitlab"), Model.ID.make("claude")), + modelID: Model.ID.make("claude"), package: "aisdk:test-provider", }), package: "gitlab-ai-provider", @@ -170,13 +170,13 @@ describe("GitLabPlugin", () => { it.effect("ignores non-GitLab SDK packages", () => Effect.gen(function* () { gitlabSDKOptions.length = 0 - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), - modelID: ModelV2.ID.make("claude"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("gitlab"), Model.ID.make("claude")), + modelID: Model.ID.make("claude"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai", @@ -189,14 +189,14 @@ describe("GitLabPlugin", () => { it.effect("uses workflowChat for duo workflow models and preserves selectedModelRef", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: [string, unknown][] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), - modelID: ModelV2.ID.make("duo-workflow-custom"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("gitlab"), Model.ID.make("duo-workflow-custom")), + modelID: Model.ID.make("duo-workflow-custom"), package: "aisdk:test-provider", headers: {}, settings: { workflowRef: "ref", workflowDefinition: "definition" }, @@ -223,14 +223,14 @@ describe("GitLabPlugin", () => { it.effect("uses exact static workflow model ids when the provider recognizes them", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: [string, unknown][] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-exact")), - modelID: ModelV2.ID.make("duo-workflow-exact"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("gitlab"), Model.ID.make("duo-workflow-exact")), + modelID: Model.ID.make("duo-workflow-exact"), package: "aisdk:test-provider", }), sdk: { @@ -251,14 +251,14 @@ describe("GitLabPlugin", () => { it.effect("uses provider feature flags instead of model settings feature flags", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: [string, unknown][] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), - modelID: ModelV2.ID.make("duo-workflow-custom"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("gitlab"), Model.ID.make("duo-workflow-custom")), + modelID: Model.ID.make("duo-workflow-custom"), package: "aisdk:test-provider", headers: {}, settings: { featureFlags: { request_flag: true } }, @@ -278,14 +278,14 @@ describe("GitLabPlugin", () => { it.effect("uses agenticChat with provider aiGatewayHeaders and feature flags for normal models", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: [string, unknown][] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), - modelID: ModelV2.ID.make("claude"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("gitlab"), Model.ID.make("claude")), + modelID: Model.ID.make("claude"), package: "aisdk:test-provider", headers: { h: "v" }, settings: {}, diff --git a/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts b/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts index 3b6726d37303..4762e9b34c9d 100644 --- a/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts +++ b/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts @@ -3,18 +3,18 @@ import type { LanguageModelV3 } from "@ai-sdk/provider" import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* (definition: typeof GoogleVertexAnthropicPlugin | typeof GoogleVertexPlugin) { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* definition.effect(host) @@ -63,15 +63,15 @@ describe("GoogleVertexAnthropicPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/google-vertex/anthropic") + catalog.provider.update(Provider.ID.make("google-vertex-anthropic"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/google-vertex/anthropic") }), ) yield* addPlugin(GoogleVertexAnthropicPlugin) - expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.project).toBe( + expect((yield* catalog.provider.get(Provider.ID.make("google-vertex-anthropic")))?.settings?.project).toBe( "cloud-project", ) - expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.location).toBe( + expect((yield* catalog.provider.get(Provider.ID.make("google-vertex-anthropic")))?.settings?.location).toBe( "cloud-location", ) }), @@ -83,16 +83,16 @@ describe("GoogleVertexAnthropicPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/google-vertex/anthropic") + catalog.provider.update(Provider.ID.make("google-vertex-anthropic"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/google-vertex/anthropic") provider.settings = { ...provider.settings, project: "configured-project", location: "configured-location" } }), ) yield* addPlugin(GoogleVertexAnthropicPlugin) - expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.project).toBe( + expect((yield* catalog.provider.get(Provider.ID.make("google-vertex-anthropic")))?.settings?.project).toBe( "configured-project", ) - expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.location).toBe( + expect((yield* catalog.provider.get(Provider.ID.make("google-vertex-anthropic")))?.settings?.location).toBe( "configured-location", ) }), @@ -111,16 +111,16 @@ describe("GoogleVertexAnthropicPlugin", () => { }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default( - ProviderV2.ID.make("google-vertex-anthropic"), - ModelV2.ID.make("claude-sonnet-4-5"), + model: Model.Info.make({ + ...Model.Info.default( + Provider.ID.make("google-vertex-anthropic"), + Model.ID.make("claude-sonnet-4-5"), ), - modelID: ModelV2.ID.make("claude-sonnet-4-5"), + modelID: Model.ID.make("claude-sonnet-4-5"), package: "aisdk:test-provider", }), package: "@ai-sdk/google-vertex/anthropic", @@ -138,16 +138,16 @@ describe("GoogleVertexAnthropicPlugin", () => { { GOOGLE_CLOUD_PROJECT: "project", GOOGLE_CLOUD_LOCATION: "cloud-location", VERTEX_LOCATION: "vertex-location" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default( - ProviderV2.ID.make("google-vertex-anthropic"), - ModelV2.ID.make("claude-sonnet-4-5"), + model: Model.Info.make({ + ...Model.Info.default( + Provider.ID.make("google-vertex-anthropic"), + Model.ID.make("claude-sonnet-4-5"), ), - modelID: ModelV2.ID.make("claude-sonnet-4-5"), + modelID: Model.ID.make("claude-sonnet-4-5"), package: "aisdk:test-provider", }), package: "@ai-sdk/google-vertex/anthropic", @@ -162,13 +162,13 @@ describe("GoogleVertexAnthropicPlugin", () => { it.effect("creates SDKs for google-vertex Anthropic models with multi-region endpoints", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), - modelID: ModelV2.ID.make("claude-sonnet-4-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make("claude-sonnet-4-5")), + modelID: Model.ID.make("claude-sonnet-4-5"), package: "aisdk:test-provider", }), package: "@ai-sdk/google-vertex/anthropic", @@ -182,13 +182,13 @@ describe("GoogleVertexAnthropicPlugin", () => { it.effect("keeps configured baseURL for google-vertex Anthropic models", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), - modelID: ModelV2.ID.make("claude-sonnet-4-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make("claude-sonnet-4-5")), + modelID: Model.ID.make("claude-sonnet-4-5"), package: "aisdk:test-provider", }), package: "@ai-sdk/google-vertex/anthropic", @@ -198,25 +198,25 @@ describe("GoogleVertexAnthropicPlugin", () => { }), ) - it.effect("selects google-vertex Anthropic language models through V2 plugins", () => + it.effect("selects google-vertex Anthropic language models through plugins", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin(GoogleVertexPlugin) yield* addPlugin(GoogleVertexAnthropicPlugin) const sdkResult = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")), - modelID: ModelV2.ID.make(" claude-sonnet-4-5 "), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make(" claude-sonnet-4-5 ")), + modelID: Model.ID.make(" claude-sonnet-4-5 "), package: "aisdk:test-provider", }), package: "@ai-sdk/google-vertex/anthropic", options: { name: "google-vertex", project: "project", location: "us" }, }) const languageResult = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")), - modelID: ModelV2.ID.make(" claude-sonnet-4-5 "), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make(" claude-sonnet-4-5 ")), + modelID: Model.ID.make(" claude-sonnet-4-5 "), package: "aisdk:test-provider", }), sdk: sdkResult.sdk, @@ -232,14 +232,14 @@ describe("GoogleVertexAnthropicPlugin", () => { it.effect("trims model IDs before selecting language models", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin(GoogleVertexAnthropicPlugin) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make(" claude-sonnet-4-5 ")), - modelID: ModelV2.ID.make(" claude-sonnet-4-5 "), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google-vertex-anthropic"), Model.ID.make(" claude-sonnet-4-5 ")), + modelID: Model.ID.make(" claude-sonnet-4-5 "), package: "aisdk:test-provider", }), sdk: { languageModel: selector(calls) }, @@ -251,14 +251,14 @@ describe("GoogleVertexAnthropicPlugin", () => { it.effect("ignores non Vertex Anthropic providers for language selection", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), - modelID: ModelV2.ID.make("claude-sonnet-4-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make("claude-sonnet-4-5")), + modelID: Model.ID.make("claude-sonnet-4-5"), package: "aisdk:test-provider", }), sdk: { languageModel: selector(calls) }, diff --git a/packages/core/test/plugin/provider-google-vertex.test.ts b/packages/core/test/plugin/provider-google-vertex.test.ts index 15949b49454d..d7b89e010670 100644 --- a/packages/core/test/plugin/provider-google-vertex.test.ts +++ b/packages/core/test/plugin/provider-google-vertex.test.ts @@ -2,11 +2,11 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect, mock } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import type { LanguageModelV3 } from "@ai-sdk/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" @@ -16,7 +16,7 @@ const googleAuthOptions: Record[] = [] const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* GoogleVertexPlugin.effect(host) @@ -91,14 +91,14 @@ describe("GoogleVertexPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.opencode, (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.opencode, (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { ...provider.settings, baseURL: "https://opencode.ai/zen/v1" } }), ) yield* addPlugin() - const provider = required(yield* catalog.provider.get(ProviderV2.ID.opencode)) + const provider = required(yield* catalog.provider.get(Provider.ID.opencode)) expect(provider.settings).toEqual({ baseURL: "https://opencode.ai/zen/v1" }) }), ) @@ -117,8 +117,8 @@ describe("GoogleVertexPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { ...provider.settings, baseURL: @@ -127,7 +127,7 @@ describe("GoogleVertexPlugin", () => { }), ) yield* addPlugin() - const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))) + const provider = required(yield* catalog.provider.get(Provider.ID.make("google-vertex"))) expect(provider.settings?.project).toBe("google-cloud-project") expect(provider.settings?.location).toBe("google-vertex-location") expect(provider).toMatchObject({ @@ -155,12 +155,12 @@ describe("GoogleVertexPlugin", () => { () => Effect.gen(function* () { vertexOptions.length = 0 - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { ...provider.settings, baseURL: @@ -169,11 +169,11 @@ describe("GoogleVertexPlugin", () => { }), ) yield* addPlugin() - const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))) + const provider = required(yield* catalog.provider.get(Provider.ID.make("google-vertex"))) yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), - modelID: ModelV2.ID.make("gemini"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make("gemini")), + modelID: Model.ID.make("gemini"), package: "aisdk:@ai-sdk/google-vertex", }), package: "@ai-sdk/google-vertex", @@ -208,8 +208,8 @@ describe("GoogleVertexPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { ...provider.settings, baseURL: @@ -219,7 +219,7 @@ describe("GoogleVertexPlugin", () => { }), ) yield* addPlugin() - const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))) + const provider = required(yield* catalog.provider.get(Provider.ID.make("google-vertex"))) expect(provider.settings?.project).toBe("config-project") expect(provider.settings?.location).toBe("global") expect(provider).toMatchObject({ @@ -234,8 +234,8 @@ describe("GoogleVertexPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { ...provider.settings, baseURL: @@ -245,7 +245,7 @@ describe("GoogleVertexPlugin", () => { }), ) yield* addPlugin() - const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))) + const provider = required(yield* catalog.provider.get(Provider.ID.make("google-vertex"))) expect(provider).toMatchObject({ package: "aisdk:@ai-sdk/openai-compatible", settings: { baseURL: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu" }, @@ -267,13 +267,13 @@ describe("GoogleVertexPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/google-vertex") + catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/google-vertex") provider.settings = { ...provider.settings, project: "config-project" } }), ) yield* addPlugin() - const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))) + const provider = required(yield* catalog.provider.get(Provider.ID.make("google-vertex"))) expect(provider.settings?.project).toBe("config-project") expect(provider.settings?.location).toBe("us-central1") }), @@ -289,13 +289,13 @@ describe("GoogleVertexPlugin", () => { () => Effect.gen(function* () { vertexOptions.length = 0 - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), - modelID: ModelV2.ID.make("gemini"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make("gemini")), + modelID: Model.ID.make("gemini"), package: "aisdk:@ai-sdk/google-vertex", }), package: "@ai-sdk/google-vertex", @@ -313,7 +313,7 @@ describe("GoogleVertexPlugin", () => { Effect.gen(function* () { googleAuthOptions.length = 0 const fetchCalls: { input: Parameters[0]; init?: RequestInit }[] = [] - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.hook.sdk((evt) => @@ -338,9 +338,9 @@ describe("GoogleVertexPlugin", () => { Effect.void, () => aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), - modelID: ModelV2.ID.make("gemini"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make("gemini")), + modelID: Model.ID.make("gemini"), package: "aisdk:@ai-sdk/openai-compatible", }), package: "@ai-sdk/openai-compatible", @@ -361,14 +361,14 @@ describe("GoogleVertexPlugin", () => { it.effect("trims model IDs before selecting language models", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" gemini-2.5-pro ")), - modelID: ModelV2.ID.make(" gemini-2.5-pro "), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make(" gemini-2.5-pro ")), + modelID: Model.ID.make(" gemini-2.5-pro "), package: "aisdk:test-provider", }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, diff --git a/packages/core/test/plugin/provider-google.test.ts b/packages/core/test/plugin/provider-google.test.ts index 2dea12a2e662..655c525ea029 100644 --- a/packages/core/test/plugin/provider-google.test.ts +++ b/packages/core/test/plugin/provider-google.test.ts @@ -1,18 +1,18 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { GooglePlugin } from "@opencode-ai/core/plugin/provider/google" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* GooglePlugin.effect(host) @@ -21,13 +21,13 @@ const addPlugin = Effect.fn(function* () { describe("GooglePlugin", () => { it.effect("creates a Google Generative AI SDK for @ai-sdk/google using the provider ID as SDK name", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("gemini")), - modelID: ModelV2.ID.make("gemini"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom-google"), Model.ID.make("gemini")), + modelID: Model.ID.make("gemini"), package: "aisdk:@ai-sdk/google", }), package: "@ai-sdk/google", @@ -40,13 +40,13 @@ describe("GooglePlugin", () => { it.effect("ignores non-Google SDK packages", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini")), - modelID: ModelV2.ID.make("gemini"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google"), Model.ID.make("gemini")), + modelID: Model.ID.make("gemini"), package: "aisdk:@ai-sdk/google", }), package: "@ai-sdk/google-vertex", @@ -58,13 +58,13 @@ describe("GooglePlugin", () => { it.effect("uses default languageModel loading with provider ID parity", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const sdkEvent = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("gemini-api"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom-google"), Model.ID.make("alias")), + modelID: Model.ID.make("gemini-api"), package: "aisdk:@ai-sdk/google", }), package: "@ai-sdk/google", @@ -87,9 +87,9 @@ describe("GooglePlugin", () => { yield* addPlugin() const resolved = yield* aisdk.model( - ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("gemini-api"), + Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom-google"), Model.ID.make("alias")), + modelID: Model.ID.make("gemini-api"), package: "aisdk:@ai-sdk/google", settings: { apiKey: "test" }, }), diff --git a/packages/core/test/plugin/provider-kilo.test.ts b/packages/core/test/plugin/provider-kilo.test.ts index e4af57cda6ba..e45cf39a3bd7 100644 --- a/packages/core/test/plugin/provider-kilo.test.ts +++ b/packages/core/test/plugin/provider-kilo.test.ts @@ -1,18 +1,18 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { ProviderPlugins } from "@opencode-ai/core/plugin/provider" import { KiloPlugin } from "@opencode-ai/core/plugin/provider/kilo" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) yield* KiloPlugin.effect(host) }) @@ -26,20 +26,20 @@ describe("KiloPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("kilo"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" } provider.headers = { Existing: "value" } }) - catalog.provider.update(ProviderV2.ID.openrouter, () => {}) + catalog.provider.update(Provider.ID.openrouter, () => {}) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toEqual({ + expect((yield* catalog.provider.get(Provider.ID.make("kilo")))?.headers).toEqual({ Existing: "value", "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", }) - expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toBeUndefined() + expect((yield* catalog.provider.get(Provider.ID.openrouter))?.headers).toBeUndefined() }), ) @@ -47,20 +47,20 @@ describe("KiloPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("kilo"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" } }) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toEqual({ + expect((yield* catalog.provider.get(Provider.ID.make("kilo")))?.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", }) - expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).not.toHaveProperty("http-referer") - expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).not.toHaveProperty("x-title") - expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).not.toHaveProperty("X-Source") + expect((yield* catalog.provider.get(Provider.ID.make("kilo")))?.headers).not.toHaveProperty("http-referer") + expect((yield* catalog.provider.get(Provider.ID.make("kilo")))?.headers).not.toHaveProperty("x-title") + expect((yield* catalog.provider.get(Provider.ID.make("kilo")))?.headers).not.toHaveProperty("X-Source") }), ) @@ -68,18 +68,18 @@ describe("KiloPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => { - provider.package = ProviderV2.aisdk("kilo") + catalog.provider.update(Provider.ID.make("kilo"), (provider) => { + provider.package = Provider.aisdk("kilo") }) - catalog.provider.update(ProviderV2.ID.make("custom-kilo"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("custom-kilo"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" } }) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toBeUndefined() - expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo")))?.headers).toEqual({ + expect((yield* catalog.provider.get(Provider.ID.make("kilo")))?.headers).toBeUndefined() + expect((yield* catalog.provider.get(Provider.ID.make("custom-kilo")))?.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", }) diff --git a/packages/core/test/plugin/provider-llmgateway.test.ts b/packages/core/test/plugin/provider-llmgateway.test.ts index 9ff6d2566e7b..11455d3b628b 100644 --- a/packages/core/test/plugin/provider-llmgateway.test.ts +++ b/packages/core/test/plugin/provider-llmgateway.test.ts @@ -2,18 +2,18 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { Integration } from "@opencode-ai/core/integration" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { ProviderPlugins } from "@opencode-ai/core/plugin/provider" import { LLMGatewayPlugin } from "@opencode-ai/core/plugin/provider/llmgateway" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) const integration = yield* Integration.Service yield* LLMGatewayPlugin.effect(host).pipe(Effect.provideService(Integration.Service, integration)) @@ -33,21 +33,21 @@ describe("LLMGatewayPlugin", () => { editor.update(Integration.ID.make("openrouter"), () => {}) }) yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("llmgateway"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://api.llmgateway.io/v1" } provider.headers = { Existing: "value" } }) - catalog.provider.update(ProviderV2.ID.openrouter, () => {}) + catalog.provider.update(Provider.ID.openrouter, () => {}) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.headers).toEqual({ + expect((yield* catalog.provider.get(Provider.ID.make("llmgateway")))?.headers).toEqual({ Existing: "value", "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", "X-Source": "opencode", }) - expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toBeUndefined() + expect((yield* catalog.provider.get(Provider.ID.openrouter))?.headers).toBeUndefined() }), ) @@ -59,16 +59,16 @@ describe("LLMGatewayPlugin", () => { editor.update(Integration.ID.make("llmgateway"), () => {}) }) yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => { + catalog.provider.update(Provider.ID.make("llmgateway"), (provider) => { provider.disabled = true - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://api.llmgateway.io/v1" } }) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.disabled).toBe(true) - expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.headers).toBeUndefined() + expect((yield* catalog.provider.get(Provider.ID.make("llmgateway")))?.disabled).toBe(true) + expect((yield* catalog.provider.get(Provider.ID.make("llmgateway")))?.headers).toBeUndefined() }), ) }) diff --git a/packages/core/test/plugin/provider-nvidia.test.ts b/packages/core/test/plugin/provider-nvidia.test.ts index 47f78fd0228b..278e4f99c144 100644 --- a/packages/core/test/plugin/provider-nvidia.test.ts +++ b/packages/core/test/plugin/provider-nvidia.test.ts @@ -1,18 +1,18 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { ProviderPlugins } from "@opencode-ai/core/plugin/provider" import { NvidiaPlugin } from "@opencode-ai/core/plugin/provider/nvidia" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) yield* NvidiaPlugin.effect(host) }) @@ -26,21 +26,21 @@ describe("NvidiaPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("nvidia"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" } provider.headers = { Existing: "value" } }) - catalog.provider.update(ProviderV2.ID.openrouter, () => {}) + catalog.provider.update(Provider.ID.openrouter, () => {}) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({ + expect((yield* catalog.provider.get(Provider.ID.make("nvidia")))?.headers).toEqual({ Existing: "value", "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", "X-BILLING-INVOKE-ORIGIN": "OpenCode", }) - expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toBeUndefined() + expect((yield* catalog.provider.get(Provider.ID.openrouter))?.headers).toBeUndefined() }), ) @@ -48,14 +48,14 @@ describe("NvidiaPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("nvidia"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" } }) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({ + expect((yield* catalog.provider.get(Provider.ID.make("nvidia")))?.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", "X-BILLING-INVOKE-ORIGIN": "OpenCode", @@ -67,15 +67,15 @@ describe("NvidiaPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("nvidia"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" } provider.headers = { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" } }) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({ + expect((yield* catalog.provider.get(Provider.ID.make("nvidia")))?.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", "X-BILLING-INVOKE-ORIGIN": "CustomOrigin", diff --git a/packages/core/test/plugin/provider-openai-compatible.test.ts b/packages/core/test/plugin/provider-openai-compatible.test.ts index e1cf1ed6c863..407d7d357c4f 100644 --- a/packages/core/test/plugin/provider-openai-compatible.test.ts +++ b/packages/core/test/plugin/provider-openai-compatible.test.ts @@ -1,18 +1,18 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { OpenAICompatiblePlugin } from "@opencode-ai/core/plugin/provider/openai-compatible" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* OpenAICompatiblePlugin.effect(host) @@ -21,22 +21,22 @@ const addPlugin = Effect.fn(function* () { describe("OpenAICompatiblePlugin", () => { it.effect("preserves explicit includeUsage false and defaults it to true", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const defaulted = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom"), Model.ID.make("model")), + modelID: Model.ID.make("model"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", options: { name: "custom" }, }) const disabled = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom"), Model.ID.make("model")), + modelID: Model.ID.make("model"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", @@ -49,13 +49,13 @@ describe("OpenAICompatiblePlugin", () => { it.effect("defaults includeUsage for OpenAI-compatible package matches", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom"), Model.ID.make("model")), + modelID: Model.ID.make("model"), package: "aisdk:test-provider", }), package: "file:///tmp/@ai-sdk/openai-compatible-provider.js", @@ -67,7 +67,7 @@ describe("OpenAICompatiblePlugin", () => { it.effect("uses the provider ID as the OpenAI-compatible provider name", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const observed: string[] = [] yield* addPlugin() @@ -77,9 +77,9 @@ describe("OpenAICompatiblePlugin", () => { }), ) yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom-provider"), Model.ID.make("model")), + modelID: Model.ID.make("model"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", @@ -98,9 +98,9 @@ describe("OpenAICompatiblePlugin", () => { }) yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("model")), + modelID: Model.ID.make("model"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", diff --git a/packages/core/test/plugin/provider-openai.test.ts b/packages/core/test/plugin/provider-openai.test.ts index 741966eebede..75b6385c021f 100644 --- a/packages/core/test/plugin/provider-openai.test.ts +++ b/packages/core/test/plugin/provider-openai.test.ts @@ -6,18 +6,18 @@ import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { Credential } from "@opencode-ai/core/credential" import { Integration } from "@opencode-ai/core/integration" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) const integrations = yield* Integration.Service @@ -63,14 +63,14 @@ describe("OpenAIPlugin", () => { it.effect("creates an OpenAI SDK for @ai-sdk/openai using the provider ID as SDK name", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")), - modelID: ModelV2.ID.make("gpt-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom-openai"), Model.ID.make("gpt-5")), + modelID: Model.ID.make("gpt-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/openai", options: { name: "custom-openai", apiKey: "test" }, @@ -81,14 +81,14 @@ describe("OpenAIPlugin", () => { it.effect("ignores non-OpenAI SDK packages", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")), - modelID: ModelV2.ID.make("gpt-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.openai, Model.ID.make("gpt-5")), + modelID: Model.ID.make("gpt-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/openai-compatible", options: { name: "openai" }, @@ -99,15 +99,15 @@ describe("OpenAIPlugin", () => { it.effect("uses the Responses API for language models", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("gpt-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.openai, Model.ID.make("alias")), + modelID: Model.ID.make("gpt-5"), + package: Provider.aisdk("test-provider"), }), sdk: fakeSelectorSdk(calls), options: {}, @@ -119,15 +119,15 @@ describe("OpenAIPlugin", () => { it.effect("ignores non-OpenAI providers", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")), - modelID: ModelV2.ID.make("gpt-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.anthropic, Model.ID.make("gpt-5")), + modelID: Model.ID.make("gpt-5"), + package: Provider.aisdk("test-provider"), }), sdk: fakeSelectorSdk(calls), options: {}, @@ -141,20 +141,20 @@ describe("OpenAIPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const item = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.openai), - package: ProviderV2.aisdk("@ai-sdk/openai"), + const item = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.openai), + package: Provider.aisdk("@ai-sdk/openai"), }) catalog.provider.update(item.id, (draft) => { draft.package = item.package }) - catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), () => {}) - catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {}) + catalog.model.update(item.id, Model.ID.make("gpt-5"), () => {}) + catalog.model.update(item.id, Model.ID.make("gpt-5-chat-latest"), () => {}) }) yield* addPlugin() - expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5"))).enabled).toBe(true) + expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5"))).enabled).toBe(true) expect( - required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5-chat-latest"))).enabled, + required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5-chat-latest"))).enabled, ).toBe(false) }), ) @@ -164,14 +164,14 @@ describe("OpenAIPlugin", () => { const catalog = yield* Catalog.Service const credentials = yield* Credential.Service yield* catalog.transform((catalog) => { - const item = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.openai), - package: ProviderV2.aisdk("@ai-sdk/openai"), + const item = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.openai), + package: Provider.aisdk("@ai-sdk/openai"), }) catalog.provider.update(item.id, (draft) => { draft.package = item.package }) - catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5"), (model) => { + catalog.model.update(item.id, Model.ID.make("gpt-5.5"), (model) => { model.cost = [ { input: Money.USDPerMillionTokens.make(1), @@ -183,14 +183,14 @@ describe("OpenAIPlugin", () => { }, ] }) - catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5-pro"), () => {}) - catalog.model.update(item.id, ModelV2.ID.make("gpt-5.4-pro"), (model) => { - model.modelID = ModelV2.ID.make("gpt-5.4") + catalog.model.update(item.id, Model.ID.make("gpt-5.5-pro"), () => {}) + catalog.model.update(item.id, Model.ID.make("gpt-5.4-pro"), (model) => { + model.modelID = Model.ID.make("gpt-5.4") model.body = { reasoning: { mode: "pro" } } }) - catalog.model.update(item.id, ModelV2.ID.make("gpt-5.6"), () => {}) - catalog.model.update(item.id, ModelV2.ID.make("gpt-5.6-sol"), () => {}) - catalog.model.update(item.id, ModelV2.ID.make("gpt-4.1"), () => {}) + catalog.model.update(item.id, Model.ID.make("gpt-5.6"), () => {}) + catalog.model.update(item.id, Model.ID.make("gpt-5.6-sol"), () => {}) + catalog.model.update(item.id, Model.ID.make("gpt-4.1"), () => {}) }) yield* credentials.create({ integrationID: Integration.ID.make("openai"), @@ -205,20 +205,20 @@ describe("OpenAIPlugin", () => { }) yield* addPlugin() - const eligible = required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5.5"))) + const eligible = required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5"))) expect(eligible.cost).toEqual([]) expect(eligible.enabled).toBe(true) - expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5.5-pro"))).enabled).toBe( + expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5-pro"))).enabled).toBe( false, ) - expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5.4-pro"))).enabled).toBe( + expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.4-pro"))).enabled).toBe( false, ) - expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5.6"))).enabled).toBe(false) - expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5.6-sol"))).enabled).toBe( + expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.6"))).enabled).toBe(false) + expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.6-sol"))).enabled).toBe( true, ) - expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-4.1"))).enabled).toBe(false) + expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-4.1"))).enabled).toBe(false) }), ) @@ -227,15 +227,15 @@ describe("OpenAIPlugin", () => { const catalog = yield* Catalog.Service const credentials = yield* Credential.Service yield* catalog.transform((catalog) => { - const item = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.openai), - package: ProviderV2.aisdk("@ai-sdk/openai"), + const item = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.openai), + package: Provider.aisdk("@ai-sdk/openai"), }) catalog.provider.update(item.id, (draft) => { draft.package = item.package }) - catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5"), () => {}) - catalog.model.update(item.id, ModelV2.ID.make("gpt-4.1"), () => {}) + catalog.model.update(item.id, Model.ID.make("gpt-5.5"), () => {}) + catalog.model.update(item.id, Model.ID.make("gpt-4.1"), () => {}) }) yield* credentials.create({ integrationID: Integration.ID.make("openai"), @@ -243,8 +243,8 @@ describe("OpenAIPlugin", () => { }) yield* addPlugin() - expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5.5"))).enabled).toBe(true) - expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-4.1"))).enabled).toBe(true) + expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5"))).enabled).toBe(true) + expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-4.1"))).enabled).toBe(true) }), ) @@ -252,18 +252,18 @@ describe("OpenAIPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const item = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.make("custom-openai")), - package: ProviderV2.aisdk("test-provider"), + const item = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.make("custom-openai")), + package: Provider.aisdk("test-provider"), }) catalog.provider.update(item.id, (draft) => { draft.package = item.package }) - catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {}) + catalog.model.update(item.id, Model.ID.make("gpt-5-chat-latest"), () => {}) }) yield* addPlugin() expect( - required(yield* catalog.model.get(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5-chat-latest"))) + required(yield* catalog.model.get(Provider.ID.make("custom-openai"), Model.ID.make("gpt-5-chat-latest"))) .enabled, ).toBe(true) }), diff --git a/packages/core/test/plugin/provider-opencode.test.ts b/packages/core/test/plugin/provider-opencode.test.ts index f0b9fff8d7ae..e8f8323dbf9a 100644 --- a/packages/core/test/plugin/provider-opencode.test.ts +++ b/packages/core/test/plugin/provider-opencode.test.ts @@ -3,25 +3,25 @@ import { Money } from "@opencode-ai/schema/money" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { Credential } from "@opencode-ai/core/credential" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Integration } from "@opencode-ai/core/integration" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { OpencodePlugin } from "@opencode-ai/core/plugin/provider/opencode" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) - const events = yield* EventV2.Service + const bus = yield* Bus.Service const integration = yield* Integration.Service yield* OpencodePlugin.effect(host).pipe( - Effect.provideService(EventV2.Service, events), + Effect.provideService(Bus.Service, bus), Effect.provideService(Integration.Service, integration), ) }) @@ -215,18 +215,18 @@ describe("OpencodePlugin", () => { const credentials = yield* Credential.Service const catalog = yield* Catalog.Service yield* catalog.transform((draft) => { - draft.provider.update(ProviderV2.ID.make("remote"), () => {}) - draft.model.update(ProviderV2.ID.make("remote"), ModelV2.ID.make("model"), (model) => { + draft.provider.update(Provider.ID.make("remote"), () => {}) + draft.model.update(Provider.ID.make("remote"), Model.ID.make("model"), (model) => { model.variants = [ { - id: ModelV2.VariantID.make("custom"), + id: Model.VariantID.make("custom"), settings: {}, headers: { "x-custom": "true" }, body: { custom: true }, }, ] }) - draft.model.update(ProviderV2.ID.make("remote"), ModelV2.ID.make("stale"), () => {}) + draft.model.update(Provider.ID.make("remote"), Model.ID.make("stale"), () => {}) }) yield* credentials.create({ integrationID: Integration.ID.make("opencode"), @@ -240,44 +240,44 @@ describe("OpencodePlugin", () => { yield* addPlugin() expect(authorization).toEqual(["Bearer secret"]) - const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("remote"))) + const provider = required(yield* catalog.provider.get(Provider.ID.make("remote"))) expect(provider).toMatchObject({ name: "Remote", integrationID: "opencode", - package: ProviderV2.aisdk("@ai-sdk/openai-compatible"), + package: Provider.aisdk("@ai-sdk/openai-compatible"), settings: { baseURL: `${server.url.origin}/v1`, custom: "value" }, headers: { "x-org-id": "org" }, }) expect(yield* (yield* Integration.Service).get(Integration.ID.make("remote"))).toBeUndefined() - const model = required(yield* catalog.model.get(ProviderV2.ID.make("remote"), ModelV2.ID.make("model"))) + const model = required(yield* catalog.model.get(Provider.ID.make("remote"), Model.ID.make("model"))) expect(model).toMatchObject({ name: "Remote Model", family: "remote", capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }], limit: { context: 1000, output: 100 }, - package: ProviderV2.aisdk("@ai-sdk/openai-compatible"), + package: Provider.aisdk("@ai-sdk/openai-compatible"), settings: { baseURL: `${server.url.origin}/v1`, custom: "value", temperature: 0.5 }, headers: { "x-org-id": "org" }, }) expect(model.variants).toEqual([ { - id: ModelV2.VariantID.make("custom"), + id: Model.VariantID.make("custom"), settings: {}, headers: { "x-custom": "true" }, body: { custom: true }, }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { temperature: 0.2 }, headers: {}, }, ]) expect( - required(yield* catalog.model.get(ProviderV2.ID.make("remote"), ModelV2.ID.make("disabled"))).enabled, + required(yield* catalog.model.get(Provider.ID.make("remote"), Model.ID.make("disabled"))).enabled, ).toBe(false) - expect(yield* catalog.model.get(ProviderV2.ID.make("remote"), ModelV2.ID.make("stale"))).toBeDefined() + expect(yield* catalog.model.get(Provider.ID.make("remote"), Model.ID.make("stale"))).toBeDefined() }), ({ server }) => Effect.promise(() => server.stop(true)), ), @@ -288,14 +288,14 @@ describe("OpencodePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const provider = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.opencode), - package: ProviderV2.aisdk("test-provider"), + const provider = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.opencode), + package: Provider.aisdk("test-provider"), }) - const model = ModelV2.Info.make({ - ...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")), - modelID: ModelV2.ID.make("paid"), - package: ProviderV2.aisdk("test-provider"), + const model = Model.Info.make({ + ...Model.Info.default(provider.id, Model.ID.make("paid")), + modelID: Model.ID.make("paid"), + package: Provider.aisdk("test-provider"), cost: cost(1), }) catalog.provider.update(provider.id, () => {}) @@ -304,8 +304,8 @@ describe("OpencodePlugin", () => { }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public") - expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(false) + expect(required(yield* catalog.provider.get(Provider.ID.opencode)).settings?.apiKey).toBe("public") + expect(required(yield* catalog.model.get(Provider.ID.opencode, Model.ID.make("paid"))).enabled).toBe(false) }), ), ) @@ -315,14 +315,14 @@ describe("OpencodePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const provider = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.opencode), - package: ProviderV2.aisdk("test-provider"), + const provider = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.opencode), + package: Provider.aisdk("test-provider"), }) - const model = ModelV2.Info.make({ - ...ModelV2.Info.default(provider.id, ModelV2.ID.make("free")), - modelID: ModelV2.ID.make("free"), - package: ProviderV2.aisdk("test-provider"), + const model = Model.Info.make({ + ...Model.Info.default(provider.id, Model.ID.make("free")), + modelID: Model.ID.make("free"), + package: Provider.aisdk("test-provider"), cost: cost(0), }) catalog.provider.update(provider.id, () => {}) @@ -331,8 +331,8 @@ describe("OpencodePlugin", () => { }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public") - expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("free"))).enabled).toBe(true) + expect(required(yield* catalog.provider.get(Provider.ID.opencode)).settings?.apiKey).toBe("public") + expect(required(yield* catalog.model.get(Provider.ID.opencode, Model.ID.make("free"))).enabled).toBe(true) }), ), ) @@ -342,14 +342,14 @@ describe("OpencodePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const provider = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.opencode), - package: ProviderV2.aisdk("test-provider"), + const provider = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.opencode), + package: Provider.aisdk("test-provider"), }) - const model = ModelV2.Info.make({ - ...ModelV2.Info.default(provider.id, ModelV2.ID.make("output-only")), - modelID: ModelV2.ID.make("output-only"), - package: ProviderV2.aisdk("test-provider"), + const model = Model.Info.make({ + ...Model.Info.default(provider.id, Model.ID.make("output-only")), + modelID: Model.ID.make("output-only"), + package: Provider.aisdk("test-provider"), cost: cost(0, 1), }) catalog.provider.update(provider.id, () => {}) @@ -358,8 +358,8 @@ describe("OpencodePlugin", () => { }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public") - expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("output-only"))).enabled).toBe( + expect(required(yield* catalog.provider.get(Provider.ID.opencode)).settings?.apiKey).toBe("public") + expect(required(yield* catalog.model.get(Provider.ID.opencode, Model.ID.make("output-only"))).enabled).toBe( true, ) }), @@ -371,14 +371,14 @@ describe("OpencodePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const provider = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.opencode), - package: ProviderV2.aisdk("test-provider"), + const provider = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.opencode), + package: Provider.aisdk("test-provider"), }) - const model = ModelV2.Info.make({ - ...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")), - modelID: ModelV2.ID.make("paid"), - package: ProviderV2.aisdk("test-provider"), + const model = Model.Info.make({ + ...Model.Info.default(provider.id, Model.ID.make("paid")), + modelID: Model.ID.make("paid"), + package: Provider.aisdk("test-provider"), cost: cost(1), }) catalog.provider.update(provider.id, () => {}) @@ -387,8 +387,8 @@ describe("OpencodePlugin", () => { }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBeUndefined() - expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true) + expect(required(yield* catalog.provider.get(Provider.ID.opencode)).settings?.apiKey).toBeUndefined() + expect(required(yield* catalog.model.get(Provider.ID.opencode, Model.ID.make("paid"))).enabled).toBe(true) }), ), ) @@ -405,14 +405,14 @@ describe("OpencodePlugin", () => { }) }) yield* catalog.transform((catalog) => { - const provider = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.opencode), - package: ProviderV2.aisdk("test-provider"), + const provider = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.opencode), + package: Provider.aisdk("test-provider"), }) - const model = ModelV2.Info.make({ - ...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")), - modelID: ModelV2.ID.make("paid"), - package: ProviderV2.aisdk("test-provider"), + const model = Model.Info.make({ + ...Model.Info.default(provider.id, Model.ID.make("paid")), + modelID: Model.ID.make("paid"), + package: Provider.aisdk("test-provider"), cost: cost(1), }) catalog.provider.update(provider.id, () => {}) @@ -421,8 +421,8 @@ describe("OpencodePlugin", () => { }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBeUndefined() - expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true) + expect(required(yield* catalog.provider.get(Provider.ID.opencode)).settings?.apiKey).toBeUndefined() + expect(required(yield* catalog.model.get(Provider.ID.opencode, Model.ID.make("paid"))).enabled).toBe(true) }), ), ) @@ -432,15 +432,15 @@ describe("OpencodePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const provider = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.opencode), - package: ProviderV2.aisdk("test-provider"), + const provider = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.opencode), + package: Provider.aisdk("test-provider"), settings: { apiKey: "configured" }, }) - const model = ModelV2.Info.make({ - ...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")), - modelID: ModelV2.ID.make("paid"), - package: ProviderV2.aisdk("test-provider"), + const model = Model.Info.make({ + ...Model.Info.default(provider.id, Model.ID.make("paid")), + modelID: Model.ID.make("paid"), + package: Provider.aisdk("test-provider"), cost: cost(1), }) catalog.provider.update(provider.id, (draft) => { @@ -452,8 +452,8 @@ describe("OpencodePlugin", () => { }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("configured") - expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true) + expect(required(yield* catalog.provider.get(Provider.ID.opencode)).settings?.apiKey).toBe("configured") + expect(required(yield* catalog.model.get(Provider.ID.opencode, Model.ID.make("paid"))).enabled).toBe(true) }), ), ) @@ -463,14 +463,14 @@ describe("OpencodePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const provider = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.openai), - package: ProviderV2.aisdk("test-provider"), + const provider = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.openai), + package: Provider.aisdk("test-provider"), }) - const model = ModelV2.Info.make({ - ...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")), - modelID: ModelV2.ID.make("paid"), - package: ProviderV2.aisdk("test-provider"), + const model = Model.Info.make({ + ...Model.Info.default(provider.id, Model.ID.make("paid")), + modelID: Model.ID.make("paid"), + package: Provider.aisdk("test-provider"), cost: cost(1), }) catalog.provider.update(provider.id, () => {}) @@ -479,8 +479,8 @@ describe("OpencodePlugin", () => { }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).settings?.apiKey).toBeUndefined() - expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("paid"))).enabled).toBe(true) + expect(required(yield* catalog.provider.get(Provider.ID.openai)).settings?.apiKey).toBeUndefined() + expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("paid"))).enabled).toBe(true) }), ), ) @@ -488,17 +488,17 @@ describe("OpencodePlugin", () => { it.effect("prefers gpt-5-nano as the opencode small model", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.opencode + const providerID = Provider.ID.opencode yield* catalog.transform((catalog) => { catalog.provider.update(providerID, () => {}) - catalog.model.update(providerID, ModelV2.ID.make("cheap-mini"), (model) => { + catalog.model.update(providerID, Model.ID.make("cheap-mini"), (model) => { model.capabilities.input = ["text"] model.capabilities.output = ["text"] model.cost = [...cost(1, 1)] model.time.released = Date.now() }) - catalog.model.update(providerID, ModelV2.ID.make("gpt-5-nano"), (model) => { + catalog.model.update(providerID, Model.ID.make("gpt-5-nano"), (model) => { model.capabilities.input = ["text"] model.capabilities.output = ["text"] model.cost = [...cost(10, 10)] @@ -508,7 +508,7 @@ describe("OpencodePlugin", () => { const selected = yield* catalog.model.small(providerID) - expect(selected?.id).toBe(ModelV2.ID.make("gpt-5-nano")) + expect(selected?.id).toBe(Model.ID.make("gpt-5-nano")) }), ) }) diff --git a/packages/core/test/plugin/provider-openrouter.test.ts b/packages/core/test/plugin/provider-openrouter.test.ts index 63520e01d3f6..c2e3109f16f4 100644 --- a/packages/core/test/plugin/provider-openrouter.test.ts +++ b/packages/core/test/plugin/provider-openrouter.test.ts @@ -2,19 +2,19 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { ProviderPlugins } from "@opencode-ai/core/plugin/provider" import { OpenRouterPlugin } from "@opencode-ai/core/plugin/provider/openrouter" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* OpenRouterPlugin.effect(host) @@ -29,34 +29,34 @@ describe("OpenRouterPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.openrouter, (provider) => { - provider.package = ProviderV2.aisdk("@openrouter/ai-sdk-provider") + catalog.provider.update(Provider.ID.openrouter, (provider) => { + provider.package = Provider.aisdk("@openrouter/ai-sdk-provider") provider.headers = { Existing: "value" } }) - catalog.provider.update(ProviderV2.ID.make("nvidia"), () => {}) + catalog.provider.update(Provider.ID.make("nvidia"), () => {}) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toEqual({ + expect((yield* catalog.provider.get(Provider.ID.openrouter))?.headers).toEqual({ Existing: "value", "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", }) - expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toBeUndefined() + expect((yield* catalog.provider.get(Provider.ID.make("nvidia")))?.headers).toBeUndefined() }), ) it.effect("creates an SDK only for the OpenRouter package", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const ignored = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.openrouter, Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/openai-compatible", options: { name: "openrouter" }, @@ -64,10 +64,10 @@ describe("OpenRouterPlugin", () => { expect(ignored.sdk).toBeUndefined() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), + package: Provider.aisdk("test-provider"), }), package: "@openrouter/ai-sdk-provider", options: { name: "custom" }, @@ -80,21 +80,21 @@ describe("OpenRouterPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.openrouter, (provider) => { - provider.package = ProviderV2.aisdk("@openrouter/ai-sdk-provider") + catalog.provider.update(Provider.ID.openrouter, (provider) => { + provider.package = Provider.aisdk("@openrouter/ai-sdk-provider") }) - catalog.provider.update(ProviderV2.ID.openai, () => {}) - catalog.model.update(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5-chat"), () => {}) - catalog.model.update(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5"), () => {}) - catalog.model.update(ProviderV2.ID.openai, ModelV2.ID.make("openai/gpt-5-chat"), () => {}) + catalog.provider.update(Provider.ID.openai, () => {}) + catalog.model.update(Provider.ID.openrouter, Model.ID.make("openai/gpt-5-chat"), () => {}) + catalog.model.update(Provider.ID.openrouter, Model.ID.make("openai/gpt-5"), () => {}) + catalog.model.update(Provider.ID.openai, Model.ID.make("openai/gpt-5-chat"), () => {}) }) yield* addPlugin() - expect((yield* catalog.model.get(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5-chat")))?.enabled).toBe( + expect((yield* catalog.model.get(Provider.ID.openrouter, Model.ID.make("openai/gpt-5-chat")))?.enabled).toBe( false, ) - expect((yield* catalog.model.get(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")))?.enabled).toBe(true) - expect((yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("openai/gpt-5-chat")))?.enabled).toBe(true) + expect((yield* catalog.model.get(Provider.ID.openrouter, Model.ID.make("openai/gpt-5")))?.enabled).toBe(true) + expect((yield* catalog.model.get(Provider.ID.openai, Model.ID.make("openai/gpt-5-chat")))?.enabled).toBe(true) }), ) @@ -102,12 +102,12 @@ describe("OpenRouterPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("custom-openrouter"), () => {}) - catalog.model.update(ProviderV2.ID.make("custom-openrouter"), ModelV2.ID.make("gpt-5-chat-latest"), () => {}) + catalog.provider.update(Provider.ID.make("custom-openrouter"), () => {}) + catalog.model.update(Provider.ID.make("custom-openrouter"), Model.ID.make("gpt-5-chat-latest"), () => {}) }) yield* addPlugin() expect( - (yield* catalog.model.get(ProviderV2.ID.make("custom-openrouter"), ModelV2.ID.make("gpt-5-chat-latest"))) + (yield* catalog.model.get(Provider.ID.make("custom-openrouter"), Model.ID.make("gpt-5-chat-latest"))) ?.enabled, ).toBe(true) }), diff --git a/packages/core/test/plugin/provider-sap-ai-core.test.ts b/packages/core/test/plugin/provider-sap-ai-core.test.ts index 09d99867f2ca..5524c93af057 100644 --- a/packages/core/test/plugin/provider-sap-ai-core.test.ts +++ b/packages/core/test/plugin/provider-sap-ai-core.test.ts @@ -1,12 +1,12 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { Npm } from "@opencode-ai/util/npm" import { SapAICorePlugin } from "@opencode-ai/core/plugin/provider/sap-ai-core" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" @@ -19,7 +19,7 @@ const npm = Npm.Service.of({ }) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* SapAICorePlugin.effect(host).pipe(Effect.provideService(Npm.Service, npm)) @@ -47,10 +47,10 @@ function withEnv(vars: Record, effect: () = } function model(providerID: string) { - return ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make(providerID), ModelV2.ID.make("sap-model")), - modelID: ModelV2.ID.make("sap-model"), - package: ProviderV2.aisdk(fixtureProvider), + return Model.Info.make({ + ...Model.Info.default(Provider.ID.make(providerID), Model.ID.make("sap-model")), + modelID: Model.ID.make("sap-model"), + package: Provider.aisdk(fixtureProvider), }) } @@ -60,7 +60,7 @@ describe("SapAICorePlugin", () => { { AICORE_SERVICE_KEY: undefined, AICORE_DEPLOYMENT_ID: "deployment", AICORE_RESOURCE_GROUP: "resource-group" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const sdk = yield* aisdk.runSDK({ @@ -83,7 +83,7 @@ describe("SapAICorePlugin", () => { }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const sdk = yield* aisdk.runSDK({ @@ -102,7 +102,7 @@ describe("SapAICorePlugin", () => { { AICORE_SERVICE_KEY: undefined, AICORE_DEPLOYMENT_ID: "deployment", AICORE_RESOURCE_GROUP: "resource-group" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const sdk = yield* aisdk.runSDK({ @@ -118,7 +118,7 @@ describe("SapAICorePlugin", () => { it.effect("uses the callable SDK for language selection", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const sdk = Object.assign((modelID: string) => ({ modelID, provider: "callable" }), { @@ -136,7 +136,7 @@ describe("SapAICorePlugin", () => { { AICORE_SERVICE_KEY: undefined, AICORE_DEPLOYMENT_ID: "deployment", AICORE_RESOURCE_GROUP: "resource-group" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const sdk = yield* aisdk.runSDK({ diff --git a/packages/core/test/plugin/provider-snowflake-cortex.test.ts b/packages/core/test/plugin/provider-snowflake-cortex.test.ts index 749f4c5520fe..f72c19f90f3c 100644 --- a/packages/core/test/plugin/provider-snowflake-cortex.test.ts +++ b/packages/core/test/plugin/provider-snowflake-cortex.test.ts @@ -1,19 +1,19 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect, it as bun_it } from "bun:test" import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { SnowflakeCortexPlugin, cortexFetch } from "@opencode-ai/core/plugin/provider/snowflake-cortex" import { ProviderPlugins } from "@opencode-ai/core/plugin/provider" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* SnowflakeCortexPlugin.effect(host) @@ -53,13 +53,13 @@ describe("SnowflakeCortexPlugin", () => { it.effect("ignores non-snowflake-cortex providers", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-4")), - modelID: ModelV2.ID.make("gpt-4"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("openai"), Model.ID.make("gpt-4")), + modelID: Model.ID.make("gpt-4"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai", @@ -72,13 +72,13 @@ describe("SnowflakeCortexPlugin", () => { it.effect("creates SDK for snowflake-cortex using SNOWFLAKE_CORTEX_PAT env var", () => withEnv({ SNOWFLAKE_CORTEX_PAT: "test-pat" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), - modelID: ModelV2.ID.make("claude-sonnet-4-6"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("snowflake-cortex"), Model.ID.make("claude-sonnet-4-6")), + modelID: Model.ID.make("claude-sonnet-4-6"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", @@ -92,13 +92,13 @@ describe("SnowflakeCortexPlugin", () => { it.effect("falls back to options.apiKey when SNOWFLAKE_CORTEX_PAT env var is absent", () => withEnv({ SNOWFLAKE_CORTEX_PAT: undefined }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), - modelID: ModelV2.ID.make("claude-sonnet-4-6"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("snowflake-cortex"), Model.ID.make("claude-sonnet-4-6")), + modelID: Model.ID.make("claude-sonnet-4-6"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", @@ -116,13 +116,13 @@ describe("SnowflakeCortexPlugin", () => { it.effect("uses SNOWFLAKE_CORTEX_TOKEN env var", () => withEnv({ SNOWFLAKE_CORTEX_TOKEN: "oauth-token", SNOWFLAKE_CORTEX_PAT: undefined }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), - modelID: ModelV2.ID.make("claude-sonnet-4-6"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("snowflake-cortex"), Model.ID.make("claude-sonnet-4-6")), + modelID: Model.ID.make("claude-sonnet-4-6"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", @@ -136,13 +136,13 @@ describe("SnowflakeCortexPlugin", () => { it.effect("falls back to options.token when no Snowflake env token is set", () => withEnv({ SNOWFLAKE_CORTEX_TOKEN: undefined, SNOWFLAKE_CORTEX_PAT: undefined }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), - modelID: ModelV2.ID.make("claude-sonnet-4-6"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("snowflake-cortex"), Model.ID.make("claude-sonnet-4-6")), + modelID: Model.ID.make("claude-sonnet-4-6"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", @@ -160,13 +160,13 @@ describe("SnowflakeCortexPlugin", () => { it.effect("sets includeUsage on the SDK options", () => withEnv({ SNOWFLAKE_CORTEX_PAT: "test-pat" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), - modelID: ModelV2.ID.make("claude-sonnet-4-6"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("snowflake-cortex"), Model.ID.make("claude-sonnet-4-6")), + modelID: Model.ID.make("claude-sonnet-4-6"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", diff --git a/packages/core/test/plugin/provider-vercel.test.ts b/packages/core/test/plugin/provider-vercel.test.ts index 29820b714c56..bf4998ae7d30 100644 --- a/packages/core/test/plugin/provider-vercel.test.ts +++ b/packages/core/test/plugin/provider-vercel.test.ts @@ -2,18 +2,18 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { VercelPlugin } from "@opencode-ai/core/plugin/provider/vercel" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* VercelPlugin.effect(host) @@ -24,13 +24,13 @@ describe("VercelPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/vercel") + catalog.provider.update(Provider.ID.make("vercel"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/vercel") provider.headers = { ...provider.headers, Existing: "1" } }) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).toEqual({ + expect((yield* catalog.provider.get(Provider.ID.make("vercel")))?.headers).toEqual({ Existing: "1", "http-referer": "https://opencode.ai/", "x-title": "opencode", @@ -42,25 +42,25 @@ describe("VercelPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/vercel") + catalog.provider.update(Provider.ID.make("vercel"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/vercel") }), ) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).not.toHaveProperty("HTTP-Referer") - expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).not.toHaveProperty("X-Title") + expect((yield* catalog.provider.get(Provider.ID.make("vercel")))?.headers).not.toHaveProperty("HTTP-Referer") + expect((yield* catalog.provider.get(Provider.ID.make("vercel")))?.headers).not.toHaveProperty("X-Title") }), ) it.effect("creates @ai-sdk/vercel SDKs for custom provider IDs", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const event = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom-vercel"), ModelV2.ID.make("v0-1.0-md")), - modelID: ModelV2.ID.make("v0-1.0-md"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom-vercel"), Model.ID.make("v0-1.0-md")), + modelID: Model.ID.make("v0-1.0-md"), package: "aisdk:@ai-sdk/vercel", }), package: "@ai-sdk/vercel", @@ -74,9 +74,9 @@ describe("VercelPlugin", () => { it.effect("ignores non-Vercel providers", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gateway"), () => {})) + yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.make("gateway"), () => {})) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("gateway")))?.headers).toBeUndefined() + expect((yield* catalog.provider.get(Provider.ID.make("gateway")))?.headers).toBeUndefined() }), ) }) diff --git a/packages/core/test/plugin/provider-xai.test.ts b/packages/core/test/plugin/provider-xai.test.ts index 4b3e672c1ed8..429ccdba1944 100644 --- a/packages/core/test/plugin/provider-xai.test.ts +++ b/packages/core/test/plugin/provider-xai.test.ts @@ -3,18 +3,18 @@ import type { LanguageModelV3 } from "@ai-sdk/provider" import { describe, expect } from "bun:test" import { Effect } from "effect" import { Integration } from "@opencode-ai/core/integration" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { XAIPlugin } from "@opencode-ai/core/plugin/provider/xai" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) yield* XAIPlugin.effect(host) }) @@ -61,9 +61,9 @@ describe("XAIPlugin", () => { yield* addPlugin() const ignored = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), - modelID: ModelV2.ID.make("grok-4"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("xai"), Model.ID.make("grok-4")), + modelID: Model.ID.make("grok-4"), package: "aisdk:@ai-sdk/xai", }), package: "@ai-sdk/openai-compatible", @@ -71,9 +71,9 @@ describe("XAIPlugin", () => { }) const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), - modelID: ModelV2.ID.make("grok-4"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("xai"), Model.ID.make("grok-4")), + modelID: Model.ID.make("grok-4"), package: "aisdk:@ai-sdk/xai", }), package: "@ai-sdk/xai", @@ -91,9 +91,9 @@ describe("XAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom-xai"), ModelV2.ID.make("grok-4")), - modelID: ModelV2.ID.make("grok-4"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom-xai"), Model.ID.make("grok-4")), + modelID: Model.ID.make("grok-4"), package: "aisdk:@ai-sdk/xai", }), package: "@ai-sdk/xai", @@ -111,9 +111,9 @@ describe("XAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("xai"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("grok-4"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("xai"), Model.ID.make("alias")), + modelID: Model.ID.make("grok-4"), package: "aisdk:@ai-sdk/xai", }), sdk: fakeSelectorSdk(calls), @@ -132,9 +132,9 @@ describe("XAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("grok-4")), - modelID: ModelV2.ID.make("grok-4"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.openai, Model.ID.make("grok-4")), + modelID: Model.ID.make("grok-4"), package: "aisdk:@ai-sdk/xai", }), sdk: fakeSelectorSdk(calls), diff --git a/packages/core/test/plugin/provider-zenmux.test.ts b/packages/core/test/plugin/provider-zenmux.test.ts index 8f3bdd343a0d..de599148dbc1 100644 --- a/packages/core/test/plugin/provider-zenmux.test.ts +++ b/packages/core/test/plugin/provider-zenmux.test.ts @@ -1,18 +1,18 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { ProviderPlugins } from "@opencode-ai/core/plugin/provider" import { ZenmuxPlugin } from "@opencode-ai/core/plugin/provider/zenmux" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) yield* ZenmuxPlugin.effect(host) }) @@ -31,13 +31,13 @@ describe("ZenmuxPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("zenmux"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" } }) }) yield* addPlugin() - const result = required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))) + const result = required(yield* catalog.provider.get(Provider.ID.make("zenmux"))) expect(result.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }) expect(Object.keys(required(result.headers)).sort()).toEqual(["HTTP-Referer", "X-Title"]) }), @@ -47,15 +47,15 @@ describe("ZenmuxPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("zenmux"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" } provider.headers = { ...provider.headers, Existing: "value" } }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).headers).toEqual({ + expect(required(yield* catalog.provider.get(Provider.ID.make("zenmux"))).headers).toEqual({ Existing: "value", "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", @@ -67,15 +67,15 @@ describe("ZenmuxPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("zenmux"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" } provider.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" } }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).headers).toEqual({ + expect(required(yield* catalog.provider.get(Provider.ID.make("zenmux"))).headers).toEqual({ "HTTP-Referer": "https://example.com/", "X-Title": "custom-title", }) @@ -86,13 +86,13 @@ describe("ZenmuxPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.openrouter, (provider) => { + catalog.provider.update(Provider.ID.openrouter, (provider) => { provider.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" } }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).headers).toEqual({ + expect(required(yield* catalog.provider.get(Provider.ID.openrouter)).headers).toEqual({ "HTTP-Referer": "https://example.com/", "X-Title": "custom-title", }) diff --git a/packages/core/test/plugin/skill.test.ts b/packages/core/test/plugin/skill.test.ts index cd9a9b3d82ad..7ce387f97fa1 100644 --- a/packages/core/test/plugin/skill.test.ts +++ b/packages/core/test/plugin/skill.test.ts @@ -7,17 +7,17 @@ import { Location } from "@opencode-ai/core/location" import { Effect } from "effect" import { SkillPlugin } from "@opencode-ai/core/plugin/skill" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SkillV2 } from "@opencode-ai/core/skill" +import { Skill } from "@opencode-ai/core/skill" import { location } from "../fixture/location" import { testEffect } from "../lib/effect" import { host } from "./host" -const it = testEffect(AppNodeBuilder.build(SkillV2.node)) +const it = testEffect(AppNodeBuilder.build(Skill.node)) describe("SkillPlugin.Plugin", () => { it.effect("registers built-in skills", () => Effect.gen(function* () { - const skill = yield* SkillV2.Service + const skill = yield* Skill.Service yield* SkillPlugin.Plugin.effect( host({ app: { name: "test", version: "1.2.3", channel: "beta" }, diff --git a/packages/core/test/plugin/system-prompt.test.ts b/packages/core/test/plugin/system-prompt.test.ts index 724ab681b11f..1c9c67d9c5ca 100644 --- a/packages/core/test/plugin/system-prompt.test.ts +++ b/packages/core/test/plugin/system-prompt.test.ts @@ -1,13 +1,13 @@ import { describe, expect, test } from "bun:test" import { SystemPart } from "@opencode-ai/ai" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Catalog } from "@opencode-ai/core/catalog" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHooks } from "@opencode-ai/core/plugin/hooks" import { PluginHost } from "@opencode-ai/core/plugin/host" import { SystemPromptPlugin } from "@opencode-ai/core/plugin/system-prompt" -import { SessionV2 } from "@opencode-ai/core/session" -import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session" +import { Session } from "@opencode-ai/core/session" +import type { SessionHooks } from "@opencode-ai/plugin/effect/session" import { Model } from "@opencode-ai/schema/model" import { Provider } from "@opencode-ai/schema/provider" import { Effect } from "effect" @@ -19,13 +19,15 @@ import PROMPT_DEFAULT from "../../src/session/runner/prompt/base.txt" const it = testEffect(PluginTestLayer) const fallback = PROMPT_DEFAULT const makeHost = Effect.gen(function* () { - const plugins = yield* PluginV2.Service + const agents = yield* Agent.Service + const plugins = yield* Plugin.Service + yield* agents.transform((draft) => draft.update(Agent.ID.make("build"), () => {})) return yield* PluginHost.make(plugins) }) const context = (id: string, system = fallback): SessionHooks["context"] => ({ - sessionID: SessionV2.ID.make("ses_system_prompt"), - agent: AgentV2.ID.make("build"), + sessionID: Session.ID.make("ses_system_prompt"), + agent: Agent.ID.make("build"), model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make(id) }), system: [SystemPart.make(system)], messages: [], @@ -33,7 +35,7 @@ const context = (id: string, system = fallback): SessionHooks["context"] => ({ }) describe("SystemPromptPlugin", () => { - test("uses V2 vocabulary in the Meta prompt", () => { + test("uses current vocabulary in the Meta prompt", () => { expect(PROMPT_META).toContain("webfetch tool") expect(PROMPT_META).toContain("subagent tool") expect(PROMPT_META).toContain("shell tool") @@ -92,10 +94,10 @@ describe("SystemPromptPlugin", () => { it.effect("preserves an explicit agent system prompt", () => Effect.gen(function* () { - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const hooks = yield* PluginHooks.Service yield* agents.transform((draft) => - draft.update(AgentV2.ID.make("build"), (agent) => { + draft.update(Agent.ID.make("build"), (agent) => { agent.system = "Custom agent prompt" }), ) @@ -111,6 +113,21 @@ describe("SystemPromptPlugin", () => { }), ) + it.effect("skips the hook when agent lookup fails", () => + Effect.gen(function* () { + const agents = yield* Agent.Service + const hooks = yield* PluginHooks.Service + const pluginHost = yield* makeHost + yield* SystemPromptPlugin.OpenAIPlugin.effect(pluginHost) + yield* agents.transform((draft) => draft.remove(Agent.ID.make("build"))) + const event = context("gpt-5") + + yield* hooks.trigger("session", "context", event) + + expect(event.system[0]?.text).toBe(fallback) + }), + ) + it.effect("allows one model-lab prompt plugin to be enabled independently", () => Effect.gen(function* () { const hooks = yield* PluginHooks.Service diff --git a/packages/core/test/plugin/variant.test.ts b/packages/core/test/plugin/variant.test.ts index 5e87748dc4af..1df1082fb663 100644 --- a/packages/core/test/plugin/variant.test.ts +++ b/packages/core/test/plugin/variant.test.ts @@ -3,9 +3,9 @@ import { Catalog } from "@opencode-ai/core/catalog" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Location } from "@opencode-ai/core/location" -import { ModelV2 } from "@opencode-ai/core/model" +import { Model } from "@opencode-ai/core/model" import { VariantPlugin } from "@opencode-ai/core/plugin/variant" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" import { Effect, Layer } from "effect" import { location } from "../fixture/location" @@ -23,17 +23,17 @@ describe("VariantPlugin", () => { Effect.gen(function* () { const service = yield* Catalog.Service yield* service.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.opencode, (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.opencode, (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") }) - catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => { - model.modelID = ModelV2.ID.make("glm-5.2") - model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.model.update(Provider.ID.opencode, Model.ID.make("glm-5.2"), (model) => { + model.modelID = Model.ID.make("glm-5.2") + model.package = Provider.aisdk("@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((yield* service.model.get(Provider.ID.opencode, Model.ID.make("glm-5.2")))?.variants).toEqual([ expect.objectContaining({ id: "high", settings: { reasoningEffort: "high" } }), expect.objectContaining({ id: "max", settings: { reasoningEffort: "max" } }), ]) @@ -44,15 +44,15 @@ describe("VariantPlugin", () => { 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.modelID = ModelV2.ID.make("glm-5.2") - model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") - model.variants = [{ id: ModelV2.VariantID.make("high"), settings: {}, headers: { custom: "true" }, body: {} }] + catalog.model.update(Provider.ID.opencode, Model.ID.make("glm-5.2"), (model) => { + model.modelID = Model.ID.make("glm-5.2") + model.package = Provider.aisdk("@ai-sdk/openai-compatible") + model.variants = [{ id: Model.VariantID.make("high"), settings: {}, 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((yield* service.model.get(Provider.ID.opencode, Model.ID.make("glm-5.2")))?.variants).toEqual([ expect.objectContaining({ id: "high", headers: { custom: "true" } }), expect.objectContaining({ id: "max", settings: { reasoningEffort: "max" } }), ]) diff --git a/packages/core/test/plugin/websearch-fixture.ts b/packages/core/test/plugin/websearch-fixture.ts index 406e093249cf..61046aefc843 100644 --- a/packages/core/test/plugin/websearch-fixture.ts +++ b/packages/core/test/plugin/websearch-fixture.ts @@ -4,7 +4,7 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Credential } from "@opencode-ai/core/credential" import { Config } from "@opencode-ai/core/config" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Form } from "@opencode-ai/core/form" import { Integration } from "@opencode-ai/core/integration" import { WebSearch } from "@opencode-ai/core/websearch" @@ -42,7 +42,7 @@ const http = Layer.succeed( export const webSearchIntegrationTest = testEffect( Layer.merge( AppNodeBuilder.build( - LayerNode.group([Integration.node, Credential.node, EventV2.node, Form.node, WebSearch.node]), + LayerNode.group([Integration.node, Credential.node, Bus.node, Form.node, WebSearch.node]), [[Config.node, Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) }))]], ), http, diff --git a/packages/core/test/project-copy.test.ts b/packages/core/test/project-copy.test.ts index d85d13dcc3bb..c574a9b0415d 100644 --- a/packages/core/test/project-copy.test.ts +++ b/packages/core/test/project-copy.test.ts @@ -9,7 +9,7 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { AbsolutePath } from "@opencode-ai/core/schema" import { Git } from "@opencode-ai/core/git" import { Database } from "@opencode-ai/core/database/database" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Project } from "@opencode-ai/core/project" import { ProjectDirectoryTable, ProjectTable } from "@opencode-ai/core/project/sql" import { ProjectCopy } from "@opencode-ai/core/project/copy" @@ -18,7 +18,7 @@ import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" const it = testEffect( - AppNodeBuilder.build(LayerNode.group([ProjectCopy.node, Database.node, EventV2.node, ProjectDirectories.node])), + AppNodeBuilder.build(LayerNode.group([ProjectCopy.node, Database.node, Bus.node, ProjectDirectories.node])), ) function abs(input: string) { @@ -116,14 +116,14 @@ describe("ProjectCopy", () => { Effect.gen(function* () { const input = yield* setup() const copy = yield* ProjectCopy.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const temp = yield* Effect.promise(() => fs.realpath(path.dirname(input.root.path))) const parent = abs(path.join(temp, path.basename(input.root.path) + "-copy-created")) const target = abs(path.join(parent, "copy")) yield* Effect.addFinalizer(() => Effect.promise(() => fs.rm(parent, { recursive: true, force: true })).pipe(Effect.ignore), ) - const fiber = yield* events + const fiber = yield* bus .subscribe(ProjectCopy.Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow @@ -278,8 +278,8 @@ describe("ProjectCopy", () => { Effect.gen(function* () { const input = yield* setup() const copy = yield* ProjectCopy.Service - const events = yield* EventV2.Service - const event = yield* events.subscribe(ProjectCopy.Event.Updated).pipe( + const bus = yield* Bus.Service + const event = yield* bus.subscribe(ProjectCopy.Event.Updated).pipe( Stream.take(1), Stream.runCollect, Effect.forkScoped, @@ -300,7 +300,7 @@ describe("ProjectCopy", () => { Effect.gen(function* () { const input = yield* setup() const copy = yield* ProjectCopy.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const target = abs(`${input.root.path}-copy-external`) yield* Effect.addFinalizer(() => Effect.promise(() => fs.rm(target, { recursive: true, force: true })).pipe(Effect.ignore), @@ -311,7 +311,7 @@ describe("ProjectCopy", () => { .values({ project_id: input.projectID, directory: target }) .run() .pipe(Effect.orDie) - const fiber = yield* events + const fiber = yield* bus .subscribe(ProjectCopy.Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow diff --git a/packages/core/test/project.test.ts b/packages/core/test/project.test.ts index dc505465c72d..44d36fd85f81 100644 --- a/packages/core/test/project.test.ts +++ b/packages/core/test/project.test.ts @@ -5,25 +5,25 @@ import path from "path" import { Effect, Layer, Schema } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Database } from "@opencode-ai/core/database/database" -import { ProjectV2 } from "@opencode-ai/core/project" +import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { AbsolutePath } from "@opencode-ai/core/schema" import { Hash } from "@opencode-ai/util/hash" import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" -const it = testEffect(Layer.merge(AppNodeBuilder.build(ProjectV2.node), AppNodeBuilder.build(Database.node))) +const it = testEffect(Layer.merge(AppNodeBuilder.build(Project.node), AppNodeBuilder.build(Database.node))) -describe("ProjectV2.list", () => { +describe("Project.list", () => { it.effect("returns complete projects ordered by recent update", () => Effect.gen(function* () { const db = (yield* Database.Service).db - const project = yield* ProjectV2.Service + const project = yield* Project.Service yield* db .insert(ProjectTable) .values([ { - id: ProjectV2.ID.make("older"), + id: Project.ID.make("older"), worktree: abs("/older"), vcs: "git", name: "Older", @@ -34,7 +34,7 @@ describe("ProjectV2.list", () => { time_updated: 1, }, { - id: ProjectV2.ID.make("newer"), + id: Project.ID.make("newer"), worktree: abs("/newer"), sandboxes: [], time_created: 2, @@ -46,13 +46,13 @@ describe("ProjectV2.list", () => { expect(yield* project.list()).toEqual([ { - id: ProjectV2.ID.make("newer"), + id: Project.ID.make("newer"), worktree: abs("/newer"), time: { created: 2, updated: 2, initialized: 3 }, sandboxes: [], }, { - id: ProjectV2.ID.make("older"), + id: Project.ID.make("older"), worktree: abs("/older"), vcs: "git", name: "Older", @@ -67,7 +67,7 @@ describe("ProjectV2.list", () => { }) function remoteID(remote: string) { - return ProjectV2.ID.make(Hash.fast(`git-remote:${remote}`)) + return Project.ID.make(Hash.fast(`git-remote:${remote}`)) } function abs(value: string) { @@ -92,18 +92,18 @@ async function rootCommit(dir: string) { return (await $`git rev-list --max-parents=0 HEAD`.cwd(dir).text()).trim() } -describe("ProjectV2.resolve", () => { +describe("Project.resolve", () => { it.live("returns global for non-git directory", () => Effect.gen(function* () { const tmp = yield* Effect.acquireRelease( Effect.promise(() => tmpdir()), (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(tmp.path)) - expect(result.id).toBe(ProjectV2.ID.make("global")) + expect(result.id).toBe(Project.ID.make("global")) expect(path.resolve(result.directory)).toBe(path.parse(tmp.path).root) expect(result.previous).toBeUndefined() expect(result.vcs).toBeUndefined() @@ -117,11 +117,11 @@ describe("ProjectV2.resolve", () => { (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ) yield* Effect.promise(() => initRepo(tmp.path)) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(tmp.path)) - expect(result.id).toBe(ProjectV2.ID.make("global")) + expect(result.id).toBe(Project.ID.make("global")) expect(result.directory).toBe(yield* real(tmp.path)) expect(result.previous).toBeUndefined() expect(result.vcs?.type).toBe("git") @@ -135,11 +135,11 @@ describe("ProjectV2.resolve", () => { (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ) yield* Effect.promise(() => initRepo(tmp.path, { commit: true })) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(tmp.path)) - expect(result.id).toBe(ProjectV2.ID.make(yield* Effect.promise(() => rootCommit(tmp.path)))) + expect(result.id).toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path)))) expect(result.directory).toBe(yield* real(tmp.path)) expect(result.previous).toBeUndefined() expect(result.vcs?.type).toBe("git") @@ -153,12 +153,12 @@ describe("ProjectV2.resolve", () => { (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ) yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:Acme/App.git" })) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(tmp.path)) expect(result.id).toBe(remoteID("github.com/Acme/App")) - expect(result.id).not.toBe(ProjectV2.ID.make(yield* Effect.promise(() => rootCommit(tmp.path)))) + expect(result.id).not.toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path)))) expect(result.directory).toBe(yield* real(tmp.path)) expect(result.vcs?.type).toBe("git") }), @@ -176,7 +176,7 @@ describe("ProjectV2.resolve", () => { ) yield* Effect.promise(() => initRepo(ssh.path, { commit: true, remote: "git@github.com:owner/repo.git" })) yield* Effect.promise(() => initRepo(https.path, { commit: true, remote: "https://github.com/owner/repo.git" })) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const a = yield* project.resolve(abs(ssh.path)) const b = yield* project.resolve(abs(https.path)) @@ -193,11 +193,11 @@ describe("ProjectV2.resolve", () => { (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ) yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: `file://${tmp.path}` })) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(tmp.path)) - expect(result.id).toBe(ProjectV2.ID.make(yield* Effect.promise(() => rootCommit(tmp.path)))) + expect(result.id).toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path)))) }), ) @@ -209,11 +209,11 @@ describe("ProjectV2.resolve", () => { ) yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" })) yield* Effect.promise(() => Bun.write(path.join(tmp.path, ".git", "opencode"), "old-id")) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(tmp.path)) - expect(result.previous).toBe(ProjectV2.ID.make("old-id")) + expect(result.previous).toBe(Project.ID.make("old-id")) expect(result.id).toBe(remoteID("github.com/owner/repo")) }), ) @@ -225,7 +225,7 @@ describe("ProjectV2.resolve", () => { (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ) yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" })) - const project = yield* ProjectV2.Service + const project = yield* Project.Service yield* project.resolve(abs(tmp.path)) @@ -241,7 +241,7 @@ describe("ProjectV2.resolve", () => { ) yield* Effect.promise(() => initRepo(tmp.path, { commit: true })) yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "a", "b"), { recursive: true })) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(path.join(tmp.path, "a", "b"))) @@ -270,13 +270,13 @@ describe("ProjectV2.resolve", () => { .quiet() await fs.mkdir(path.join(tmp.path, "a", "b"), { recursive: true }) }) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(path.join(tmp.path, "a", "b"))) expect(result.vcs?.type).toBe("hg") expect(result.directory).toBe(abs(tmp.path)) - expect(result.id).not.toBe(ProjectV2.ID.make("global")) + expect(result.id).not.toBe(Project.ID.make("global")) expect(result.previous).toBeUndefined() }), ) @@ -289,7 +289,7 @@ describe("ProjectV2.resolve", () => { ) yield* Effect.promise(() => initRepo(tmp.path, { commit: true })) yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, ".hg"))) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(tmp.path)) @@ -304,12 +304,12 @@ describe("ProjectV2.resolve", () => { (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ) yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, ".hg"))) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(tmp.path)) expect(result.vcs?.type).toBe("hg") - expect(result.id).toBe(ProjectV2.ID.make("global")) + expect(result.id).toBe(Project.ID.make("global")) }), ) @@ -326,12 +326,12 @@ describe("ProjectV2.resolve", () => { yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" })) yield* Effect.promise(() => Bun.write(path.join(tmp.path, ".git", "opencode"), "old-id")) yield* Effect.promise(() => $`git worktree add ${worktree} -b test-${Date.now()}`.cwd(tmp.path).quiet()) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(worktree)) expect(result.directory).toBe(yield* real(worktree)) - expect(result.previous).toBe(ProjectV2.ID.make("old-id")) + expect(result.previous).toBe(Project.ID.make("old-id")) expect(result.id).toBe(remoteID("github.com/owner/repo")) expect(result.vcs?.type).toBe("git") }), diff --git a/packages/core/test/pty/pty-session.test.ts b/packages/core/test/pty/pty-session.test.ts index d11f36fc0bf5..05f6dbc588f1 100644 --- a/packages/core/test/pty/pty-session.test.ts +++ b/packages/core/test/pty/pty-session.test.ts @@ -3,7 +3,7 @@ import { Cause, Deferred, Effect, Exit, Layer, Queue } from "effect" import { Config } from "@opencode-ai/core/config" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Location } from "@opencode-ai/core/location" import { Pty } from "@opencode-ai/core/pty" import type { PtyID } from "@opencode-ai/core/pty/schema" @@ -19,7 +19,7 @@ const locationLayer = Layer.succeed( ) const configLayer = Layer.mock(Config.Service)({ entries: () => Effect.succeed([]) }) const it = testEffect( - AppNodeBuilder.build(LayerNode.group([Pty.node, EventV2.node]), [ + AppNodeBuilder.build(LayerNode.group([Pty.node, Bus.node]), [ [Config.node, configLayer], [Location.node, locationLayer], ]), @@ -27,7 +27,7 @@ const it = testEffect( const ptyTest = process.platform === "win32" ? it.live.skip : it.live const subscribePtyEvents = Effect.fn("PtySessionTest.subscribePtyEvents")(function* () { - const source = yield* EventV2.Service + const source = yield* Bus.Service const events = yield* Queue.unbounded() const unsubscribe = yield* source.listen((event) => { if (event.type === Pty.Event.Created.type) @@ -206,7 +206,7 @@ describe("pty", () => { const configuredShell = process.platform === "win32" ? undefined : Bun.which("bash") const configuredIt = testEffect( - AppNodeBuilder.build(LayerNode.group([Pty.node, EventV2.node]), [ + AppNodeBuilder.build(LayerNode.group([Pty.node, Bus.node]), [ [ Config.node, Layer.mock(Config.Service)({ diff --git a/packages/core/test/pty/ticket.test.ts b/packages/core/test/pty/ticket.test.ts index c478e721dba1..563264ec71e9 100644 --- a/packages/core/test/pty/ticket.test.ts +++ b/packages/core/test/pty/ticket.test.ts @@ -3,7 +3,7 @@ import { Effect, Layer } from "effect" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { PtyID } from "@opencode-ai/core/pty/schema" import { PtyTicket } from "@opencode-ai/core/pty/ticket" -import { WorkspaceV2 } from "@opencode-ai/core/workspace" +import { Workspace } from "@opencode-ai/core/workspace" import { testEffect } from "../lib/effect" const it = testEffect(LayerNode.compile(PtyTicket.node)) @@ -50,10 +50,10 @@ describe("PTY websocket tickets", () => { Effect.gen(function* () { const tickets = yield* PtyTicket.Service const ptyID = PtyID.ascending() - const workspaceID = WorkspaceV2.ID.ascending() + const workspaceID = Workspace.ID.ascending() const issued = yield* tickets.issue({ ptyID, workspaceID }) - expect(yield* tickets.consume({ ptyID, workspaceID: WorkspaceV2.ID.ascending(), ticket: issued.ticket })).toBe( + expect(yield* tickets.consume({ ptyID, workspaceID: Workspace.ID.ascending(), ticket: issued.ticket })).toBe( false, ) expect(yield* tickets.consume({ ptyID, workspaceID, ticket: issued.ticket })).toBe(true) diff --git a/packages/core/test/question.test.ts b/packages/core/test/question.test.ts index d60288cc7964..cd3e0369b2a3 100644 --- a/packages/core/test/question.test.ts +++ b/packages/core/test/question.test.ts @@ -2,30 +2,31 @@ import { describe, expect } from "bun:test" import { Context, Deferred, Effect, Exit, Fiber, Layer, Scope } from "effect" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { EventV2 } from "@opencode-ai/core/event" -import { QuestionV2 } from "@opencode-ai/core/question" -import { SessionV2 } from "@opencode-ai/core/session" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" +import { Question } from "@opencode-ai/core/question" +import { Session } from "@opencode-ai/core/session" import { testEffect } from "./lib/effect" -const questions = AppNodeBuilder.build(LayerNode.group([EventV2.node, QuestionV2.node])) +const questions = AppNodeBuilder.build(LayerNode.group([Bus.node, Question.node])) const it = testEffect(questions) -const sessionID = SessionV2.ID.make("ses_question_test") -const question: QuestionV2.Info = { +const sessionID = Session.ID.make("ses_question_test") +const question: Question.Info = { question: "Which option?", header: "Option", options: [{ label: "One", description: "First option" }], } -const waitForAsk = Effect.fn("QuestionV2Test.waitForAsk")(function* ( - service: QuestionV2.Interface, - input: QuestionV2.AskInput, +const waitForAsk = Effect.fn("QuestionTest.waitForAsk")(function* ( + service: Question.Interface, + input: Question.AskInput, ) { - const events = yield* EventV2.Service - const asked = yield* Deferred.make() - const unsubscribe = yield* events.listen((event) => - event.type === QuestionV2.Event.Asked.type - ? Deferred.succeed(asked, event.data as QuestionV2.Request).pipe(Effect.asVoid) + const bus = yield* Bus.Service + const asked = yield* Deferred.make() + const unsubscribe = yield* bus.listen((event) => + event.type === Question.Event.Asked.type + ? Deferred.succeed(asked, event.data as Question.Request).pipe(Effect.asVoid) : Effect.void, ) yield* Effect.addFinalizer(() => unsubscribe) @@ -33,15 +34,15 @@ const waitForAsk = Effect.fn("QuestionV2Test.waitForAsk")(function* ( return { fiber, request: yield* Deferred.await(asked) } }) -describe("QuestionV2", () => { +describe("Question", () => { it.effect("publishes lifecycle events and settles a pending reply", () => Effect.gen(function* () { - const service = yield* QuestionV2.Service - const events = yield* EventV2.Service - const published: EventV2.Payload[] = [] - const unsubscribe = yield* events.listen((event) => + const service = yield* Question.Service + const bus = yield* Bus.Service + const published: Event.Payload[] = [] + const unsubscribe = yield* bus.listen((event) => Effect.sync(() => { - if (event.type.startsWith("question.v2.")) published.push(event) + if (event.type.startsWith("question.")) published.push(event) }), ) yield* Effect.addFinalizer(() => unsubscribe) @@ -54,20 +55,20 @@ describe("QuestionV2", () => { expect(yield* Fiber.join(fiber)).toEqual([["One"]]) expect(yield* service.list()).toEqual([]) expect(published.map((event) => [event.type, event.data])).toEqual([ - [QuestionV2.Event.Asked.type, request], - [QuestionV2.Event.Replied.type, { sessionID, requestID: request.id, answers: [["One"]] }], + [Question.Event.Asked.type, request], + [Question.Event.Replied.type, { sessionID, requestID: request.id, answers: [["One"]] }], ]) }), ) it.effect("publishes rejection, fails the ask, and rejects unknown IDs", () => Effect.gen(function* () { - const service = yield* QuestionV2.Service - const events = yield* EventV2.Service - const published: EventV2.Payload[] = [] - const unsubscribe = yield* events.listen((event) => + const service = yield* Question.Service + const bus = yield* Bus.Service + const published: Event.Payload[] = [] + const unsubscribe = yield* bus.listen((event) => Effect.sync(() => { - if (event.type === QuestionV2.Event.Rejected.type) published.push(event) + if (event.type === Question.Event.Rejected.type) published.push(event) }), ) yield* Effect.addFinalizer(() => unsubscribe) @@ -76,15 +77,15 @@ describe("QuestionV2", () => { yield* service.reject(request.id) const exit = yield* Fiber.await(fiber) expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) expect(exit.cause.toString()).toContain("QuestionV2.RejectedError") + if (Exit.isFailure(exit)) expect(exit.cause.toString()).toContain("Question.RejectedError") expect(published.map((event) => event.data)).toEqual([{ sessionID, requestID: request.id }]) - const unknown = QuestionV2.ID.ascending("que_unknown") + const unknown = Question.ID.ascending("que_unknown") expect(yield* service.reply({ requestID: unknown, answers: [] }).pipe(Effect.flip)).toEqual( - new QuestionV2.NotFoundError({ requestID: unknown }), + new Question.NotFoundError({ requestID: unknown }), ) expect(yield* service.reject(unknown).pipe(Effect.flip)).toEqual( - new QuestionV2.NotFoundError({ requestID: unknown }), + new Question.NotFoundError({ requestID: unknown }), ) }), ) @@ -93,21 +94,21 @@ describe("QuestionV2", () => { Effect.gen(function* () { const firstScope = yield* Scope.make() const secondScope = yield* Scope.make() - const first = Context.get(yield* Layer.buildWithScope(Layer.fresh(questions), firstScope), QuestionV2.Service) - const second = Context.get(yield* Layer.buildWithScope(Layer.fresh(questions), secondScope), QuestionV2.Service) + const first = Context.get(yield* Layer.buildWithScope(Layer.fresh(questions), firstScope), Question.Service) + const second = Context.get(yield* Layer.buildWithScope(Layer.fresh(questions), secondScope), Question.Service) const fiber = yield* first.ask({ sessionID, questions: [question] }).pipe(Effect.forkScoped) yield* Effect.yieldNow const request = (yield* first.list())[0]! expect(yield* second.list()).toEqual([]) expect(yield* second.reply({ requestID: request.id, answers: [["One"]] }).pipe(Effect.flip)).toEqual( - new QuestionV2.NotFoundError({ requestID: request.id }), + new Question.NotFoundError({ requestID: request.id }), ) yield* Scope.close(firstScope, Exit.void) const exit = yield* Fiber.await(fiber) expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) expect(exit.cause.toString()).toContain("QuestionV2.RejectedError") + if (Exit.isFailure(exit)) expect(exit.cause.toString()).toContain("Question.RejectedError") yield* Scope.close(secondScope, Exit.void) }), ) diff --git a/packages/core/test/session-compact.test.ts b/packages/core/test/session-compact.test.ts index 3ad15fc730ad..b8f46d91aff9 100644 --- a/packages/core/test/session-compact.test.ts +++ b/packages/core/test/session-compact.test.ts @@ -5,14 +5,14 @@ import { Config } from "@opencode-ai/core/config" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Job } from "@opencode-ai/core/job" import { Location } from "@opencode-ai/core/location" import { LocationServiceMap } from "@opencode-ai/core/location-service-map" import type { LocationServices } from "@opencode-ai/core/location-services" -import { ProjectV2 } from "@opencode-ai/core/project" +import { Project } from "@opencode-ai/core/project" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionCompaction } from "@opencode-ai/core/session/compaction" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionPending } from "@opencode-ai/core/session/pending" @@ -31,10 +31,10 @@ const model = Model.make({ route: OpenAIChat.route.with({ limits: { context: 10_000, output: 1_000 } }), }) const projects = Layer.succeed( - ProjectV2.Service, - ProjectV2.Service.of({ + Project.Service, + Project.Service.of({ list: () => Effect.succeed([]), - resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }), + resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory }), directories: () => Effect.succeed([]), commit: () => Effect.void, }), @@ -62,7 +62,7 @@ const locations = Layer.effect( LocationServiceMap.Service, LayerMap.make( () => - // The test only needs the compaction location service used by SessionV2.compact. + // The test only needs the compaction location service used by Session.compact. // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion SessionCompaction.layer.pipe( Layer.provide(client), @@ -73,25 +73,25 @@ const locations = Layer.effect( ) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]), + LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Session.node]), [ [LocationServiceMap.node, locations], - [ProjectV2.node, projects], + [Project.node, projects], [SessionExecution.node, SessionExecution.noopLayer], ], ), ) -describe("SessionV2.compact", () => { +describe("Session.compact", () => { it.effect("durably admits and coalesces manual compaction", () => Effect.gen(function* () { requests = [] - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const created = yield* session.create({ location }) const messageID = SessionMessage.ID.create() - yield* events.publish(SessionEvent.InputAdmitted, { + yield* bus.publish(SessionEvent.InputAdmitted, { sessionID: created.id, inputID: messageID, input: { @@ -100,7 +100,7 @@ describe("SessionV2.compact", () => { delivery: "steer", }, }) - yield* events.publish(SessionEvent.InputPromoted, { + yield* bus.publish(SessionEvent.InputPromoted, { sessionID: created.id, inputID: messageID, }) diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts index 4111ac222fc6..89f1f18d39e2 100644 --- a/packages/core/test/session-compaction.test.ts +++ b/packages/core/test/session-compaction.test.ts @@ -6,7 +6,7 @@ import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { llmClient } from "@opencode-ai/core/effect/app-node-platform" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { EventTable } from "@opencode-ai/core/event/sql" import { SessionCompaction } from "@opencode-ai/core/session/compaction" import { SessionEvent } from "@opencode-ai/core/session/event" @@ -15,7 +15,7 @@ import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" import { SessionTable } from "@opencode-ai/core/session/sql" import { SessionStore } from "@opencode-ai/core/session/store" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { App } from "@opencode-ai/core/app" @@ -78,7 +78,7 @@ const models = Layer.mock(SessionRunnerModel.Service)({ }) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionCompaction.node]), + LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, SessionCompaction.node]), [ [llmClient, client], [Config.node, config], @@ -136,10 +136,10 @@ it.effect("manual compaction summarizes short context instead of no-op", () => requests = [] const db = (yield* Database.Service).db const compaction = yield* SessionCompaction.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const store = yield* SessionStore.Service - const sessionID = SessionV2.ID.make("ses_manual_compaction") - const parentID = SessionV2.ID.make("ses_manual_compaction_parent") + const sessionID = Session.ID.make("ses_manual_compaction") + const parentID = Session.ID.make("ses_manual_compaction_parent") const userMessage = { id: SessionMessage.ID.create(), type: "user" as const, @@ -174,7 +174,7 @@ it.effect("manual compaction summarizes short context instead of no-op", () => ), ) - const delta = yield* events + const delta = yield* bus .subscribe(SessionEvent.Compaction.Delta) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow @@ -215,9 +215,9 @@ it.effect("manual compaction summarizes short context instead of no-op", () => .all() .pipe(Effect.orDie), ).toEqual([ - { type: EventV2.versionedType(SessionEvent.Compaction.Started.type, 1) }, - { type: EventV2.versionedType(SessionEvent.UsageRecorded.type, 1) }, - { type: EventV2.versionedType(SessionEvent.Compaction.Ended.type, 1) }, + { type: Bus.versionedType(SessionEvent.Compaction.Started.type, 1) }, + { type: Bus.versionedType(SessionEvent.UsageRecorded.type, 1) }, + { type: Bus.versionedType(SessionEvent.Compaction.Ended.type, 1) }, ]) }), ) diff --git a/packages/core/test/session-create.test.ts b/packages/core/test/session-create.test.ts index 422d5892300e..ae048e228b1b 100644 --- a/packages/core/test/session-create.test.ts +++ b/packages/core/test/session-create.test.ts @@ -2,20 +2,20 @@ import { describe, expect } from "bun:test" import path from "path" import { DateTime, Effect, Layer, Stream } from "effect" import { Money } from "@opencode-ai/schema/money" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { asc, eq } from "drizzle-orm" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { EventTable } from "@opencode-ai/core/event/sql" import { Location } from "@opencode-ai/core/location" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProjectV2 } from "@opencode-ai/core/project" +import { Model } from "@opencode-ai/core/model" +import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionV1 } from "@opencode-ai/core/v1/session" import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionProjector } from "@opencode-ai/core/session/projector" @@ -24,42 +24,42 @@ import { SessionPending } from "@opencode-ai/core/session/pending" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionTable } from "@opencode-ai/core/session/sql" import { SessionStore } from "@opencode-ai/core/session/store" -import { WorkspaceV2 } from "@opencode-ai/core/workspace" +import { Workspace } from "@opencode-ai/core/workspace" import { testEffect } from "./lib/effect" import { tmpdir } from "./fixture/tmpdir" const projects = Layer.succeed( - ProjectV2.Service, - ProjectV2.Service.of({ + Project.Service, + Project.Service.of({ list: () => Effect.succeed([]), - resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }), + resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory }), directories: () => Effect.succeed([]), commit: () => Effect.void, }), ) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]), + LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Session.node]), [ - [ProjectV2.node, projects], + [Project.node, projects], [SessionExecution.node, SessionExecution.noopLayer], ], ), ) const location = Location.Ref.make({ directory: AbsolutePath.make("/project") }) -const id = SessionV2.ID.create() +const id = Session.ID.create() /** Public session events from a `log` read, without synced markers. */ -const logEvents = (session: SessionV2.Interface, sessionID: SessionV2.ID, follow?: boolean) => +const logEvents = (session: Session.Interface, sessionID: Session.ID, follow?: boolean) => session .log({ sessionID, follow }) - .pipe(Stream.filter((item): item is SessionEvent.DurableEvent => !EventV2.isSynced(item))) + .pipe(Stream.filter((item): item is SessionEvent.DurableEvent => !Bus.isSynced(item))) -const assertCreateInputTypes = (session: SessionV2.Interface) => { +const assertCreateInputTypes = (session: Session.Interface) => { // @ts-expect-error location or parentID is required. session.create({}) // @ts-expect-error child sessions inherit their parent's location. - session.create({ parentID: SessionV2.ID.create(), location }) + session.create({ parentID: Session.ID.create(), location }) } void assertCreateInputTypes @@ -70,10 +70,10 @@ function withTmp(f: (directory: string) => Effect.Effect) { ).pipe(Effect.flatMap((tmp) => f(tmp.path))) } -describe("SessionV2.create", () => { +describe("Session.create", () => { it.effect("creates a fresh projected session when the ID is omitted", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const first = yield* session.create({ location }) const second = yield* session.create({ location }) @@ -85,7 +85,7 @@ describe("SessionV2.create", () => { it.effect("returns the original session when the ID is retried", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const input = { id, location } const first = yield* session.create(input) @@ -98,18 +98,18 @@ describe("SessionV2.create", () => { it.effect("stores supplied immutable create attributes", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const workspaceID = WorkspaceV2.ID.make("wrk_test") - const model = ModelV2.Ref.make({ - id: ModelV2.ID.make("sonnet"), - providerID: ProviderV2.ID.anthropic, - variant: ModelV2.VariantID.make("fast"), + const session = yield* Session.Service + const workspaceID = Workspace.ID.make("wrk_test") + const model = Model.Ref.make({ + id: Model.ID.make("sonnet"), + providerID: Provider.ID.anthropic, + variant: Model.VariantID.make("fast"), }) expect( yield* session.create({ location: Location.Ref.make({ directory: location.directory, workspaceID }), - agent: AgentV2.ID.make("build"), + agent: Agent.ID.make("build"), model, }), ).toMatchObject({ location: { directory: location.directory, workspaceID }, agent: "build", model }) @@ -118,7 +118,7 @@ describe("SessionV2.create", () => { it.effect("inherits location from an existing parent when omitted", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const parent = yield* session.create({ location }) const child = yield* session.create({ parentID: parent.id, title: "child" }) @@ -128,18 +128,18 @@ describe("SessionV2.create", () => { it.effect("rejects child creation when the parent does not exist", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const missing = SessionV2.ID.create() + const session = yield* Session.Service + const missing = Session.ID.create() expect(yield* Effect.flip(session.create({ parentID: missing, title: "child" }))).toEqual( - new SessionV2.NotFoundError({ sessionID: missing }), + new Session.NotFoundError({ sessionID: missing }), ) }), ) it.effect("filters root sessions before applying the page limit", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const { db } = yield* Database.Service const staleRoot = yield* session.create({ location, title: "stale root" }) const root = yield* session.create({ location, title: "root" }) @@ -173,7 +173,7 @@ describe("SessionV2.create", () => { it.effect("filters direct child sessions by parent ID", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const parent = yield* session.create({ location, title: "parent" }) const child = yield* session.create({ parentID: parent.id, title: "child" }) yield* session.create({ location, title: "other root" }) @@ -186,8 +186,8 @@ describe("SessionV2.create", () => { it.effect("forks a session by replaying a durable fork event into copied projected rows", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const parent = yield* session.create({ location, title: "Parent" }) const admitted = yield* session.prompt({ @@ -195,9 +195,9 @@ describe("SessionV2.create", () => { text: "First", resume: false, }) - yield* SessionPending.promote(db, events, parent.id, "steer") + yield* SessionPending.promote(db, bus, parent.id, "steer") yield* session.synthetic({ sessionID: parent.id, text: "parent note", resume: false }) - yield* SessionPending.promote(db, events, parent.id, "steer") + yield* SessionPending.promote(db, bus, parent.id, "steer") const forked = yield* session.fork({ sessionID: parent.id }) const parentContext = yield* session.context(parent.id) @@ -232,13 +232,13 @@ describe("SessionV2.create", () => { text: "Parent changed", resume: false, }) - yield* SessionPending.promote(db, events, parent.id, "steer") + yield* SessionPending.promote(db, bus, parent.id, "steer") yield* session.prompt({ sessionID: forked.id, text: "Child continues", resume: false, }) - yield* SessionPending.promote(db, events, forked.id, "steer") + yield* SessionPending.promote(db, bus, forked.id, "steer") expect((yield* session.context(parent.id)).map((message) => message.type)).toEqual(["user", "synthetic", "user"]) expect((yield* session.context(forked.id)).map((message) => message.type)).toEqual(["user", "synthetic", "user"]) @@ -254,8 +254,8 @@ describe("SessionV2.create", () => { it.effect("forks before the selected boundary message", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const parent = yield* session.create({ location }) const first = yield* session.prompt({ @@ -263,22 +263,22 @@ describe("SessionV2.create", () => { text: "First", resume: false, }) - yield* SessionPending.promote(db, events, parent.id, "steer") + yield* SessionPending.promote(db, bus, parent.id, "steer") const second = yield* session.prompt({ sessionID: parent.id, text: "Second", resume: false, }) - yield* SessionPending.promote(db, events, parent.id, "steer") + yield* SessionPending.promote(db, bus, parent.id, "steer") const assistantMessageID = SessionMessage.ID.create() - const model = ModelV2.Ref.make({ id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }) - yield* events.publish(SessionEvent.Step.Started, { + const model = Model.Ref.make({ id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }) + yield* bus.publish(SessionEvent.Step.Started, { sessionID: parent.id, assistantMessageID, - agent: AgentV2.ID.make("build"), + agent: Agent.ID.make("build"), model, }) - yield* events.publish(SessionEvent.Step.Ended, { + yield* bus.publish(SessionEvent.Step.Ended, { sessionID: parent.id, assistantMessageID, finish: "stop", @@ -308,15 +308,15 @@ describe("SessionV2.create", () => { it.effect("returns the existing Session when one ID is reused with different create arguments", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const created = yield* session.create({ id, location }) const changed = [ { id, location: Location.Ref.make({ directory: AbsolutePath.make("/other") }) }, - { id, location, agent: AgentV2.ID.make("build") }, + { id, location, agent: Agent.ID.make("build") }, { id, location, - model: ModelV2.Ref.make({ id: ModelV2.ID.make("sonnet"), providerID: ProviderV2.ID.anthropic }), + model: Model.Ref.make({ id: Model.ID.make("sonnet"), providerID: Provider.ID.anthropic }), }, ] @@ -329,7 +329,7 @@ describe("SessionV2.create", () => { it.effect("returns one recorded session to concurrent exact retries", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const input = { id, location } const created = yield* Effect.all([session.create(input), session.create(input)], { concurrency: "unbounded" }) @@ -341,7 +341,7 @@ describe("SessionV2.create", () => { it.effect("returns the current Session projection after updates", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const { db } = yield* Database.Service const input = { id, location } const created = yield* session.create(input) @@ -354,12 +354,12 @@ describe("SessionV2.create", () => { it.effect("returns the current Session projection after projected updates", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const input = { id, location } const created = yield* session.create(input) - yield* events.publish(SessionV1.Event.Updated, { + yield* bus.publish(SessionV1.Event.Updated, { sessionID: id, info: SessionV1.SessionInfo.make({ id, @@ -379,19 +379,19 @@ describe("SessionV2.create", () => { it.effect("persists creation through the existing legacy created event", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const { db } = yield* Database.Service const created = yield* session.create({ location }) expect( yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, created.id)).all().pipe(Effect.orDie), - ).toMatchObject([{ type: EventV2.versionedType(SessionV1.Event.Created.type, 1) }]) + ).toMatchObject([{ type: Bus.versionedType(SessionV1.Event.Created.type, 1) }]) }), ) it.effect("persists caller-ID creation through the existing created event", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const { db } = yield* Database.Service const created = yield* session.create({ id, location }) @@ -403,10 +403,10 @@ describe("SessionV2.create", () => { }), ) - it.effect("omits legacy creation rows from the V2 Session event stream", () => + it.effect("omits legacy creation rows from the Session event stream", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const created = yield* session.create({ location }) yield* session.prompt({ @@ -414,7 +414,7 @@ describe("SessionV2.create", () => { text: "Hello", resume: false, }) - yield* SessionPending.promote(db, events, created.id, "steer") + yield* SessionPending.promote(db, bus, created.id, "steer") expect( Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(2), Stream.runCollect)), @@ -431,10 +431,10 @@ describe("SessionV2.create", () => { it.effect("replays one prompt lifecycle into a fresh target database", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const sourceEvents = yield* EventV2.Service + const session = yield* Session.Service + const sourceEvents = yield* Bus.Service const sourceDb = (yield* Database.Service).db - const created = yield* session.create({ id: SessionV2.ID.make("ses_fresh_target_replay"), location }) + const created = yield* session.create({ id: Session.ID.make("ses_fresh_target_replay"), location }) const admitted = yield* session.prompt({ sessionID: created.id, text: "Replay lifecycle", @@ -462,22 +462,22 @@ describe("SessionV2.create", () => { ) const targetDatabase = Database.layer({ path: path.join(tmp.path, "target.sqlite") }) const targetLayer = AppNodeBuilder.build( - LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node]), + LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node]), [[Database.node, targetDatabase]], ) yield* Effect.gen(function* () { const db = (yield* Database.Service).db - const events = yield* EventV2.Service + const bus = yield* Bus.Service const store = yield* SessionStore.Service yield* db .insert(ProjectTable) - .values({ id: ProjectV2.ID.global, worktree: location.directory, sandboxes: [] }) + .values({ id: Project.ID.global, worktree: location.directory, sandboxes: [] }) .run() .pipe(Effect.orDie) expect(yield* store.get(created.id)).toBeUndefined() - expect(yield* events.replayAll(serialized.slice(0, 2))).toBe(created.id) + expect(yield* bus.replayAll(serialized.slice(0, 2))).toBe(created.id) expect(yield* SessionPending.find(db, admitted.id)).toMatchObject({ id: admitted.id, sessionID: created.id, @@ -488,7 +488,7 @@ describe("SessionV2.create", () => { }) expect(yield* store.context(created.id)).toEqual([]) - expect(yield* events.replayAll(serialized.slice(2))).toBe(created.id) + expect(yield* bus.replayAll(serialized.slice(2))).toBe(created.id) expect(yield* SessionPending.find(db, admitted.id)).toBeUndefined() expect(yield* store.context(created.id)).toMatchObject([ { id: admitted.id, type: "user", text: "Replay lifecycle" }, @@ -502,9 +502,9 @@ describe("SessionV2.create", () => { .all() .pipe(Effect.orDie)).map((event) => [event.seq, event.type]), ).toEqual([ - [0, EventV2.versionedType(SessionV1.Event.Created.type, 1)], - [1, EventV2.versionedType(SessionEvent.InputAdmitted.type, 1)], - [2, EventV2.versionedType(SessionEvent.InputPromoted.type, 1)], + [0, Bus.versionedType(SessionV1.Event.Created.type, 1)], + [1, Bus.versionedType(SessionEvent.InputAdmitted.type, 1)], + [2, Bus.versionedType(SessionEvent.InputPromoted.type, 1)], ]) }).pipe(Effect.provide(Layer.fresh(targetLayer))) }), @@ -512,8 +512,8 @@ describe("SessionV2.create", () => { it.effect("does not mask unrelated created projector defects", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const event = yield* EventV2.Service + const session = yield* Session.Service + const event = yield* Bus.Service const defect = new Error("unrelated projector defect") yield* event.project(SessionV1.Event.Created, () => Effect.die(defect)) @@ -524,7 +524,7 @@ describe("SessionV2.create", () => { it.live("runs a shell command and projects the started/ended shell message", () => withTmp((directory) => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const created = yield* session.create({ location: Location.Ref.make({ directory: AbsolutePath.make(directory) }), }) @@ -544,7 +544,7 @@ describe("SessionV2.create", () => { it.live("still emits shell ended for a failing command", () => withTmp((directory) => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const created = yield* session.create({ location: Location.Ref.make({ directory: AbsolutePath.make(directory) }), }) @@ -562,10 +562,10 @@ describe("SessionV2.create", () => { it.effect("switches the selected agent through the durable Session event", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const created = yield* session.create({ location }) - yield* session.switchAgent({ sessionID: created.id, agent: AgentV2.ID.make("plan") }) + yield* session.switchAgent({ sessionID: created.id, agent: Agent.ID.make("plan") }) expect(yield* session.get(created.id)).toMatchObject({ agent: "plan" }) expect( @@ -576,11 +576,11 @@ describe("SessionV2.create", () => { it.effect("rejects an agent switch for a missing Session", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const missing = SessionV2.ID.make("ses_missing_agent_switch") + const session = yield* Session.Service + const missing = Session.ID.make("ses_missing_agent_switch") expect( - yield* session.switchAgent({ sessionID: missing, agent: AgentV2.ID.make("plan") }).pipe( + yield* session.switchAgent({ sessionID: missing, agent: Agent.ID.make("plan") }).pipe( Effect.flip, Effect.map((error) => error._tag), ), @@ -590,28 +590,28 @@ describe("SessionV2.create", () => { it.effect("switches the selected model through the durable Session event", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const created = yield* session.create({ location }) - const model = ModelV2.Ref.make({ - id: ModelV2.ID.make("sonnet"), - providerID: ProviderV2.ID.anthropic, - variant: ModelV2.VariantID.make("high"), + const model = Model.Ref.make({ + id: Model.ID.make("sonnet"), + providerID: Provider.ID.anthropic, + variant: Model.VariantID.make("high"), }) yield* session.switchModel({ sessionID: created.id, model }) expect(yield* session.get(created.id)).toMatchObject({ model }) - const events = Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(1), Stream.runCollect)) - expect(events).toMatchObject([{ type: "session.model.selected" }]) - expect(events[0]?.data).toEqual({ sessionID: created.id, model }) + const bus = Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(1), Stream.runCollect)) + expect(bus).toMatchObject([{ type: "session.model.selected" }]) + expect(bus[0]?.data).toEqual({ sessionID: created.id, model }) }), ) it.effect("ignores a model switch when the selected model is unchanged", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const created = yield* session.create({ location }) - const model = ModelV2.Ref.make({ id: ModelV2.ID.make("sonnet"), providerID: ProviderV2.ID.anthropic }) + const model = Model.Ref.make({ id: Model.ID.make("sonnet"), providerID: Provider.ID.anthropic }) yield* session.switchModel({ sessionID: created.id, model }) yield* session.switchModel({ sessionID: created.id, model }) @@ -626,13 +626,13 @@ describe("SessionV2.create", () => { it.effect("treats an omitted variant as the default variant", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const model = ModelV2.Ref.make({ id: ModelV2.ID.make("sonnet"), providerID: ProviderV2.ID.anthropic }) + const session = yield* Session.Service + const model = Model.Ref.make({ id: Model.ID.make("sonnet"), providerID: Provider.ID.anthropic }) const created = yield* session.create({ location, model }) yield* session.switchModel({ sessionID: created.id, - model: ModelV2.Ref.make({ ...model, variant: ModelV2.VariantID.make("default") }), + model: Model.Ref.make({ ...model, variant: Model.VariantID.make("default") }), }) const { db } = yield* Database.Service @@ -644,14 +644,14 @@ describe("SessionV2.create", () => { it.effect("rejects a model switch for a missing Session", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const missing = SessionV2.ID.make("ses_missing_model_switch") + const session = yield* Session.Service + const missing = Session.ID.make("ses_missing_model_switch") expect( yield* session .switchModel({ sessionID: missing, - model: ModelV2.Ref.make({ id: ModelV2.ID.make("sonnet"), providerID: ProviderV2.ID.anthropic }), + model: Model.Ref.make({ id: Model.ID.make("sonnet"), providerID: Provider.ID.anthropic }), }) .pipe( Effect.flip, diff --git a/packages/core/test/session-error.test.ts b/packages/core/test/session-error.test.ts index 825910f7a48b..0b0ed9d905b6 100644 --- a/packages/core/test/session-error.test.ts +++ b/packages/core/test/session-error.test.ts @@ -15,8 +15,8 @@ import { UnknownProviderReason, ToolFailure, } from "@opencode-ai/ai" -import { PermissionV2 } from "@opencode-ai/core/permission" -import { Tool } from "@opencode-ai/plugin/v2/effect/tool" +import { Permission } from "@opencode-ai/core/permission" +import { Tool } from "@opencode-ai/schema/tool" import { toSessionError } from "@opencode-ai/core/session/to-session-error" import { SessionRunnerRetry } from "@opencode-ai/core/session/runner/retry" @@ -56,7 +56,7 @@ describe("toSessionError", () => { }) test("preserves the permission rejection type without exposing internal fields", () => { - const blocked = new PermissionV2.BlockedError({ rules: [], permission: "external_directory", resources: [] }) + const blocked = new Permission.BlockedError({ rules: [], permission: "external_directory", resources: [] }) expect(toSessionError(blocked)).toEqual({ type: "permission.rejected", message: "Permission denied: external_directory", @@ -65,7 +65,7 @@ describe("toSessionError", () => { type: "permission.rejected", message: "Permission denied: external_directory", }) - expect(toSessionError(new Tool.Failure({ message: "failed" }))).toEqual({ + expect(toSessionError(new Tool.Error({ message: "failed" }))).toEqual({ type: "tool.execution", message: "failed", }) diff --git a/packages/core/test/session-execution.test.ts b/packages/core/test/session-execution.test.ts index 9ff3cc0c54d6..09920eae7c92 100644 --- a/packages/core/test/session-execution.test.ts +++ b/packages/core/test/session-execution.test.ts @@ -3,24 +3,23 @@ import { LLMError, TransportReason } from "@opencode-ai/ai" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { LocationServiceMap } from "@opencode-ai/core/location-service-map" import type { LocationServices } from "@opencode-ai/core/location-services" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionExecution } from "@opencode-ai/core/session/execution" import { SessionRestart } from "@opencode-ai/core/session/execution/restart" import { UserInterruptedError } from "@opencode-ai/core/session/error" import { SessionRunner } from "@opencode-ai/core/session/runner" import { SessionTable } from "@opencode-ai/core/session/sql" import { SessionStore } from "@opencode-ai/core/session/store" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { Context, Deferred, Effect, Exit, Fiber, Layer, LayerMap, Scope } from "effect" import { testEffect } from "./lib/effect" -const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SessionStore.node]))) +const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SessionStore.node]))) describe("SessionExecution lifecycle", () => { test("classifies success and typed failure terminals", () => { @@ -36,11 +35,6 @@ describe("SessionExecution lifecycle", () => { ), ), ).toEqual({ type: "failed", error: { type: "provider.transport", message: "Disconnected" } }) - const storage = new ToolOutputStore.StorageError({ operation: "encode", cause: new Error("invalid output") }) - expect(SessionExecution.terminal(Exit.fail(storage))).toEqual({ - type: "failed", - error: { type: "unknown", message: storage.message }, - }) }) test("defaults owner-scope interruption to shutdown and preserves explicit reasons", () => { @@ -58,8 +52,8 @@ describe("SessionExecution lifecycle", () => { Effect.gen(function* () { const database = yield* Database.Service const store = yield* SessionStore.Service - const first = SessionV2.ID.make("ses_recover_first") - const second = SessionV2.ID.make("ses_recover_second") + const first = Session.ID.make("ses_recover_first") + const second = Session.ID.make("ses_recover_second") yield* seedSessions(database, [first, second], { time_suspended: Date.now() }) expect(yield* store.consumeSuspended(first)).toBe(true) @@ -72,8 +66,8 @@ describe("SessionExecution lifecycle", () => { it.effect("suspension survives teardown interruption and clears when a drain finishes on its own", () => Effect.gen(function* () { const database = yield* Database.Service - const interrupted = SessionV2.ID.make("ses_suspend_interrupted") - const completed = SessionV2.ID.make("ses_suspend_completed") + const interrupted = Session.ID.make("ses_suspend_interrupted") + const completed = Session.ID.make("ses_suspend_completed") yield* seedSessions(database, [interrupted, completed]) const draining = yield* Deferred.make() @@ -108,11 +102,11 @@ describe("SessionExecution lifecycle", () => { it.effect("starts every suspended execution without waiting for earlier drains to finish", () => Effect.gen(function* () { const database = yield* Database.Service - const sessionIDs = Array.from({ length: 5 }, (_, index) => SessionV2.ID.make(`ses_resume_concurrent_${index}`)) + const sessionIDs = Array.from({ length: 5 }, (_, index) => Session.ID.make(`ses_resume_concurrent_${index}`)) yield* seedSessions(database, sessionIDs, { time_suspended: Date.now() }) const fourStarted = yield* Deferred.make() - const started: SessionV2.ID[] = [] + const started: Session.ID[] = [] const scope = yield* Scope.make() yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void)) const context = yield* buildExecution(scope, ({ sessionID }) => @@ -133,8 +127,8 @@ describe("SessionExecution lifecycle", () => { it.effect("resumes each suspended Session at most once", () => Effect.gen(function* () { const database = yield* Database.Service - const first = SessionV2.ID.make("ses_resume_first") - const second = SessionV2.ID.make("ses_resume_second") + const first = Session.ID.make("ses_resume_first") + const second = Session.ID.make("ses_resume_second") yield* seedSessions(database, [first, second], { time_suspended: Date.now() }) const drained: string[] = [] @@ -157,7 +151,7 @@ describe("SessionExecution lifecycle", () => { function seedSessions( database: Database.Service["Service"], - sessionIDs: ReadonlyArray, + sessionIDs: ReadonlyArray, values: { time_suspended?: number } = {}, ) { return Effect.gen(function* () { @@ -199,7 +193,7 @@ function suspensions(database: Database.Service["Service"]) { function buildExecution(scope: Scope.Closeable, drain: SessionRunner.Interface["drain"]) { return Effect.gen(function* () { const database = yield* Database.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const store = yield* SessionStore.Service const runner = Layer.succeed(SessionRunner.Service, SessionRunner.Service.of({ drain })) const locations = Layer.effect( @@ -215,7 +209,7 @@ function buildExecution(scope: Scope.Closeable, drain: SessionRunner.Interface[" SessionRestart.layer.pipe( Layer.provideMerge(SessionExecution.layer), Layer.provide(Layer.succeed(Database.Service, database)), - Layer.provide(Layer.succeed(EventV2.Service, events)), + Layer.provide(Layer.succeed(Bus.Service, bus)), Layer.provide(Layer.succeed(SessionStore.Service, store)), Layer.provide(locations), ), diff --git a/packages/core/test/session-generate.test.ts b/packages/core/test/session-generate.test.ts index a6910f826f70..ea12b528f247 100644 --- a/packages/core/test/session-generate.test.ts +++ b/packages/core/test/session-generate.test.ts @@ -1,22 +1,22 @@ import { expect } from "bun:test" import { LLMClient, LLMEvent, LLMResponse, Model, SystemPart, ToolDefinition, type LLMRequest } from "@opencode-ai/ai" import { OpenAIChat } from "@opencode-ai/ai/protocols" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { llmClient } from "@opencode-ai/core/effect/app-node-platform" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { EventTable } from "@opencode-ai/core/event/sql" import { InstructionDiscovery } from "@opencode-ai/core/instruction-discovery" import { Instructions } from "@opencode-ai/core/instructions" import { InstructionBuiltIns } from "@opencode-ai/core/instructions/builtins" import { Location } from "@opencode-ai/core/location" import { McpInstructions } from "@opencode-ai/core/mcp/instructions" -import { ModelV2 } from "@opencode-ai/core/model" +import { ID } from "@opencode-ai/core/model" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { ReferenceInstructions } from "@opencode-ai/core/reference/instructions" import { AbsolutePath } from "@opencode-ai/core/schema" import { SessionEvent } from "@opencode-ai/core/session/event" @@ -38,7 +38,7 @@ import { SessionStore } from "@opencode-ai/core/session/store" import { SkillInstructions } from "@opencode-ai/core/skill/instructions" import { PluginHooks } from "@opencode-ai/core/plugin/hooks" import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" +import { Tool } from "@opencode-ai/core/tool" import { asc, eq } from "drizzle-orm" import { Effect, Layer, Schema, Stream } from "effect" import { testEffect } from "./lib/effect" @@ -98,7 +98,7 @@ const skills = Layer.mock(SkillInstructions.Service, { load: () => Effect.succee const references = Layer.mock(ReferenceInstructions.Service, { load: () => Effect.succeed(Instructions.empty) }) const mcp = Layer.mock(McpInstructions.Service, { load: () => Effect.succeed(Instructions.empty) }) const plugins = Layer.mock(PluginSupervisor.Service, { flush: Effect.void }) -const tools = Layer.mock(ToolRegistry.Service, { +const tools = Layer.mock(Tool.Service, { snapshot: () => Effect.succeed({ codeModeCatalog: [ @@ -111,18 +111,17 @@ const tools = Layer.mock(ToolRegistry.Service, { definitions: [ToolDefinition.make({ name: "lookup", description: "Lookup", inputSchema: { type: "object" } })], execute: () => Effect.die(new Error("unused")), }), - register: () => Effect.die(new Error("unused")), - registerBatch: () => Effect.die(new Error("unused")), + transform: () => Effect.die(new Error("unused")), }) const it = testEffect( AppNodeBuilder.build( LayerNode.group([ Database.node, - EventV2.node, + Bus.node, SessionProjector.node, SessionStore.node, - AgentV2.node, + Agent.node, InstructionBuiltIns.node, PluginHooks.node, SessionGenerateNode.node, @@ -136,7 +135,7 @@ const it = testEffect( [ReferenceInstructions.node, references], [McpInstructions.node, mcp], [PluginSupervisor.node, plugins], - [ToolRegistry.node, tools], + [Tool.node, tools], [Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })], ], ), @@ -144,8 +143,8 @@ const it = testEffect( const durableState = (db: Database.Interface["db"], sessionID: SessionSchema.ID) => Effect.all({ - sequence: EventV2.latestSequence(db, sessionID), - events: db + sequence: Bus.latestSequence(db, sessionID), + bus: db .select() .from(EventTable) .where(eq(EventTable.aggregate_id, sessionID)) @@ -185,11 +184,11 @@ const userTexts = (request: LLMRequest) => const setup = Effect.gen(function* () { const { db } = yield* Database.Service - const events = yield* EventV2.Service - const agents = yield* AgentV2.Service + const bus = yield* Bus.Service + const agents = yield* Agent.Service const instructionBuiltIns = yield* InstructionBuiltIns.Service yield* agents.transform((draft) => - draft.update(AgentV2.ID.make("build"), (agent) => { + draft.update(Agent.ID.make("build"), (agent) => { agent.mode = "primary" }), ) @@ -207,71 +206,71 @@ const setup = Effect.gen(function* () { directory: "/project", title: "Generate test", version: "test", - agent: AgentV2.ID.make("build"), + agent: Agent.ID.make("build"), }) .run() .pipe(Effect.orDie) - return { db, events, instructions: yield* instructionBuiltIns.load(sessionID) } + return { db, bus, instructions: yield* instructionBuiltIns.load(sessionID) } }) it.effect("generates from fresh settled Session context without durable mutation", () => Effect.gen(function* () { requests.length = 0 instruction = "Initial context" - const { db, events, instructions } = yield* setup - yield* InstructionState.prepare(db, events, instructions, sessionID) + const { db, bus, instructions } = yield* setup + yield* InstructionState.prepare(db, bus, instructions, sessionID) const existing = SessionMessage.ID.create() - yield* events.publish(SessionEvent.InputAdmitted, { + yield* bus.publish(SessionEvent.InputAdmitted, { sessionID, inputID: existing, input: { type: "user", data: { text: "Existing durable context" }, delivery: "steer" }, }) - yield* events.publish(SessionEvent.InputPromoted, { sessionID, inputID: existing }) + yield* bus.publish(SessionEvent.InputPromoted, { sessionID, inputID: existing }) const settledAssistant = SessionMessage.ID.create() - yield* events.publish(SessionEvent.Step.Started, { + yield* bus.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: settledAssistant, - agent: AgentV2.ID.make("build"), - model: { id: ModelV2.ID.make("generate-model"), providerID: ProviderV2.ID.make("test") }, + agent: Agent.ID.make("build"), + model: { id: ID.make("generate-model"), providerID: Provider.ID.make("test") }, }) - yield* events.publish(SessionEvent.Text.Started, { + yield* bus.publish(SessionEvent.Text.Started, { sessionID, assistantMessageID: settledAssistant, ordinal: 0, }) - yield* events.publish(SessionEvent.Text.Ended, { + yield* bus.publish(SessionEvent.Text.Ended, { sessionID, assistantMessageID: settledAssistant, ordinal: 0, text: "Settled partial answer", }) const activeAssistant = SessionMessage.ID.create() - yield* events.publish(SessionEvent.Step.Started, { + yield* bus.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: activeAssistant, - agent: AgentV2.ID.make("build"), - model: { id: ModelV2.ID.make("generate-model"), providerID: ProviderV2.ID.make("test") }, + agent: Agent.ID.make("build"), + model: { id: ID.make("generate-model"), providerID: Provider.ID.make("test") }, }) - yield* events.publish(SessionEvent.Tool.Input.Started, { + yield* bus.publish(SessionEvent.Tool.Input.Started, { sessionID, assistantMessageID: activeAssistant, callID: "active-call", name: "echo", }) - yield* events.publish(SessionEvent.Tool.Input.Ended, { + yield* bus.publish(SessionEvent.Tool.Input.Ended, { sessionID, assistantMessageID: activeAssistant, callID: "active-call", text: "{}", }) - yield* events.publish(SessionEvent.Tool.Called, { + yield* bus.publish(SessionEvent.Tool.Called, { sessionID, assistantMessageID: activeAssistant, callID: "active-call", input: {}, executed: false, }) - yield* events.publish(SessionEvent.InputAdmitted, { + yield* bus.publish(SessionEvent.InputAdmitted, { sessionID, inputID: SessionMessage.ID.create(), input: { type: "user", data: { text: "Queued input must remain invisible" }, delivery: "queue" }, diff --git a/packages/core/test/session-instructions.test.ts b/packages/core/test/session-instructions.test.ts index 9d40f95f4b56..bee32501ec80 100644 --- a/packages/core/test/session-instructions.test.ts +++ b/packages/core/test/session-instructions.test.ts @@ -3,22 +3,22 @@ import fs from "fs/promises" import path from "path" import { DateTime, Effect, Layer } from "effect" import { Message } from "@opencode-ai/ai" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Config } from "@opencode-ai/core/config" import { Database } from "@opencode-ai/core/database/database" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { FSUtil } from "@opencode-ai/util/fs-util" import { Global } from "@opencode-ai/util/global" import { Image } from "@opencode-ai/core/image" import { Location } from "@opencode-ai/core/location" import { LocationMutation } from "@opencode-ai/core/location-mutation" -import { ModelV2 } from "@opencode-ai/core/model" -import { PermissionV2 } from "@opencode-ai/core/permission" -import { ProjectV2 } from "@opencode-ai/core/project" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { ReadTool } from "@opencode-ai/core/tool/read" +import { Model } from "@opencode-ai/core/model" +import { Permission } from "@opencode-ai/core/permission" +import { Project } from "@opencode-ai/core/project" +import { Provider } from "@opencode-ai/core/provider" +import { ReadTool } from "@opencode-ai/core/tool/plugin/read" import { ReadToolFileSystem } from "@opencode-ai/core/tool/read-filesystem" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionExecution } from "@opencode-ai/core/session/execution" @@ -26,11 +26,10 @@ import { SessionInstructions } from "@opencode-ai/core/session/instructions" import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionStore } from "@opencode-ai/core/session/store" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { toLLMMessages } from "@opencode-ai/core/session/runner/to-llm-message" -import { ToolHooks } from "@opencode-ai/core/tool/hooks" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" +import { PluginHooks } from "@opencode-ai/core/plugin/hooks" +import { Tool } from "@opencode-ai/core/tool" import { tempLocationLayer } from "./fixture/location" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { testEffect } from "./lib/effect" @@ -40,11 +39,11 @@ const readToolNode = makeLocationNode({ name: "test/read-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(ReadTool.Plugin)), deps: [ - ToolRegistry.toolsNode, + Tool.node, ReadToolFileSystem.node, LocationMutation.node, Image.node, - PermissionV2.node, + Permission.node, SessionInstructions.node, FSUtil.node, Location.node, @@ -52,17 +51,17 @@ const readToolNode = makeLocationNode({ }) const projects = Layer.succeed( - ProjectV2.Service, - ProjectV2.Service.of({ + Project.Service, + Project.Service.of({ list: () => Effect.succeed([]), - resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }), + resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory }), directories: () => Effect.succeed([]), commit: () => Effect.void, }), ) const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: () => Effect.void, ask: () => Effect.die("unused"), reply: () => Effect.die("unused"), @@ -77,41 +76,39 @@ const imageLayer = AppNodeBuilder.build(Image.node, [[Config.node, config]]) const testLayer = AppNodeBuilder.build( LayerNode.group([ Database.node, - EventV2.node, + Bus.node, SessionProjector.node, SessionStore.node, - SessionV2.node, + Session.node, Location.node, FSUtil.node, LocationMutation.node, ReadToolFileSystem.node, readToolNode, - ToolRegistry.node, - ToolRegistry.toolsNode, - ToolHooks.node, + Tool.node, + Tool.node, + PluginHooks.node, SessionInstructions.node, Global.node, - ToolOutputStore.node, Image.node, ]), [ - [ProjectV2.node, projects], + [Project.node, projects], [SessionExecution.node, SessionExecution.noopLayer], [Location.node, tempLocationLayer], - [PermissionV2.node, permission], + [Permission.node, permission], [Config.node, config], [Image.node, imageLayer], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], ], ) as unknown as Layer.Layer const it = testEffect(testLayer) const identity = { - agent: AgentV2.ID.make("build"), + agent: Agent.ID.make("build"), messageID: SessionMessage.ID.make("msg_nearby"), } -const readCall = (sessionID: SessionV2.ID, id: string, readPath: string): ToolRegistry.ExecuteInput => ({ +const readCall = (sessionID: Session.ID, id: string, readPath: string): Parameters[0] => ({ sessionID, ...identity, call: { type: "tool-call", id, name: "read", input: { path: readPath } }, @@ -120,7 +117,7 @@ const readCall = (sessionID: SessionV2.ID, id: string, readPath: string): ToolRe const writeAgents = (file: string, content: string) => Effect.promise(() => fs.writeFile(file, content)) const mkdir = (dir: string) => Effect.promise(() => fs.mkdir(dir, { recursive: true })) -const synthetics = (sessionID: SessionV2.ID) => +const synthetics = (sessionID: Session.ID) => Effect.gen(function* () { const store = yield* SessionStore.Service return (yield* store.context(sessionID)).filter((message) => message.type === "synthetic") @@ -128,10 +125,10 @@ const synthetics = (sessionID: SessionV2.ID) => // Seed a prior synthetic message with an instruction dedup ledger, simulating a prior turn // after the Location layer was reopened (in-memory set empty). -const seedSynthetic = (sessionID: SessionV2.ID, paths: string[]) => +const seedSynthetic = (sessionID: Session.ID, paths: string[]) => Effect.gen(function* () { - const events = yield* EventV2.Service - yield* events.publish(SessionEvent.Synthetic, { + const bus = yield* Bus.Service + yield* bus.publish(SessionEvent.Synthetic, { sessionID, text: `Instructions from: ${paths[0]}\nprior`, description: `Loaded ${paths[0]}`, @@ -157,8 +154,8 @@ describe("SessionInstructions", () => { yield* Effect.promise(() => fs.writeFile(path.resolve(dir, "sub", "deep", "file.txt"), "file content")) yield* Effect.promise(() => fs.writeFile(path.resolve(dir, "sub", "other", "file2.txt"), "file content 2")) - const session = yield* SessionV2.Service - const registry = yield* ToolRegistry.Service + const session = yield* Session.Service + const registry = yield* Tool.Service const sessionID = (yield* session.create({ location: Location.Ref.make({ directory: dir }) })).id // A read deep under sub/ discovers deep and sub AGENTS.md, walking up to but @@ -201,8 +198,8 @@ describe("SessionInstructions", () => { yield* writeAgents(subPath, "sub-instructions") yield* Effect.promise(() => fs.writeFile(path.resolve(dir, "sub", "file.txt"), "content")) - const session = yield* SessionV2.Service - const registry = yield* ToolRegistry.Service + const session = yield* Session.Service + const registry = yield* Tool.Service const sessionID = (yield* session.create({ location: Location.Ref.make({ directory: dir }) })).id // Seed the durable history with a prior synthetic that already claims sub's AGENTS.md @@ -230,8 +227,8 @@ describe("SessionInstructions", () => { yield* writeAgents(pkgPath, "pkg-instructions") yield* Effect.promise(() => fs.writeFile(path.resolve(dir, "packages", "foo", "file.txt"), "content")) - const session = yield* SessionV2.Service - const registry = yield* ToolRegistry.Service + const session = yield* Session.Service + const registry = yield* Tool.Service const sessionID = (yield* session.create({ location: Location.Ref.make({ directory: dir }) })).id // Listing packages/foo/ discovers its own AGENTS.md, walking up to but excluding @@ -263,8 +260,8 @@ describe("SessionInstructions", () => { yield* writeAgents(rootPath, "root-instructions") yield* writeAgents(subPath, "sub-instructions") - const session = yield* SessionV2.Service - const registry = yield* ToolRegistry.Service + const session = yield* Session.Service + const registry = yield* Tool.Service const sessionID = (yield* session.create({ location: Location.Ref.make({ directory: dir }) })).id // The walk starts and stops at the Location root: the root AGENTS.md is searched but @@ -283,7 +280,7 @@ describe("SessionInstructions", () => { yield* mkdir(path.resolve(dir, "sub")) yield* writeAgents(subPath, "sub-instructions") - const session = yield* SessionV2.Service + const session = yield* Session.Service const sessionInstructions = yield* SessionInstructions.Service const sessionID = (yield* session.create({ location: Location.Ref.make({ directory: dir }) })).id @@ -299,7 +296,7 @@ describe("SessionInstructions", () => { test("toLLMMessages does not forward synthetic metadata to the provider", () => { const created = DateTime.makeUnsafe(0) - const model = ModelV2.Ref.make({ id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }) + const model = Model.Ref.make({ id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }) const synthetic = SessionMessage.Synthetic.make({ id: SessionMessage.ID.make("msg_synthetic"), type: "synthetic", diff --git a/packages/core/test/session-log.test.ts b/packages/core/test/session-log.test.ts index 870f6acf02af..f223542cafb4 100644 --- a/packages/core/test/session-log.test.ts +++ b/packages/core/test/session-log.test.ts @@ -1,15 +1,16 @@ import { describe, expect } from "bun:test" import { Effect, Fiber, Layer, Schema, Stream } from "effect" import { Database } from "@opencode-ai/core/database/database" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" import { Location } from "@opencode-ai/core/location" -import { ProjectV2 } from "@opencode-ai/core/project" +import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionExecution } from "@opencode-ai/core/session/execution" import { SessionStore } from "@opencode-ai/core/session/store" @@ -17,35 +18,35 @@ import { SessionTable } from "@opencode-ai/core/session/sql" import { testEffect } from "./lib/effect" const projects = Layer.succeed( - ProjectV2.Service, - ProjectV2.Service.of({ + Project.Service, + Project.Service.of({ list: () => Effect.succeed([]), - resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }), + resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory }), directories: () => Effect.succeed([]), commit: () => Effect.void, }), ) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]), + LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Session.node]), [ - [ProjectV2.node, projects], + [Project.node, projects], [SessionExecution.node, SessionExecution.noopLayer], ], ), ) const location = Location.Ref.make({ directory: AbsolutePath.make("/project") }) -describe("SessionV2.log", () => { +describe("Session.log", () => { it.effect("replays public session events and marks synced at the aggregate watermark", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const created = yield* session.create({ location }) yield* session.rename({ sessionID: created.id, title: "session.renamed" }) const items = Array.from(yield* Stream.runCollect(session.log({ sessionID: created.id }))) - const watermark = (yield* events.sequences([created.id])).get(created.id) + const watermark = (yield* bus.sequences([created.id])).get(created.id) // Session creation commits a non-public durable event, so the marker's // seq covers more of the aggregate than the public events emitted. @@ -56,7 +57,7 @@ describe("SessionV2.log", () => { it.effect("continues with live public events when following", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const created = yield* session.create({ location }) const fiber = yield* session .log({ sessionID: created.id, follow: true }) @@ -72,52 +73,52 @@ describe("SessionV2.log", () => { it.effect("fails with NotFound for an unknown session", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const error = yield* Effect.flip(Stream.runCollect(session.log({ sessionID: SessionV2.ID.create() }))) + const session = yield* Session.Service + const error = yield* Effect.flip(Stream.runCollect(session.log({ sessionID: Session.ID.create() }))) expect(error._tag).toBe("Session.NotFoundError") }), ) it.effect("reads across undecodable gaps in aggregate order and marks the true log position", () => Effect.gen(function* () { - const GapEvent = EventV2.durable({ + const GapEvent = Bus.durable({ type: "test.session.log.gap", durable: { aggregate: "sessionID", version: 1 }, - schema: { sessionID: SessionV2.ID, value: Schema.String }, + schema: { sessionID: Session.ID, value: Schema.String }, }) - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const created = yield* session.create({ location }) - yield* session.switchAgent({ sessionID: created.id, agent: AgentV2.ID.make("one") }) + yield* session.switchAgent({ sessionID: created.id, agent: Agent.ID.make("one") }) // Not in the durable manifest, so reads must skip it without failing. - yield* events.publish(GapEvent, { sessionID: created.id, value: "filtered" }) - yield* session.switchAgent({ sessionID: created.id, agent: AgentV2.ID.make("two") }) - yield* session.switchAgent({ sessionID: created.id, agent: AgentV2.ID.make("three") }) + yield* bus.publish(GapEvent, { sessionID: created.id, value: "filtered" }) + yield* session.switchAgent({ sessionID: created.id, agent: Agent.ID.make("two") }) + yield* session.switchAgent({ sessionID: created.id, agent: Agent.ID.make("three") }) const items = Array.from(yield* Stream.runCollect(session.log({ sessionID: created.id, after: 1 }))) expect( - items.map((item): number | string | undefined => (EventV2.isSynced(item) ? item.type : item.durable?.seq)), + items.map((item): number | string | undefined => (Bus.isSynced(item) ? item.type : item.durable?.seq)), ).toEqual([3, 4, "log.synced"]) - expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID: created.id, seq: EventV2.Seq.make(4) }) + expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID: created.id, seq: Event.Seq.make(4) }) }), ) it.effect("completes with a bare synced marker for a migrated Session with no event sequence", () => Effect.gen(function* () { const db = (yield* Database.Service).db - const session = yield* SessionV2.Service - const sessionID = SessionV2.ID.make("ses_empty_log") + const session = yield* Session.Service + const sessionID = Session.ID.make("ses_empty_log") yield* db .insert(ProjectTable) - .values({ id: ProjectV2.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) + .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) .onConflictDoNothing() .run() yield* db .insert(SessionTable) .values({ id: sessionID, - project_id: ProjectV2.ID.global, + project_id: Project.ID.global, slug: "empty-log", directory: "/project", title: "Empty log", diff --git a/packages/core/test/session-projector.test.ts b/packages/core/test/session-projector.test.ts index 18bdcdbd62ed..9891a0b9622c 100644 --- a/packages/core/test/session-projector.test.ts +++ b/packages/core/test/session-projector.test.ts @@ -2,17 +2,18 @@ import { describe, expect } from "bun:test" import { DateTime, Effect, Fiber, Option, Schema, Stream } from "effect" import { asc, eq, sql } from "drizzle-orm" import { Database } from "@opencode-ai/core/database/database" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" import { EventTable } from "@opencode-ai/core/event/sql" -import { ModelV2 } from "@opencode-ai/core/model" +import { Model } from "@opencode-ai/core/model" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionMessage } from "@opencode-ai/core/session/message" import { Money } from "@opencode-ai/schema/money" @@ -31,14 +32,14 @@ import { import { testEffect } from "./lib/effect" import { Snapshot } from "@opencode-ai/core/snapshot" -const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SessionProjector.node]))) -const sessionsLayer = AppNodeBuilder.build(SessionV2.node, [[SessionExecution.node, SessionExecution.noopLayer]]) -const sessionID = SessionV2.ID.make("ses_projector_test") +const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SessionProjector.node]))) +const sessionsLayer = AppNodeBuilder.build(Session.node, [[SessionExecution.node, SessionExecution.noopLayer]]) +const sessionID = Session.ID.make("ses_projector_test") const created = DateTime.makeUnsafe(0) -const model = { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") } -const previousModel = { ...model, variant: ModelV2.VariantID.make("medium") } +const model = { id: Model.ID.make("model"), providerID: Provider.ID.make("provider") } +const previousModel = { ...model, variant: Model.VariantID.make("medium") } const encodeMessage = Schema.encodeSync(SessionMessage.Info) -const build = AgentV2.defaultID +const build = Agent.defaultID const assistantRow = ( id: SessionMessage.ID, @@ -75,11 +76,11 @@ describe("SessionProjector", () => { version: "test", }) .run() - const events = yield* EventV2.Service + const bus = yield* Bus.Service const inputID = SessionMessage.ID.make("msg_manual_compaction") - yield* SessionPending.admitCompaction(db, events, { id: inputID, sessionID }) + yield* SessionPending.admitCompaction(db, bus, { id: inputID, sessionID }) - yield* events.publish(SessionEvent.Compaction.Failed, { + yield* bus.publish(SessionEvent.Compaction.Failed, { sessionID, reason: "auto", error: { type: "compaction.failed", message: "Auto compaction failed" }, @@ -143,7 +144,7 @@ describe("SessionProjector", () => { yield* SessionMessageUpdater.update( SessionMessageUpdater.memory(state), SessionEvent.Compaction.Delta.make({ - id: EventV2.ID.make("evt_delta"), + id: Event.ID.make("evt_delta"), type: "session.compaction.delta", created, data: { sessionID, text: "summary" }, @@ -213,8 +214,8 @@ describe("SessionProjector", () => { current_values: {}, }) .run() - const events = yield* EventV2.Service - yield* events.publish(SessionEvent.RevertEvent.Staged, { + const bus = yield* Bus.Service + yield* bus.publish(SessionEvent.RevertEvent.Staged, { sessionID, revert: { messageID: boundary, snapshot: Snapshot.ID.make("tree"), files: [] }, }) @@ -223,13 +224,13 @@ describe("SessionProjector", () => { snapshot: "tree", files: [], }) - yield* events.publish(SessionEvent.RevertEvent.Cleared, { sessionID }) + yield* bus.publish(SessionEvent.RevertEvent.Cleared, { sessionID }) expect((yield* db.select({ revert: SessionTable.revert }).from(SessionTable).get())?.revert).toBeNull() - yield* events.publish(SessionEvent.RevertEvent.Staged, { + yield* bus.publish(SessionEvent.RevertEvent.Staged, { sessionID, revert: { messageID: boundary, files: [] }, }) - yield* events.publish(SessionEvent.RevertEvent.Committed, { + yield* bus.publish(SessionEvent.RevertEvent.Committed, { sessionID, to: boundary, }) @@ -269,36 +270,36 @@ describe("SessionProjector", () => { }) .run() .pipe(Effect.orDie) - const events = yield* EventV2.Service + const bus = yield* Bus.Service - yield* events.publish(SessionEvent.InputAdmitted, { + yield* bus.publish(SessionEvent.InputAdmitted, { sessionID, inputID: SessionMessage.ID.make("msg_first"), input: { type: "user", data: { text: "first" }, delivery: "steer" }, }) - yield* events.publish( + yield* bus.publish( SessionEvent.InputPromoted, { sessionID, inputID: SessionMessage.ID.make("msg_first"), }, - { id: EventV2.ID.make("evt_z") }, + { id: Event.ID.make("evt_z") }, ) - yield* events.publish(SessionEvent.InputAdmitted, { + yield* bus.publish(SessionEvent.InputAdmitted, { sessionID, inputID: SessionMessage.ID.make("msg_second"), input: { type: "user", data: { text: "second" }, delivery: "steer" }, }) - yield* events.publish( + yield* bus.publish( SessionEvent.InputPromoted, { sessionID, inputID: SessionMessage.ID.make("msg_second"), }, - { id: EventV2.ID.make("evt_a") }, + { id: Event.ID.make("evt_a") }, ) - const sessions = yield* SessionV2.Service + const sessions = yield* Session.Service const firstPage = yield* sessions.messages({ sessionID, limit: 1, order: "asc" }) expect(firstPage.map((message) => (message.type === "user" ? message.text : message.type))).toEqual(["first"]) const secondPage = yield* sessions.messages({ @@ -342,16 +343,16 @@ describe("SessionProjector", () => { }) .run() .pipe(Effect.orDie) - const events = yield* EventV2.Service + const bus = yield* Bus.Service const id = SessionMessage.ID.make("msg_admitted") - const admitted = yield* SessionPending.admit(db, events, { + const admitted = yield* SessionPending.admit(db, bus, { id, sessionID, input: { type: "user", data: { text: "promote me" }, delivery: "steer" }, }) if (!admitted) return yield* Effect.die("Prompt admission failed") - const event = yield* events.publish(SessionEvent.InputPromoted, { + const event = yield* bus.publish(SessionEvent.InputPromoted, { sessionID, inputID: id, }) @@ -386,22 +387,22 @@ describe("SessionProjector", () => { }) .run() .pipe(Effect.orDie) - const events = yield* EventV2.Service + const bus = yield* Bus.Service - yield* events.publish(SessionEvent.AgentSelected, { + yield* bus.publish(SessionEvent.AgentSelected, { sessionID, agent: build, }) - yield* events.publish(SessionEvent.ModelSelected, { + yield* bus.publish(SessionEvent.ModelSelected, { sessionID, model, }) - yield* events.publish(SessionEvent.Synthetic, { + yield* bus.publish(SessionEvent.Synthetic, { sessionID, text: "synthetic context", metadata: { source: "projector-test" }, }) - yield* events.publish(SessionEvent.Shell.Started, { + yield* bus.publish(SessionEvent.Shell.Started, { sessionID, shell: Shell.Info.make({ id: Shell.ID.make("sh_projector"), @@ -414,7 +415,7 @@ describe("SessionProjector", () => { time: { started: 0 }, }), }) - yield* events.publish(SessionEvent.Shell.Ended, { + yield* bus.publish(SessionEvent.Shell.Ended, { sessionID, shell: Shell.Info.make({ id: Shell.ID.make("sh_projector"), @@ -429,12 +430,12 @@ describe("SessionProjector", () => { }), output: { output: "/project", cursor: 8, size: 8, truncated: false }, }) - yield* events.publish(SessionEvent.Compaction.Started, { + yield* bus.publish(SessionEvent.Compaction.Started, { sessionID, reason: "manual", recent: "recent context", }) - yield* events.publish(SessionEvent.Compaction.Delta, { + yield* bus.publish(SessionEvent.Compaction.Delta, { sessionID, text: "partial", }) @@ -454,7 +455,7 @@ describe("SessionProjector", () => { .all() .pipe(Effect.orDie), ).toEqual([{ data: expect.objectContaining({ status: "running", summary: "", recent: "recent context" }) }]) - yield* events.publish(SessionEvent.Compaction.Ended, { + yield* bus.publish(SessionEvent.Compaction.Ended, { sessionID, reason: "manual", text: "summary", @@ -525,7 +526,7 @@ describe("SessionProjector", () => { }) .run() .pipe(Effect.orDie) - const events = yield* EventV2.Service + const bus = yield* Bus.Service const id = SessionMessage.ID.make("msg_creator_collision") const { id: _, type, ...data } = encodeMessage({ id, type: "synthetic", text: "existing", time: { created } }) yield* db @@ -533,7 +534,7 @@ describe("SessionProjector", () => { .values({ id, session_id: sessionID, type, seq: 0, time_created: 0, data }) .run() - const exit = yield* events + const exit = yield* bus .publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: id, @@ -594,11 +595,11 @@ describe("SessionProjector", () => { }) .run() .pipe(Effect.orDie) - const events = yield* EventV2.Service + const bus = yield* Bus.Service const first = SessionMessage.ID.make("msg_retry_first") const second = SessionMessage.ID.make("msg_retry_second") - yield* events.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: first, agent: build, model }) - yield* events.publish(SessionEvent.RetryScheduled, { + yield* bus.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: first, agent: build, model }) + yield* bus.publish(SessionEvent.RetryScheduled, { sessionID, assistantMessageID: first, attempt: 2, @@ -619,15 +620,15 @@ describe("SessionProjector", () => { retry: { attempt: 2, at: DateTime.makeUnsafe(2_000), error: { type: "provider.transport" } }, }) - yield* events.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: second, agent: build, model }) - yield* events.publish(SessionEvent.RetryScheduled, { + yield* bus.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: second, agent: build, model }) + yield* bus.publish(SessionEvent.RetryScheduled, { sessionID, assistantMessageID: second, attempt: 3, at: 6_000, error: { type: "provider.internal", message: "Unavailable" }, }) - yield* events.publish(SessionEvent.Execution.Interrupted, { sessionID, reason: "shutdown" }) + yield* bus.publish(SessionEvent.Execution.Interrupted, { sessionID, reason: "shutdown" }) const rows = yield* db .select() @@ -661,7 +662,7 @@ describe("SessionProjector", () => { }) .run() .pipe(Effect.orDie) - const events = yield* EventV2.Service + const bus = yield* Bus.Service const suspended = () => db .select({ timeSuspended: SessionTable.time_suspended }) @@ -670,10 +671,10 @@ describe("SessionProjector", () => { .get() .pipe(Effect.orDie) - yield* events.publish(SessionEvent.Execution.Interrupted, { sessionID, reason: "shutdown" }) + yield* bus.publish(SessionEvent.Execution.Interrupted, { sessionID, reason: "shutdown" }) expect((yield* suspended())?.timeSuspended).toBeNull() - yield* events.publish(SessionEvent.Execution.Started, { sessionID }) + yield* bus.publish(SessionEvent.Execution.Started, { sessionID }) expect((yield* suspended())?.timeSuspended).toBeNull() }), ) @@ -707,7 +708,7 @@ describe("SessionProjector", () => { .run() .pipe(Effect.orDie) - const service = yield* EventV2.Service + const service = yield* Bus.Service const usageUpdated = yield* service .subscribe(SessionEvent.UsageUpdated) .pipe(Stream.runHead, Effect.forkScoped({ startImmediately: true })) @@ -787,7 +788,7 @@ describe("SessionProjector", () => { .run() .pipe(Effect.orDie) - const service = yield* EventV2.Service + const service = yield* Bus.Service yield* service.publish(SessionEvent.Text.Started, { sessionID, assistantMessageID: SessionMessage.ID.make("msg_assistant_completed"), diff --git a/packages/core/test/session-prompt.test.ts b/packages/core/test/session-prompt.test.ts index 8cab48f14a15..9f30efe9c063 100644 --- a/packages/core/test/session-prompt.test.ts +++ b/packages/core/test/session-prompt.test.ts @@ -6,18 +6,18 @@ import path from "path" import { pathToFileURL } from "url" import { eq } from "drizzle-orm" import { Database } from "@opencode-ai/core/database/database" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { EventTable } from "@opencode-ai/core/event/sql" import { SessionEvent } from "@opencode-ai/core/session/event" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionExecution } from "@opencode-ai/core/session/execution" @@ -29,10 +29,10 @@ import type { LocationServices } from "@opencode-ai/core/location-services" import { testEffect } from "./lib/effect" import { imagePassthrough } from "./lib/image" -const executionCalls: SessionV2.ID[] = [] -const interruptCalls: SessionV2.ID[] = [] -const wakeCalls: SessionV2.ID[] = [] -const activeSessions = new Set() +const executionCalls: Session.ID[] = [] +const interruptCalls: Session.ID[] = [] +const wakeCalls: Session.ID[] = [] +const activeSessions = new Set() const execution = Layer.succeed( SessionExecution.Service, SessionExecution.Service.of({ @@ -63,14 +63,14 @@ const locations = Layer.effect( ) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]), + LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Session.node]), [ [SessionExecution.node, execution], [LocationServiceMap.node, locations], ], ), ) -const sessionID = SessionV2.ID.make("ses_prompt_test") +const sessionID = Session.ID.make("ses_prompt_test") const messageID = SessionMessage.ID.create() const setup = Effect.gen(function* () { @@ -130,8 +130,8 @@ const assistantRow = (id: SessionMessage.ID, seq: number) => { SessionMessage.Assistant.make({ id, type: "assistant", - agent: AgentV2.ID.make("build"), - model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }, + agent: Agent.ID.make("build"), + model: { id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }, content: [], time: { created: DateTime.makeUnsafe(0) }, }), @@ -139,18 +139,18 @@ const assistantRow = (id: SessionMessage.ID, seq: number) => { return { id, session_id: sessionID, type, seq, time_created: 0, data } } -describe("SessionV2.prompt", () => { +describe("Session.prompt", () => { it.effect("exposes the execution registry", () => Effect.gen(function* () { activeSessions.add(sessionID) - expect(Array.from(yield* (yield* SessionV2.Service).active)).toEqual([sessionID]) + expect(Array.from(yield* (yield* Session.Service).active)).toEqual([sessionID]) }).pipe(Effect.ensuring(Effect.sync(() => activeSessions.clear()))), ) it.effect("delegates execution continuation through SessionExecution", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service executionCalls.length = 0 wakeCalls.length = 0 yield* session.resume(sessionID) @@ -162,7 +162,7 @@ describe("SessionV2.prompt", () => { it.effect("delegates process-local interruption through SessionExecution", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service interruptCalls.length = 0 yield* session.interrupt(sessionID) @@ -173,18 +173,18 @@ describe("SessionV2.prompt", () => { it.effect("delegates interruption without requiring a recorded Session", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service interruptCalls.length = 0 - yield* session.interrupt(SessionV2.ID.make("ses_missing")) - expect(interruptCalls).toEqual([SessionV2.ID.make("ses_missing")]) + yield* session.interrupt(Session.ID.make("ses_missing")) + expect(interruptCalls).toEqual([Session.ID.make("ses_missing")]) }), ) it.effect("durably admits one user message before transcript promotion", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const message = yield* session.prompt({ sessionID, @@ -207,8 +207,8 @@ describe("SessionV2.prompt", () => { it.effect("commits a staged revert before admitting a new prompt", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const boundary = yield* session.prompt({ @@ -216,10 +216,10 @@ describe("SessionV2.prompt", () => { text: "boundary", resume: false, }) - yield* SessionPending.promote(db, events, sessionID, "steer") + yield* SessionPending.promote(db, bus, sessionID, "steer") const stale = SessionMessage.ID.make("msg_stale_assistant") yield* db.insert(SessionMessageTable).values(assistantRow(stale, 100)).run().pipe(Effect.orDie) - yield* events.publish(SessionEvent.RevertEvent.Staged, { + yield* bus.publish(SessionEvent.RevertEvent.Staged, { sessionID, revert: { messageID: boundary.id, files: [] }, }) @@ -240,16 +240,16 @@ describe("SessionV2.prompt", () => { it.effect("holds synthetic input behind a staged revert and discards it when committed", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const boundary = yield* session.prompt({ sessionID, text: "boundary", resume: false, }) - yield* SessionPending.promote(db, events, sessionID, "steer") - yield* events.publish(SessionEvent.RevertEvent.Staged, { + yield* SessionPending.promote(db, bus, sessionID, "steer") + yield* bus.publish(SessionEvent.RevertEvent.Staged, { sessionID, revert: { messageID: boundary.id, files: [] }, }) @@ -269,7 +269,7 @@ describe("SessionV2.prompt", () => { it.effect("resolves attachment MIME before admission", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" @@ -298,7 +298,7 @@ describe("SessionV2.prompt", () => { it.effect("materializes selected source file content", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const directory = import.meta.dir const source = path.join(directory, "session-prompt.test.ts") const sourceUri = pathToFileURL(source) @@ -329,7 +329,7 @@ describe("SessionV2.prompt", () => { it.effect("materializes directories as directory attachments", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const uri = pathToFileURL(import.meta.dir).href const message = yield* session.prompt({ @@ -354,7 +354,7 @@ describe("SessionV2.prompt", () => { it.effect("materializes local image content before admission", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const directory = yield* Effect.acquireRelease( Effect.promise(() => mkdtemp(path.join(tmpdir(), "opencode-session-prompt-"))), (directory) => Effect.promise(() => rm(directory, { recursive: true, force: true })), @@ -389,7 +389,7 @@ describe("SessionV2.prompt", () => { it.effect("sniffs data URL content instead of trusting its declared MIME", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const uri = `data:video/mp2t;base64,${Buffer.from("export const value = 1\n").toString("base64")}` const message = yield* session.prompt({ @@ -413,7 +413,7 @@ describe("SessionV2.prompt", () => { it.effect("rejects malformed base64 data URLs", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const uri = "data:image/png;base64,not-base64" const error = yield* session @@ -436,19 +436,19 @@ describe("SessionV2.prompt", () => { it.effect("streams durable Session events after an aggregate sequence", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service - const publicEvents = (input: { sessionID: SessionV2.ID; after?: number }) => + const publicEvents = (input: { sessionID: Session.ID; after?: number }) => session .log({ ...input, follow: true }) - .pipe(Stream.filter((item): item is SessionEvent.DurableEvent => !EventV2.isSynced(item))) + .pipe(Stream.filter((item): item is SessionEvent.DurableEvent => !Bus.isSynced(item))) const fiber = yield* publicEvents({ sessionID }).pipe(Stream.take(4), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow yield* session.prompt({ sessionID, text: "First", resume: false }) yield* session.prompt({ sessionID, text: "Second", resume: false }) - yield* SessionPending.promote(db, events, sessionID, "steer") + yield* SessionPending.promote(db, bus, sessionID, "steer") const streamed = Array.from(yield* Fiber.join(fiber)) expect(streamed.map((event): [number | undefined, string] => [event.durable?.seq, event.type])).toEqual([ @@ -468,7 +468,7 @@ describe("SessionV2.prompt", () => { it.effect("resumes through a recorded message without appending another prompt", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const message = yield* session.prompt({ sessionID, text: "Fix the failing tests", @@ -489,7 +489,7 @@ describe("SessionV2.prompt", () => { it.effect("records distinct messages when the ID is omitted", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const input = { sessionID, text: "Fix the failing tests", resume: false } const first = yield* session.prompt(input) @@ -504,7 +504,7 @@ describe("SessionV2.prompt", () => { it.effect("returns the original recorded message when the ID is retried", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const input = { sessionID, id: messageID, @@ -524,7 +524,7 @@ describe("SessionV2.prompt", () => { it.effect("wakes execution when an exact prompt retry recovers a committed message", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const input = { sessionID, id: messageID, @@ -544,7 +544,7 @@ describe("SessionV2.prompt", () => { it.effect("rejects reuse of one ID with a different prompt", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service yield* session.prompt({ sessionID, @@ -569,7 +569,7 @@ describe("SessionV2.prompt", () => { it.effect("rejects reuse of one ID with a different delivery mode", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service yield* session.prompt({ id: messageID, @@ -594,7 +594,7 @@ describe("SessionV2.prompt", () => { it.effect("returns one recorded message to concurrent exact retries", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const input = { sessionID, id: messageID, @@ -607,7 +607,7 @@ describe("SessionV2.prompt", () => { expect(messages[1]).toEqual(messages[0]) expect(yield* session.messages({ sessionID })).toEqual([]) expect(yield* admittedCount).toBe(1) - expect(yield* eventCount(EventV2.versionedType(SessionEvent.InputAdmitted.type, 1))).toBe(1) + expect(yield* eventCount(Bus.versionedType(SessionEvent.InputAdmitted.type, 1))).toBe(1) }), ) @@ -615,8 +615,8 @@ describe("SessionV2.prompt", () => { Effect.gen(function* () { yield* setup const { db } = yield* Database.Service - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service yield* session.prompt({ id: messageID, sessionID, @@ -625,11 +625,11 @@ describe("SessionV2.prompt", () => { }) yield* Effect.all( - [SessionPending.promote(db, events, sessionID, "steer"), SessionPending.promote(db, events, sessionID, "steer")], + [SessionPending.promote(db, bus, sessionID, "steer"), SessionPending.promote(db, bus, sessionID, "steer")], { concurrency: "unbounded" }, ) - expect(yield* eventCount(EventV2.versionedType(SessionEvent.InputPromoted.type, 1))).toBe(1) + expect(yield* eventCount(Bus.versionedType(SessionEvent.InputPromoted.type, 1))).toBe(1) expect(yield* admitted(messageID)).toBeUndefined() expect(yield* session.messages({ sessionID })).toMatchObject([ { id: messageID, type: "user", text: "Promote once" }, @@ -641,8 +641,8 @@ describe("SessionV2.prompt", () => { Effect.gen(function* () { yield* setup const { db } = yield* Database.Service - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service wakeCalls.length = 0 yield* session.prompt({ id: messageID, @@ -659,7 +659,7 @@ describe("SessionV2.prompt", () => { .all() .pipe(Effect.orDie) - yield* events.remove(sessionID) + yield* bus.remove(sessionID) yield* db .delete(SessionPendingTable) .where(eq(SessionPendingTable.session_id, sessionID)) @@ -670,7 +670,7 @@ describe("SessionV2.prompt", () => { .where(eq(SessionMessageTable.session_id, sessionID)) .run() .pipe(Effect.orDie) - yield* events.replayAll( + yield* bus.replayAll( recorded.map((event) => ({ id: event.id, created: DateTime.makeUnsafe(event.created), @@ -700,8 +700,8 @@ describe("SessionV2.prompt", () => { Effect.gen(function* () { yield* setup const { db } = yield* Database.Service - const session = yield* SessionV2.Service - const other = SessionV2.ID.make("ses_prompt_other") + const session = yield* Session.Service + const other = Session.ID.make("ses_prompt_other") yield* db .insert(SessionTable) .values({ @@ -727,7 +727,7 @@ describe("SessionV2.prompt", () => { it.effect("rejects a prompt ID already used by visible Session history", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const { db } = yield* Database.Service const { id: _, @@ -762,7 +762,7 @@ describe("SessionV2.prompt", () => { it.effect("starts execution by default after recording the prompt", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service executionCalls.length = 0 wakeCalls.length = 0 @@ -776,7 +776,7 @@ describe("SessionV2.prompt", () => { it.effect("starts execution when resume is explicitly true", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service executionCalls.length = 0 wakeCalls.length = 0 @@ -794,7 +794,7 @@ describe("SessionV2.prompt", () => { it.effect("only records the prompt when resume is false", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service executionCalls.length = 0 wakeCalls.length = 0 @@ -808,7 +808,7 @@ describe("SessionV2.prompt", () => { it.effect("treats prompt metadata as durable retry identity", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const input = { id: messageID, sessionID, @@ -830,8 +830,8 @@ describe("SessionV2.prompt", () => { it.effect("durably admits synthetic input before transcript promotion", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const input = yield* session.synthetic({ @@ -855,7 +855,7 @@ describe("SessionV2.prompt", () => { }, }) - yield* SessionPending.promote(db, events, sessionID, "steer") + yield* SessionPending.promote(db, bus, sessionID, "steer") expect(yield* session.messages({ sessionID })).toMatchObject([ { @@ -872,15 +872,15 @@ describe("SessionV2.prompt", () => { it.effect("reconciles exact synthetic retries and rejects conflicting reuse", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const database = yield* Database.Service const input = { id: messageID, sessionID, text: "Completed", resume: false } const entries = yield* Effect.all([session.synthetic(input), session.synthetic(input)], { concurrency: "unbounded", }) - yield* SessionPending.promote(database.db, events, sessionID, "steer") + yield* SessionPending.promote(database.db, bus, sessionID, "steer") const promotedRetry = yield* session.synthetic(input) const failure = yield* session.synthetic({ ...input, text: "Different completion" }).pipe(Effect.flip) @@ -888,15 +888,15 @@ describe("SessionV2.prompt", () => { expect(promotedRetry).toMatchObject({ id: messageID, type: "synthetic", data: { text: "Completed" } }) expect(failure).toMatchObject({ _tag: "Session.SyntheticConflictError", sessionID, inputID: messageID }) expect(yield* admittedCount).toBe(0) - expect(yield* eventCount(EventV2.versionedType(SessionEvent.InputAdmitted.type, 1))).toBe(1) + expect(yield* eventCount(Bus.versionedType(SessionEvent.InputAdmitted.type, 1))).toBe(1) }), ) it.effect("keeps queued input pending until the idle boundary", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const input = yield* session.synthetic({ @@ -909,11 +909,11 @@ describe("SessionV2.prompt", () => { expect(input.delivery).toBe("queue") expect(yield* SessionPending.has(db, sessionID, "input")).toBe(true) expect( - yield* SessionPending.promote(db, events, sessionID, "steer"), + yield* SessionPending.promote(db, bus, sessionID, "steer"), ).toBe(0) expect(yield* session.messages({ sessionID })).toEqual([]) expect( - yield* SessionPending.promote(db, events, sessionID, "input"), + yield* SessionPending.promote(db, bus, sessionID, "input"), ).toBe(1) expect(yield* SessionPending.has(db, sessionID, "input")).toBe(false) expect(yield* session.messages({ sessionID })).toMatchObject([ @@ -925,8 +925,8 @@ describe("SessionV2.prompt", () => { it.effect("promotes prompt and synthetic steers in admission order", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service yield* session.prompt({ @@ -941,7 +941,7 @@ describe("SessionV2.prompt", () => { resume: false, }) - yield* SessionPending.promote(db, events, sessionID, "steer") + yield* SessionPending.promote(db, bus, sessionID, "steer") expect( (yield* session.messages({ sessionID, order: "asc" })).map((message) => @@ -952,11 +952,11 @@ describe("SessionV2.prompt", () => { ) }) -describe("SessionV2.pending", () => { +describe("Session.pending", () => { it.effect("fails for an unknown session", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - expect(yield* session.pending(SessionV2.ID.make("ses_missing")).pipe(Effect.flip)).toMatchObject({ + const session = yield* Session.Service + expect(yield* session.pending(Session.ID.make("ses_missing")).pipe(Effect.flip)).toMatchObject({ _tag: "Session.NotFoundError", }) }), @@ -965,8 +965,8 @@ describe("SessionV2.pending", () => { it.effect("lists admitted work in admission order until promotion", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const first = yield* session.prompt({ sessionID, text: "First steer", resume: false }) @@ -985,12 +985,12 @@ describe("SessionV2.pending", () => { ]) expect( - yield* SessionPending.promote(db, events, sessionID, "input"), + yield* SessionPending.promote(db, bus, sessionID, "input"), ).toBe(2) expect(yield* session.pending(sessionID)).toMatchObject([{ id: queued.id, type: "synthetic" }]) expect( - yield* SessionPending.promote(db, events, sessionID, "input"), + yield* SessionPending.promote(db, bus, sessionID, "input"), ).toBe(1) expect(yield* session.pending(sessionID)).toEqual([]) }), @@ -999,7 +999,7 @@ describe("SessionV2.pending", () => { it.effect("lists an unhandled compaction barrier until it settles", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const { db } = yield* Database.Service const barrier = yield* session.compact({ sessionID }) diff --git a/packages/core/test/session-remove.test.ts b/packages/core/test/session-remove.test.ts index 8d61af76c856..65728a7b7cef 100644 --- a/packages/core/test/session-remove.test.ts +++ b/packages/core/test/session-remove.test.ts @@ -3,40 +3,40 @@ import { Effect, Layer } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Database } from "@opencode-ai/core/database/database" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Location } from "@opencode-ai/core/location" -import { ProjectV2 } from "@opencode-ai/core/project" +import { Project } from "@opencode-ai/core/project" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionExecution } from "@opencode-ai/core/session/execution" import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionStore } from "@opencode-ai/core/session/store" import { testEffect } from "./lib/effect" const projects = Layer.succeed( - ProjectV2.Service, - ProjectV2.Service.of({ + Project.Service, + Project.Service.of({ list: () => Effect.succeed([]), - resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }), + resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory }), directories: () => Effect.succeed([]), commit: () => Effect.void, }), ) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]), + LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Session.node]), [ - [ProjectV2.node, projects], + [Project.node, projects], [SessionExecution.node, SessionExecution.noopLayer], ], ), ) const location = Location.Ref.make({ directory: AbsolutePath.make("/project") }) -describe("SessionV2.remove", () => { +describe("Session.remove", () => { it.effect("removes a session and its children", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const parent = yield* session.create({ location }) const child = yield* session.create({ parentID: parent.id }) @@ -50,8 +50,8 @@ describe("SessionV2.remove", () => { it.effect("fails when the session does not exist", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const sessionID = SessionV2.ID.make("ses_missing") + const session = yield* Session.Service + const sessionID = Session.ID.make("ses_missing") expect(yield* Effect.result(session.remove(sessionID))).toMatchObject({ _tag: "Failure", diff --git a/packages/core/test/session-runner-message.test.ts b/packages/core/test/session-runner-message.test.ts index de33bcd69dc4..5e5b0e5ddbc6 100644 --- a/packages/core/test/session-runner-message.test.ts +++ b/packages/core/test/session-runner-message.test.ts @@ -1,18 +1,18 @@ import { describe, expect, test } from "bun:test" import { Message } from "@opencode-ai/ai" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { SessionMessage } from "@opencode-ai/core/session/message" import { AgentAttachment, Base64, FileAttachment } from "@opencode-ai/schema/prompt" import { toLLMMessages } from "@opencode-ai/core/session/runner/to-llm-message" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Shell } from "@opencode-ai/schema/shell" import { DateTime } from "effect" const created = DateTime.makeUnsafe(0) const id = (value: string) => SessionMessage.ID.make(`msg_${value}`) -const model = ModelV2.Ref.make({ id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }) -const build = AgentV2.defaultID +const model = Model.Ref.make({ id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }) +const build = Agent.defaultID describe("toLLMMessages", () => { test("omits empty assistant turns", () => { @@ -21,7 +21,7 @@ describe("toLLMMessages", () => { id: id(value), type: "assistant", agent: build, - model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }, + model: { id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }, content, time: { created, completed: created }, }) @@ -45,7 +45,7 @@ describe("toLLMMessages", () => { expect(messages.map((message) => message.id)).toEqual([id("text"), id("reasoning")]) }) - test("maps every top-level V2 Session message type", () => { + test("maps every top-level Session message type", () => { const file = FileAttachment.make({ data: Base64.make("aGVsbG8="), mime: "image/png", @@ -63,7 +63,7 @@ describe("toLLMMessages", () => { SessionMessage.ModelSelected.make({ id: id("model"), type: "model-switched", - model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }, + model: { id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }, time: { created }, }), SessionMessage.System.make({ @@ -345,7 +345,7 @@ Recent work id: id("assistant"), type: "assistant", agent: build, - model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }, + model: { id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }, content: [ SessionMessage.AssistantText.make({ type: "text", text: "Checking" }), SessionMessage.AssistantReasoning.make({ @@ -495,7 +495,7 @@ Recent work id: id("assistant-openai-reasoning"), type: "assistant", agent: build, - model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }, + model: { id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }, content: [ SessionMessage.AssistantReasoning.make({ type: "reasoning", @@ -519,7 +519,7 @@ Recent work }) test("replays flat state under an OpenCode hosted model's route key", () => { - const opencode = ModelV2.Ref.make({ id: ModelV2.ID.make("claude-fable-5"), providerID: ProviderV2.ID.opencode }) + const opencode = Model.Ref.make({ id: Model.ID.make("claude-fable-5"), providerID: Provider.ID.opencode }) const messages = toLLMMessages( [ SessionMessage.Assistant.make({ @@ -553,7 +553,7 @@ Recent work id: id("assistant-failed"), type: "assistant", agent: build, - model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }, + model: { id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }, content: [ SessionMessage.AssistantReasoning.make({ type: "reasoning", @@ -651,7 +651,7 @@ Recent work id: id("assistant-old-model"), type: "assistant", agent: build, - model: { id: ModelV2.ID.make("old-model"), providerID: ProviderV2.ID.make("provider") }, + model: { id: Model.ID.make("old-model"), providerID: Provider.ID.make("provider") }, content: [ SessionMessage.AssistantReasoning.make({ type: "reasoning", @@ -745,7 +745,7 @@ Recent work id: id("assistant-alias"), type: "assistant", agent: build, - model: { id: ModelV2.ID.make("fast"), providerID: ProviderV2.ID.make("provider") }, + model: { id: Model.ID.make("fast"), providerID: Provider.ID.make("provider") }, content: [ SessionMessage.AssistantReasoning.make({ type: "reasoning", @@ -756,7 +756,7 @@ Recent work time: { created, completed: created }, }), ], - ModelV2.Ref.make({ id: ModelV2.ID.make("fast"), providerID: ProviderV2.ID.make("provider") }), + Model.Ref.make({ id: Model.ID.make("fast"), providerID: Provider.ID.make("provider") }), ) expect(messages[0]?.content).toEqual([ @@ -775,7 +775,7 @@ Recent work id: id("assistant-phase"), type: "assistant", agent: build, - model: { id: ModelV2.ID.make("old"), providerID: ProviderV2.ID.make("provider") }, + model: { id: Model.ID.make("old"), providerID: Provider.ID.make("provider") }, content: [ SessionMessage.AssistantText.make({ type: "text", @@ -787,7 +787,7 @@ Recent work time: { created, completed: created }, }), ], - ModelV2.Ref.make({ id: ModelV2.ID.make("new"), providerID: ProviderV2.ID.make("provider") }), + Model.Ref.make({ id: Model.ID.make("new"), providerID: Provider.ID.make("provider") }), ) expect(messages[0]?.content).toEqual([ diff --git a/packages/core/test/session-runner-recorded.test.ts b/packages/core/test/session-runner-recorded.test.ts index 70bc15328213..a430cb212a6c 100644 --- a/packages/core/test/session-runner-recorded.test.ts +++ b/packages/core/test/session-runner-recorded.test.ts @@ -6,16 +6,16 @@ import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { EventTable } from "@opencode-ai/core/event/sql" import { Job } from "@opencode-ai/core/job" -import { PermissionV2 } from "@opencode-ai/core/permission" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Permission } from "@opencode-ai/core/permission" +import { Agent } from "@opencode-ai/core/agent" import { Config } from "@opencode-ai/core/config" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { Snapshot } from "@opencode-ai/core/snapshot" import { SessionCompaction } from "@opencode-ai/core/session/compaction" import { SessionTitle } from "@opencode-ai/core/session/title" @@ -25,8 +25,7 @@ import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator import { SessionRunner } from "@opencode-ai/core/session/runner" import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm" import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { Tool } from "@opencode-ai/core/tool" import { SessionTable } from "@opencode-ai/core/session/sql" import { SessionStore } from "@opencode-ai/core/session/store" import { Location } from "@opencode-ai/core/location" @@ -56,8 +55,8 @@ const cassette = HttpRecorder.layerFetch(cassetteName, { directory: cassetteDire const executor = RequestExecutor.layer.pipe(Layer.provide(cassette)) const client = LLMClient.layer.pipe(Layer.provide(executor)) const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: () => Effect.die("unused"), ask: () => Effect.die("unused"), reply: () => Effect.die("unused"), @@ -116,15 +115,14 @@ const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [ [ReferenceInstructions.node, referenceInstructions], [McpInstructions.node, mcpInstructions], [Config.node, config], - [PermissionV2.node, permission], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + [Permission.node, permission], [PluginSupervisor.node, pluginSupervisor], ]) const execution = Layer.effect( SessionExecution.Service, Effect.gen(function* () { const sessionRunner = yield* SessionRunner.Service - const coordinator = yield* SessionRunCoordinator.make({ + const coordinator = yield* SessionRunCoordinator.make({ drain: (sessionID, force) => sessionRunner.drain({ sessionID, force }), }) return SessionExecution.Service.of({ @@ -140,13 +138,13 @@ const it = testEffect( AppNodeBuilder.build( LayerNode.group([ Database.node, - EventV2.node, + Bus.node, SessionProjector.node, SessionStore.node, - AgentV2.node, + Agent.node, Catalog.node, PluginHooks.node, - ToolRegistry.node, + Tool.node, SessionRunnerModel.node, InstructionBuiltIns.node, InstructionDiscovery.node, @@ -155,13 +153,12 @@ const it = testEffect( Config.node, Snapshot.node, SessionRunnerLLM.node, - SessionV2.node, + Session.node, ]), [ [LayerNodePlatform.llmClient, client], - [PermissionV2.node, permission], + [Permission.node, permission], [Catalog.node, promptCatalog], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], [SessionRunnerModel.node, models], [InstructionBuiltIns.node, systemContext], [InstructionDiscovery.node, instructionContext], @@ -175,16 +172,16 @@ const it = testEffect( ], ), ) -const sessionID = SessionV2.ID.make("ses_runner_recorded") +const sessionID = Session.ID.make("ses_runner_recorded") describe("SessionRunnerLLM recorded", () => { - it.effect("executes one recorded V2 prompt through the recorded HTTP transport", () => + it.effect("executes one recorded prompt through the recorded HTTP transport", () => Effect.gen(function* () { - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const catalog = yield* Catalog.Service const hooks = yield* PluginHooks.Service yield* agents.transform((draft) => - draft.update(AgentV2.ID.make("build"), (agent) => { + draft.update(Agent.ID.make("build"), (agent) => { agent.mode = "primary" agent.permissions.push({ action: "execute", resource: "*", effect: "deny" }) }), @@ -215,7 +212,7 @@ describe("SessionRunnerLLM recorded", () => { .onConflictDoNothing() .run() .pipe(Effect.orDie) - const session = yield* SessionV2.Service + const session = yield* Session.Service const prompt = yield* session.prompt({ sessionID, text: "Say hello in one short sentence.", diff --git a/packages/core/test/session-runner-tool-events.test.ts b/packages/core/test/session-runner-tool-events.test.ts index 23b607a70e05..82c6a8691592 100644 --- a/packages/core/test/session-runner-tool-events.test.ts +++ b/packages/core/test/session-runner-tool-events.test.ts @@ -2,29 +2,30 @@ import { expect, test } from "bun:test" import { Cause, Effect, Exit, Schema } from "effect" import { LLMEvent } from "@opencode-ai/ai" import { Money } from "@opencode-ai/schema/money" -import { EventV2 } from "@opencode-ai/core/event" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" +import { Agent } from "@opencode-ai/core/agent" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionMessage } from "@opencode-ai/core/session/message" -import { SessionV2 } from "@opencode-ai/core/session" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Session } from "@opencode-ai/core/session" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { RelativePath } from "@opencode-ai/core/schema" import { Snapshot } from "@opencode-ai/core/snapshot" import { createLLMEventPublisher } from "@opencode-ai/core/session/runner/publish-llm-event" -const sessionID = SessionV2.ID.make("ses_tool_event_test") +const sessionID = Session.ID.make("ses_tool_event_test") const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB" const capture = (providerMetadataKey = "anthropic", options?: { readonly interruptProgress?: boolean }) => { const published: Array<{ readonly type: string; readonly data: unknown }> = [] - const events: Pick = { + const bus: Pick = { publish: (definition, data) => { const publish = Effect.sync(() => { - const event = { id: EventV2.ID.create(), type: definition.type, data } as EventV2.Payload + const event = { id: Event.ID.create(), type: definition.type, data } as Event.Payload published.push({ type: definition.durable - ? EventV2.versionedType(definition.type, definition.durable.version) + ? Bus.versionedType(definition.type, definition.durable.version) : definition.type, data, }) @@ -37,12 +38,12 @@ const capture = (providerMetadataKey = "anthropic", options?: { readonly interru } return { published, - publisher: createLLMEventPublisher(events, { + publisher: createLLMEventPublisher(bus, { sessionID, - agent: AgentV2.ID.make("build"), + agent: Agent.ID.make("build"), model: { - id: ModelV2.ID.make("model"), - providerID: ProviderV2.ID.opencode, + id: Model.ID.make("model"), + providerID: Provider.ID.opencode, }, providerMetadataKey, assistantMessageID: SessionMessage.ID.create(), @@ -68,7 +69,6 @@ test("local tool success serializes media base64 once through canonical content" await Effect.runPromise(publisher.publish(call)) await Effect.runPromise( publisher.toolExecution(call.id, call.name, { - status: "completed", output: { type: "media", mime: "image/png" }, content: [ { type: "text", text: "Image read successfully" }, @@ -227,10 +227,7 @@ test("binary failure emits no success event", async () => { const { published, publisher } = capture() await Effect.runPromise(publisher.publish(call)) await Effect.runPromise( - publisher.toolExecution(call.id, call.name, { - status: "error", - error: { type: "tool.execution", message: "Cannot read binary file" }, - }), + publisher.failTool(call.id, { type: "tool.execution", message: "Cannot read binary file" }), ) expect(published.some((event) => event.type === "session.tool.success.2")).toBe(false) expect(published.some((event) => event.type === "session.tool.failed.2")).toBe(true) diff --git a/packages/core/test/session-runner-tool-registry.test.ts b/packages/core/test/session-runner-tool-registry.test.ts index b924f15fb7bd..37a32fa4c7ac 100644 --- a/packages/core/test/session-runner-tool-registry.test.ts +++ b/packages/core/test/session-runner-tool-registry.test.ts @@ -1,35 +1,16 @@ import { describe, expect } from "bun:test" -import { Tool } from "@opencode-ai/core/tool/tool" -import { AgentV2 } from "@opencode-ai/core/agent" -import type { PermissionV2 } from "@opencode-ai/core/permission" +import { Agent } from "@opencode-ai/core/agent" +import type { Permission } from "@opencode-ai/core/permission" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Image } from "@opencode-ai/core/image" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionMessage } from "@opencode-ai/core/session/message" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" +import { Tool } from "@opencode-ai/core/tool" +import type { Info } from "@opencode-ai/schema/tool" import { executeTool, toolDefinitions } from "./lib/tool" import { Cause, Deferred, Effect, Exit, Fiber, Layer, Option, Schema, SchemaGetter, SchemaIssue, Scope } from "effect" import { testEffect } from "./lib/effect" -const bounds: ToolOutputStore.BoundInput[] = [] -const retentionFailure = new ToolOutputStore.StorageError({ operation: "write", cause: new Error("disk full") }) -const outputStore = Layer.mock(ToolOutputStore.Service, { - limits: () => Effect.succeed({ maxLines: ToolOutputStore.MAX_LINES, maxBytes: ToolOutputStore.MAX_BYTES }), - bound: (input) => { - if (input.callID === "call-retention-failure") return Effect.fail(retentionFailure) - return Effect.sync(() => bounds.push(input)).pipe( - Effect.as( - input.callID === "call-bounded" - ? { - content: [{ type: "text" as const, text: "bounded reference" }], - outputPaths: ["/managed/generic"], - } - : { content: input.content, outputPaths: [] }, - ), - ) - }, -}) const imageStore = Layer.mock(Image.Service, { normalize: (resource, content) => { if (resource === "corrupt.png") return Effect.fail(new Image.DecodeError({ resource })) @@ -48,43 +29,53 @@ const imageStore = Layer.mock(Image.Service, { return Effect.succeed({ ...content, content: "bm9ybWFsaXplZA==", mime: "image/jpeg" }) }, }) -const registryLayer = AppNodeBuilder.build(ToolRegistry.node, [ - [ToolOutputStore.node, outputStore], - [Image.node, imageStore], -]) +const registryLayer = AppNodeBuilder.build(Tool.node, [[Image.node, imageStore]]) const it = testEffect(registryLayer) const identity = { - agent: AgentV2.ID.make("build"), + agent: Agent.ID.make("build"), messageID: SessionMessage.ID.make("msg_registry"), } -const sessionID = SessionV2.ID.make("ses_registry") -const call = (name: string, id = `call-${name}`): ToolRegistry.ExecuteInput => ({ +const sessionID = Session.ID.make("ses_registry") +const call = (name: string, id = `call-${name}`): Parameters[0] => ({ sessionID, ...identity, call: { type: "tool-call", id, name, input: { text: name } }, }) -const make = () => - Tool.make({ +const make = (): Info => + ({ + name: "echo", description: "Echo text", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), execute: ({ text }) => Effect.succeed({ output: { text }, content: text }), }) -const constant = (text: string) => - Tool.make({ +const constant = (text: string): Info => + ({ + name: "constant", description: "Return text", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), execute: () => Effect.succeed({ output: { text }, content: text }), }) -describe("ToolRegistry", () => { +const transform = ( + service: Tool.Interface, + tools: Readonly>, + options?: Tool.Options, +) => + service.transform((draft) => + Object.entries(tools).forEach(([name, tool]) => + draft.add({ ...tool, name, options: { ...tool.options, ...options } }), + ), + ) + +describe("Tool", () => { it.effect("rejects invalid dotted namespaces", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service - const error = yield* service.register({ echo: make() }, { namespace: "slack..admin" }).pipe(Effect.flip) + const service = yield* Tool.Service + const error = yield* transform(service, { echo: make() }, { namespace: "slack..admin" }).pipe(Effect.flip) expect(error).toBeInstanceOf(Tool.RegistrationError) expect(error.message).toBe('Invalid tool namespace: "slack..admin"') @@ -94,12 +85,11 @@ describe("ToolRegistry", () => { it.effect("rejects invalid and colliding normalized names", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service - const invalid = yield* service.register({ "123": make() }, { codemode: false }).pipe(Effect.flip) + const service = yield* Tool.Service + const invalid = yield* transform(service, { "123": make() }, { codemode: false }).pipe(Effect.flip) expect(invalid.message).toBe("Invalid tool name: 123") - const collision = yield* service - .register({ "echo.tool": make(), echo_tool: make() }, { codemode: false }) + const collision = yield* transform(service, { "echo.tool": make(), echo_tool: make() }, { codemode: false }) .pipe(Effect.flip) expect(collision.message).toBe("Duplicate normalized tool name: echo_tool") expect((yield* service.snapshot()).definitions.map((tool) => tool.name)).toEqual(["execute"]) @@ -108,12 +98,12 @@ describe("ToolRegistry", () => { it.effect("validates a registration batch before installing any tools", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service const error = yield* service - .registerBatch([ - { tools: { first: make() }, options: { codemode: false } }, - { tools: { second: make() }, options: { namespace: "invalid..namespace", codemode: false } }, - ]) + .transform((draft) => { + draft.add({ ...make(), name: "first", options: { codemode: false } }) + draft.add({ ...make(), name: "second", options: { namespace: "invalid..namespace", codemode: false } }) + }) .pipe(Effect.flip) expect(error).toBeInstanceOf(Tool.RegistrationError) @@ -123,24 +113,26 @@ describe("ToolRegistry", () => { it.effect("canonicalizes effective definitions and keeps Code Mode last", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service const tool = make() - const capture = (registrations: Parameters[0]) => + const capture = (tools: ReadonlyArray) => Effect.scoped( Effect.gen(function* () { - yield* service.registerBatch(registrations) + yield* service.transform((draft) => tools.forEach(draft.add)) return (yield* service.snapshot()).definitions }), ) const first = yield* capture([ - { tools: { zeta: tool, alpha: tool }, options: { codemode: false } }, - { tools: { beta: tool }, options: { namespace: "alpha", codemode: false } }, - { tools: { echo: tool } }, + { ...tool, name: "zeta", options: { codemode: false } }, + { ...tool, name: "alpha", options: { codemode: false } }, + { ...tool, name: "beta", options: { namespace: "alpha", codemode: false } }, + { ...tool, name: "echo" }, ]) const second = yield* capture([ - { tools: { echo: tool } }, - { tools: { beta: tool }, options: { namespace: "alpha", codemode: false } }, - { tools: { alpha: tool, zeta: tool }, options: { codemode: false } }, + { ...tool, name: "echo" }, + { ...tool, name: "beta", options: { namespace: "alpha", codemode: false } }, + { ...tool, name: "alpha", options: { codemode: false } }, + { ...tool, name: "zeta", options: { codemode: false } }, ]) expect(first).toEqual(second) @@ -150,7 +142,7 @@ describe("ToolRegistry", () => { it.effect("keeps execute available without Code Mode tools unless explicitly denied", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service const available = yield* service.snapshot() expect(available.definitions.map((tool) => tool.name)).toEqual(["execute"]) @@ -164,10 +156,10 @@ describe("ToolRegistry", () => { it.effect("filters disabled tools with edit aliases and ordered wildcard precedence", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service - yield* service.register({ question: make(), bash: make() }, { codemode: false }) - yield* service.register({ edit: make(), write: make() }, { codemode: false, permission: "edit" }) - const names = (permissions: PermissionV2.Ruleset) => + const service = yield* Tool.Service + yield* transform(service, { question: make(), bash: make() }, { codemode: false }) + yield* transform(service, { edit: make(), write: make() }, { codemode: false, permission: "edit" }) + const names = (permissions: Permission.Ruleset) => toolDefinitions(service, permissions).pipe(Effect.map((definitions) => definitions.map((tool) => tool.name))) expect(yield* names([{ action: "question", resource: "*", effect: "deny" }])).toEqual([ @@ -198,10 +190,10 @@ describe("ToolRegistry", () => { it.effect("keeps permission options isolated between registrations", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service const shared = make() - yield* service.register({ first: shared }, { codemode: false }) - yield* service.register({ second: shared }, { codemode: false, permission: "edit" }) + yield* transform(service, { first: shared }, { codemode: false }) + yield* transform(service, { second: shared }, { codemode: false, permission: "edit" }) expect( (yield* toolDefinitions(service, [{ action: "edit", resource: "*", effect: "deny" }])).map((tool) => tool.name), @@ -211,9 +203,9 @@ describe("ToolRegistry", () => { it.effect("removes a scoped registration", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service const scope = yield* Scope.make() - yield* service.register({ echo: make() }, { codemode: false }).pipe(Scope.provide(scope)) + yield* transform(service, { echo: make() }, { codemode: false }).pipe(Scope.provide(scope)) expect((yield* toolDefinitions(service)).map((tool) => tool.name)).toEqual(["echo", "execute"]) yield* Scope.close(scope, Exit.void) expect((yield* toolDefinitions(service)).map((tool) => tool.name)).toEqual(["execute"]) @@ -222,11 +214,10 @@ describe("ToolRegistry", () => { it.effect("preserves an interrupted registration until its scope closes", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service const scope = yield* Scope.make() const registered = yield* Deferred.make() - const fiber = yield* service - .register({ echo: make() }, { codemode: false }) + const fiber = yield* transform(service, { echo: make() }, { codemode: false }) .pipe( Effect.andThen(Deferred.succeed(registered, undefined)), Effect.andThen(Effect.never), @@ -244,14 +235,15 @@ describe("ToolRegistry", () => { it.effect("returns model errors without swallowing interruption or defects", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service - yield* service.register( + const service = yield* Tool.Service + yield* transform(service, { - failed: Tool.make({ + failed: ({ + name: "failed", description: "Failed", input: Schema.Struct({}), output: Schema.Struct({ ok: Schema.Boolean }), - execute: () => Effect.fail(new Tool.Failure({ message: "Denied" })), + execute: () => Effect.fail(new Tool.Error({ message: "Denied" })), }), }, { codemode: false }, @@ -269,11 +261,12 @@ describe("ToolRegistry", () => { ...identity, call: { type: "tool-call", id: "missing", name: "missing", input: {} }, }), - ).toEqual({ status: "error", error: { type: "tool.unknown", message: "Unknown tool: missing" } }) + ).toEqual({ status: "error", error: { type: "tool.execution", message: "Unknown tool: missing" } }) - yield* service.register( + yield* transform(service, { - defect: Tool.make({ + defect: ({ + name: "defect", description: "Defect", input: Schema.Struct({}), output: Schema.Struct({}), @@ -297,22 +290,9 @@ describe("ToolRegistry", () => { }), ) - it.effect("propagates retention failures through execution", () => - Effect.gen(function* () { - const service = yield* ToolRegistry.Service - yield* service.register({ echo: make() }, { codemode: false }) - const toolSet = yield* service.snapshot() - const exit = yield* toolSet.execute(call("echo", "call-retention-failure")).pipe(Effect.exit) - - expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) expect(Option.getOrUndefined(Cause.findErrorOption(exit.cause))).toBe(retentionFailure) - expect(retentionFailure.message).toBe("Failed to write tool output: disk full") - }), - ) - it.effect("exposes execution only through a snapshot", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service expect("definitions" in service).toBe(false) expect("execute" in service).toBe(false) expect("settle" in service).toBe(false) @@ -322,11 +302,12 @@ describe("ToolRegistry", () => { it.effect("passes complete call identity to tool execution", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service const contexts: Tool.Context[] = [] - yield* service.register( + yield* transform(service, { - context: Tool.make({ + context: ({ + name: "context", description: "Context", input: Schema.Struct({}), output: Schema.Struct({ ok: Schema.Boolean }), @@ -341,37 +322,19 @@ describe("ToolRegistry", () => { ...identity, call: { type: "tool-call", id: "call-context", name: "context", input: {} }, }) - expect(contexts).toEqual([{ sessionID, ...identity, callID: "call-context", progress: expect.any(Function) }]) - }), - ) - - it.effect("encodes output and applies generic execution bounding", () => - Effect.gen(function* () { - bounds.length = 0 - const service = yield* ToolRegistry.Service - yield* service.register({ bounded: make() }, { codemode: false }) - expect( - yield* executeTool(service, { - sessionID, - ...identity, - call: { type: "tool-call", id: "call-bounded", name: "bounded", input: { text: "complete" } }, - }), - ).toEqual({ - status: "completed", - output: { text: "complete" }, - content: [{ type: "text", text: "bounded reference" }], - outputPaths: ["/managed/generic"], - }) - expect(bounds).toHaveLength(1) + expect(contexts).toEqual([ + { sessionID, ...identity, callID: Tool.CallID.make("call-context"), progress: expect.any(Function) }, + ]) }), ) it.effect("normalizes image tool output at execution and drops unresizable images", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service - yield* service.register( + const service = yield* Tool.Service + yield* transform(service, { - snapshot: Tool.make({ + snapshot: ({ + name: "snapshot", description: "Return images", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), @@ -379,9 +342,14 @@ describe("ToolRegistry", () => { Effect.succeed({ output: { text }, content: [ - { type: "file", data: "aW1hZ2U=", mime: "image/png", name: "frame.png" }, - { type: "file", data: "aW1hZ2U=", mime: "image/png", name: "too-large.png" }, - { type: "file", data: "aW1hZ2U=", mime: "image/png", name: "corrupt.png" }, + { type: "file", uri: "data:image/png;base64,aW1hZ2U=", mime: "image/png", name: "frame.png" }, + { + type: "file", + uri: "data:image/png;base64,aW1hZ2U=", + mime: "image/png", + name: "too-large.png", + }, + { type: "file", uri: "data:image/png;base64,aW1hZ2U=", mime: "image/png", name: "corrupt.png" }, { type: "text", text }, ], }), @@ -402,10 +370,11 @@ describe("ToolRegistry", () => { it.effect("publishes progress metadata unchanged", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service - yield* service.register( + const service = yield* Tool.Service + yield* transform(service, { - progressive: Tool.make({ + progressive: ({ + name: "progressive", description: "Emit image progress", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), @@ -416,7 +385,7 @@ describe("ToolRegistry", () => { { codemode: false }, ) - const updates: ToolRegistry.Progress[] = [] + const updates: Tool.Metadata[] = [] yield* executeTool(service, { ...call("progressive"), progress: (update) => @@ -430,7 +399,7 @@ describe("ToolRegistry", () => { it.effect("enforces transformed codecs at execution and projection boundaries", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service const executed: string[] = [] const Transformed = Schema.Boolean.pipe( Schema.decodeTo(Schema.String, { @@ -438,9 +407,10 @@ describe("ToolRegistry", () => { encode: SchemaGetter.transform((value) => value === "yes"), }), ) - yield* service.register( + yield* transform(service, { - transformed: Tool.make({ + transformed: ({ + name: "transformed", description: "Transform values", input: Schema.Struct({ value: Transformed }), output: Schema.Struct({ value: Transformed }), @@ -476,9 +446,10 @@ describe("ToolRegistry", () => { }) expect(executed).toEqual(["yes"]) - yield* service.register( + yield* transform(service, { - invalid_output: Tool.make({ + invalid_output: ({ + name: "invalid_output", description: "Return invalid output", input: Schema.Struct({}), output: Schema.Struct({ @@ -513,12 +484,12 @@ describe("ToolRegistry", () => { it.effect("executes the tool advertised in a model request", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service const scope = yield* Scope.make() - yield* service.register({ echo: constant("advertised") }, { codemode: false }).pipe(Scope.provide(scope)) + yield* transform(service, { echo: constant("advertised") }, { codemode: false }).pipe(Scope.provide(scope)) const request = yield* service.snapshot() yield* Scope.close(scope, Exit.void) - yield* service.register({ echo: constant("replacement") }, { codemode: false }) + yield* transform(service, { echo: constant("replacement") }, { codemode: false }) expect((yield* request.execute(call("echo"))).content).toEqual([{ type: "text", text: "advertised" }]) expect((yield* executeTool(service, call("echo"))).content).toEqual([{ type: "text", text: "replacement" }]) @@ -527,10 +498,10 @@ describe("ToolRegistry", () => { it.effect("reveals the previous registration after an overlay closes", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service - yield* service.register({ echo: constant("base") }, { codemode: false }) + const service = yield* Tool.Service + yield* transform(service, { echo: constant("base") }, { codemode: false }) const overlay = yield* Scope.make() - yield* service.register({ echo: constant("overlay") }, { codemode: false }).pipe(Scope.provide(overlay)) + yield* transform(service, { echo: constant("overlay") }, { codemode: false }).pipe(Scope.provide(overlay)) expect((yield* executeTool(service, call("echo"))).content).toEqual([{ type: "text", text: "overlay" }]) yield* Scope.close(overlay, Exit.void) @@ -540,12 +511,12 @@ describe("ToolRegistry", () => { it.effect("executes and reports progress for codemode tools advertised in a model request", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service const executed: string[] = [] const scope = yield* Scope.make() - yield* service - .register({ - echo: Tool.make({ + yield* transform(service, { + echo: ({ + name: "echo", description: "Echo text", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), @@ -563,8 +534,9 @@ describe("ToolRegistry", () => { expect(execute?.description).toContain("confined Code Mode runtime") expect(execute?.description).not.toContain("Echo text") yield* Scope.close(scope, Exit.void) - yield* service.register({ - echo: Tool.make({ + yield* transform(service, { + echo: ({ + name: "echo", description: "Echo text", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), @@ -572,7 +544,7 @@ describe("ToolRegistry", () => { }), }) - const progress: ToolRegistry.Progress[] = [] + const progress: Tool.Metadata[] = [] const execution = yield* toolSet.execute({ ...call("execute"), call: { @@ -584,10 +556,11 @@ describe("ToolRegistry", () => { progress: (update) => Effect.sync(() => progress.push(update)), }) - expect(execution).toMatchObject({ status: "completed", content: [{ type: "text" }] }) + expect(execution).toMatchObject({ content: [{ type: "text" }] }) expect(executed).toEqual(["old:request"]) expect(progress).toEqual([ { toolCalls: [{ tool: "echo", status: "running", input: { text: "request" } }] }, + { stage: "old" }, { toolCalls: [{ tool: "echo", status: "completed", input: { text: "request" } }] }, ]) }), diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 1f96f41cbc75..2c4876235ccb 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -21,15 +21,16 @@ import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" import { App } from "@opencode-ai/core/app" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { EventTable } from "@opencode-ai/core/event/sql" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { Form } from "@opencode-ai/core/form" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { Snapshot } from "@opencode-ai/core/snapshot" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionPending } from "@opencode-ai/core/session/pending" @@ -42,18 +43,15 @@ import { SessionRunner } from "@opencode-ai/core/session/runner" import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm" import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" import { SessionUsage } from "@opencode-ai/core/session/usage" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { CodeMode } from "@opencode-ai/core/codemode" import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" import { PluginHooks } from "@opencode-ai/core/plugin/hooks" import { SystemPromptPlugin } from "@opencode-ai/core/plugin/system-prompt" -import { QuestionTool } from "@opencode-ai/core/tool/question" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" -import { AgentV2 } from "@opencode-ai/core/agent" +import { QuestionTool } from "@opencode-ai/core/tool/plugin/question" +import { Agent } from "@opencode-ai/core/agent" import { Config } from "@opencode-ai/core/config" import { ConfigCompaction } from "@opencode-ai/core/config/compaction" -import { Tool } from "@opencode-ai/core/tool/tool" -import { ToolHooks } from "@opencode-ai/core/tool/hooks" +import { Tool } from "@opencode-ai/core/tool" +import type { Info } from "@opencode-ai/schema/tool" import { InstructionStateTable, SessionPendingTable, @@ -68,9 +66,9 @@ import { InstructionDiscovery } from "@opencode-ai/core/instruction-discovery" import { SkillInstructions } from "@opencode-ai/core/skill/instructions" import { ReferenceInstructions } from "@opencode-ai/core/reference/instructions" import { McpInstructions } from "@opencode-ai/core/mcp/instructions" -import { ModelV2 } from "@opencode-ai/core/model" +import { ID } from "@opencode-ai/core/model" import { Location } from "@opencode-ai/core/location" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Schema, Scope, Stream } from "effect" import { TestClock } from "effect/testing" import { asc, eq } from "drizzle-orm" @@ -103,14 +101,14 @@ const client = Layer.succeed( responseStream = undefined return stream } - const events = streamFailure + const bus = streamFailure ? Stream.fail(streamFailure) : Stream.fromIterable(responses === undefined ? response : (responses.shift() ?? [])) - if (!streamGate) return events + if (!streamGate) return bus return Stream.unwrap( (streamStarted ? Deferred.succeed(streamStarted, undefined) : Effect.void).pipe( Effect.andThen(Deferred.await(streamGate)), - Effect.as(events), + Effect.as(bus), ), ) }) as unknown as LLMClientShape["stream"], @@ -213,14 +211,15 @@ test("does not apply an ineligible tier without base pricing", () => { const authorizations: Tool.Context[] = [] const executions: string[] = [] -const permissionFail = Tool.make({ +const permissionFail = ({ + name: "permission_fail", description: "Reject a permission", input: Schema.Struct({}), output: Schema.Struct({}), execute: () => new ToolFailure({ message: "Permission denied: edit", - error: new PermissionV2.BlockedError({ + error: new Permission.BlockedError({ rules: [], permission: "edit", resources: ["src/index.ts"], @@ -228,8 +227,8 @@ const permissionFail = Tool.make({ }), }) const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: () => Effect.die("unused"), ask: () => Effect.die("unused"), reply: () => Effect.die("unused"), @@ -238,11 +237,22 @@ const permission = Layer.succeed( list: () => Effect.die("unused"), }), ) +const transformTools = ( + registry: Tool.Interface, + tools: Readonly>, + options?: Tool.Options, +) => + registry.transform((draft) => + Object.entries(tools).forEach(([name, tool]) => + draft.add({ ...tool, name, options: { ...tool.options, ...options } }), + ), + ) const echo = Layer.effectDiscard( - ToolRegistry.Service.use((registry) => - registry.register( + Tool.Service.use((registry) => + transformTools(registry, { - echo: Tool.make({ + echo: ({ + name: "echo", description: "Echo text", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), @@ -259,7 +269,8 @@ const echo = Layer.effectDiscard( return { output: { text }, content: text } }).pipe(Effect.ensuring(Effect.sync(() => activeToolExecutions--))), }), - defect: Tool.make({ + defect: ({ + name: "defect", description: "Fail unexpectedly", input: Schema.Struct({}), output: Schema.Struct({}), @@ -268,9 +279,8 @@ const echo = Layer.effectDiscard( Effect.andThen(Effect.die("unexpected tool defect")), ), }), - // The wrapped ToolOutputStore below fails bound for this call ID with a - // typed StorageError, exercising the infrastructure failure channel. - storefail: Tool.make({ + storefail: ({ + name: "storefail", description: "Produce output that cannot be persisted", input: Schema.Struct({}), output: Schema.Struct({}), @@ -281,7 +291,7 @@ const echo = Layer.effectDiscard( ), ), ) -const echoNode = makeLocationNode({ name: "test/session-runner-tools", layer: echo, deps: [ToolRegistry.node] }) +const echoNode = makeLocationNode({ name: "test/session-runner-tools", layer: echo, deps: [Tool.node] }) let modelResolveHook = Effect.void let currentModel = model const models = Layer.mock(SessionRunnerModel.Service)({ @@ -301,7 +311,7 @@ let systemBaseline = "Initial context" let systemRemoved = false let systemUnavailable = false let systemLoadHook = Effect.void -const skillBaselines = new Map() +const skillBaselines = new Map() const systemContext = Layer.mock(InstructionBuiltIns.Service, { load: () => Effect.sync(() => @@ -369,12 +379,6 @@ const pluginSupervisor = Layer.succeed( flush: Effect.suspend(() => pluginFlushHook), }), ) -let codeModeMaterializations: ReadonlyArray = [] -let codeModeMaterializationCount = 0 -const codeMode = Layer.mock(CodeMode.Service, { - register: () => Effect.void, - materialize: () => Effect.sync(() => codeModeMaterializations[codeModeMaterializationCount++] ?? {}), -}) const promptCatalog = Layer.mock(Catalog.Service, { provider: { get: () => Effect.succeed(undefined), @@ -389,15 +393,6 @@ const promptCatalog = Layer.mock(Catalog.Service, { small: () => Effect.succeed(undefined), }, }) -// Pass-through bounding that fails "call-storefail" with a typed StorageError so -// runner tests can exercise the infrastructure failure channel deterministically. -const toolOutputStore = Layer.mock(ToolOutputStore.Service, { - limits: () => Effect.succeed({ maxLines: ToolOutputStore.MAX_LINES, maxBytes: ToolOutputStore.MAX_BYTES }), - bound: (input) => - input.callID === "call-storefail" - ? Effect.fail(new ToolOutputStore.StorageError({ operation: "write", cause: new Error("disk full") })) - : Effect.succeed({ content: input.content, outputPaths: [] }), -}) const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [ [Snapshot.node, Snapshot.noopLayer], [LayerNodePlatform.llmClient, client], @@ -407,18 +402,16 @@ const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [ [Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })], [SkillInstructions.node, skillInstructions], [ReferenceInstructions.node, referenceInstructions], - [PermissionV2.node, permission], + [Permission.node, permission], [Config.node, config], [McpInstructions.node, mcpInstructions], - [ToolOutputStore.node, toolOutputStore], [PluginSupervisor.node, pluginSupervisor], - [CodeMode.node, codeMode], ]) const execution = Layer.effect( SessionExecution.Service, Effect.gen(function* () { const sessionRunner = yield* SessionRunner.Service - const coordinator = yield* SessionRunCoordinator.make({ + const coordinator = yield* SessionRunCoordinator.make({ drain: (sessionID, force) => sessionRunner.drain({ sessionID, force }), }) return SessionExecution.Service.of({ @@ -434,15 +427,15 @@ const it = testEffect( AppNodeBuilder.build( LayerNode.group([ Database.node, - EventV2.node, + Bus.node, Form.node, SessionProjector.node, SessionStore.node, - AgentV2.node, + Agent.node, Catalog.node, - ToolRegistry.node, - ToolRegistry.toolsNode, - ToolHooks.node, + Tool.node, + Tool.node, + PluginHooks.node, PluginHooks.node, echoNode, SessionRunnerModel.node, @@ -455,11 +448,11 @@ const it = testEffect( Snapshot.node, SessionRunnerLLM.node, SessionExecution.node, - SessionV2.node, + Session.node, ]), [ [LayerNodePlatform.llmClient, client], - [PermissionV2.node, permission], + [Permission.node, permission], [Catalog.node, promptCatalog], [SessionRunnerModel.node, models], [InstructionBuiltIns.node, systemContext], @@ -470,17 +463,15 @@ const it = testEffect( [Snapshot.node, Snapshot.noopLayer], [SessionExecution.node, execution], [Config.node, config], - [ToolOutputStore.node, toolOutputStore], [PluginSupervisor.node, pluginSupervisor], - [CodeMode.node, codeMode], ], ), ) -const sessionID = SessionV2.ID.make("ses_runner_test") -const otherSessionID = SessionV2.ID.make("ses_runner_other") -const admit = (session: SessionV2.Interface, text: string) => session.prompt({ sessionID, text, resume: false }) +const sessionID = Session.ID.make("ses_runner_test") +const otherSessionID = Session.ID.make("ses_runner_other") +const admit = (session: Session.Interface, text: string) => session.prompt({ sessionID, text, resume: false }) -const insertSession = (id: SessionV2.ID) => +const insertSession = (id: Session.ID) => Effect.gen(function* () { const { db } = yield* Database.Service yield* db @@ -500,7 +491,7 @@ const insertSession = (id: SessionV2.ID) => const setup = Effect.gen(function* () { const { db } = yield* Database.Service - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const catalog = yield* Catalog.Service const hooks = yield* PluginHooks.Service const pluginHost = host({ @@ -521,8 +512,6 @@ const setup = Effect.gen(function* () { systemLoadHook = Effect.void modelResolveHook = Effect.void pluginFlushHook = Effect.void - codeModeMaterializations = [] - codeModeMaterializationCount = 0 currentModel = model skillBaselines.clear() responses = undefined @@ -537,7 +526,7 @@ const setup = Effect.gen(function* () { activeToolExecutions = 0 maxActiveToolExecutions = 0 yield* agents.transform((draft) => - draft.update(AgentV2.ID.make("build"), (agent) => { + draft.update(Agent.ID.make("build"), (agent) => { agent.mode = "primary" }), ) @@ -548,7 +537,7 @@ const setup = Effect.gen(function* () { .run() .pipe(Effect.orDie) yield* insertSession(sessionID) - return yield* SessionV2.Service + return yield* Session.Service }) const providerUnavailable = () => @@ -589,7 +578,7 @@ const messageTexts = (request: LLMRequest, role: "user" | "system") => const userTexts = (request: LLMRequest) => messageTexts(request, "user") const systemTexts = (request: LLMRequest) => messageTexts(request, "system") -const recordedEventTypes = (id: SessionV2.ID) => +const recordedEventTypes = (id: Session.ID) => Effect.gen(function* () { const { db } = yield* Database.Service return yield* db @@ -604,7 +593,7 @@ const recordedEventTypes = (id: SessionV2.ID) => ) }) -const recordedStepSettlementEvents = (id: SessionV2.ID, assistantMessageID: SessionMessage.ID) => +const recordedStepSettlementEvents = (id: Session.ID, assistantMessageID: SessionMessage.ID) => Effect.gen(function* () { const { db } = yield* Database.Service const settlementTypes = new Set([ @@ -635,10 +624,10 @@ const requireAssistant = (messages: readonly SessionMessage.Info[]) => { return assistant } -const replaySessionProjection = (id: SessionV2.ID) => +const replaySessionProjection = (id: Session.ID) => Effect.gen(function* () { const { db } = yield* Database.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const recorded = yield* db .select() .from(EventTable) @@ -647,11 +636,11 @@ const replaySessionProjection = (id: SessionV2.ID) => .all() .pipe(Effect.orDie) - yield* events.remove(id) + yield* bus.remove(id) yield* db.delete(InstructionStateTable).where(eq(InstructionStateTable.session_id, id)).run().pipe(Effect.orDie) yield* db.delete(SessionPendingTable).where(eq(SessionPendingTable.session_id, id)).run().pipe(Effect.orDie) yield* db.delete(SessionMessageTable).where(eq(SessionMessageTable.session_id, id)).run().pipe(Effect.orDie) - yield* events.replayAll( + yield* bus.replayAll( recorded.map((event) => ({ id: event.id, created: DateTime.makeUnsafe(event.created), @@ -666,7 +655,7 @@ const replaySessionProjection = (id: SessionV2.ID) => type FragmentKind = "text" | "reasoning" | "tool input" type FragmentFixture = { - readonly delta: EventV2.Definition + readonly delta: Event.Definition readonly completeEvents: LLMEvent[] readonly partialEvents: LLMEvent[] readonly expectedAssistant: unknown @@ -746,8 +735,8 @@ const verifyEphemeralDeltas = (kind: FragmentKind) => const fixture = fragmentFixture(kind, fragmentID(kind, "many"), chunks) const expectedContext = [{ type: "user", text: prompt }, fixture.expectedAssistant] yield* admit(session, prompt) - const events = yield* EventV2.Service - const live = yield* events.subscribe(fixture.delta).pipe(Stream.take(32), Stream.runCollect, Effect.forkScoped) + const bus = yield* Bus.Service + const live = yield* bus.subscribe(fixture.delta).pipe(Stream.take(32), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow response = fixture.completeEvents @@ -757,7 +746,7 @@ const verifyEphemeralDeltas = (kind: FragmentKind) => const deltas = yield* db .select({ type: EventTable.type }) .from(EventTable) - .where(eq(EventTable.type, EventV2.versionedType(fixture.delta.type, 1))) + .where(eq(EventTable.type, Bus.versionedType(fixture.delta.type, 1))) .all() .pipe(Effect.orDie) expect(Array.from(yield* Fiber.join(live))).toHaveLength(32) @@ -834,92 +823,6 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) => }) describe("SessionRunnerLLM", () => { - it.effect("uses one Code Mode materialization per request for instructions and execution", () => - Effect.gen(function* () { - const executed: string[] = [] - const execute = (name: string) => - Tool.make({ - description: `Execute ${name}`, - input: Schema.Struct({}), - output: Schema.String, - execute: () => Effect.sync(() => executed.push(name)).pipe(Effect.as({ output: name })), - }) - const catalog = (name: string) => [ - { - path: `catalog.${name.toLowerCase()}`, - description: `Code Mode catalog ${name}`, - signature: `tools.catalog.${name.toLowerCase()}(input: {}): Promise`, - }, - ] - const session = yield* setup - codeModeMaterializations = [ - { catalog: catalog("A"), tool: execute("A") }, - { catalog: catalog("B"), tool: execute("B") }, - { catalog: catalog("C"), tool: execute("C") }, - { catalog: catalog("D"), tool: execute("D") }, - ] - yield* admit(session, "Use Code Mode") - responses = [reply.tool("call-execute", "execute", {}), reply.stop()] - - yield* session.resume(sessionID) - - expect(requests).toHaveLength(2) - expect(codeModeMaterializationCount).toBe(2) - expect(requests[0]?.system.some((part) => part.text.includes("Code Mode catalog A"))).toBe(true) - expect(requests[0]?.system.some((part) => part.text.includes("Code Mode catalog B"))).toBe(false) - expect(requests[0]?.tools.find((tool) => tool.name === "execute")?.description).toBe("Execute A") - expect(executed).toEqual(["A"]) - expect(requests[1]?.tools.find((tool) => tool.name === "execute")?.description).toBe("Execute B") - expect( - requests[1]?.messages.some( - (message) => - message.role === "system" && - message.content.some((part) => part.type === "text" && part.text.includes("Code Mode catalog B")), - ), - ).toBe(true) - }), - ) - - it.effect("advertises execute and durable guidance for an empty Code Mode catalog", () => - Effect.gen(function* () { - const session = yield* setup - const empty = { - catalog: [], - tool: Tool.make({ - description: "Execute Code Mode", - input: Schema.Struct({ code: Schema.String }), - output: Schema.String, - execute: () => Effect.succeed({ output: "unused" }), - }), - } - codeModeMaterializations = [empty, empty, {}] - yield* admit(session, "Continue without Code Mode tools") - response = reply.stop() - - yield* session.resume(sessionID) - yield* admit(session, "Still no Code Mode tools") - yield* session.resume(sessionID) - yield* admit(session, "Code Mode denied") - yield* session.resume(sessionID) - - expect(requests).toHaveLength(3) - expect(requests[0]?.tools.map((tool) => tool.name)).toEqual(["defect", "echo", "storefail", "execute"]) - expect(requests[0]?.system.some((part) => part.text.includes("Do not call `execute`"))).toBe(true) - expect(requests[1]?.tools.map((tool) => tool.name)).toEqual(["defect", "echo", "storefail", "execute"]) - expect(requests[1]?.messages.filter((message) => message.role === "system")).toEqual([]) - expect(requests[2]?.tools.map((tool) => tool.name)).toEqual(["defect", "echo", "storefail"]) - expect( - requests[2]?.messages.some( - (message) => - message.role === "system" && - message.content.some( - (part) => part.type === "text" && part.text.includes("Code Mode tools are no longer available"), - ), - ), - ).toBe(true) - }), - ) - it.effect("applies session context hooks without exposing unavailable tools", () => Effect.gen(function* () { const session = yield* setup @@ -963,11 +866,12 @@ describe("SessionRunnerLLM", () => { it.effect("advertises and executes a location registered tool", () => Effect.gen(function* () { const session = yield* setup - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const contexts: Tool.Context[] = [] - yield* registry.register( + yield* transformTools(registry, { - location_context: Tool.make({ + location_context: ({ + name: "location_context", description: "Read application context", input: Schema.Struct({ query: Schema.String }), output: Schema.Struct({ answer: Schema.String }), @@ -983,8 +887,8 @@ describe("SessionRunnerLLM", () => { ) yield* admit(session, "Use application context") responses = [reply.tool("call-location", "location_context", { query: "hello" }), []] - const events = yield* EventV2.Service - const progressFiber = yield* events.subscribe(SessionEvent.Tool.Progress).pipe( + const bus = yield* Bus.Service + const progressFiber = yield* bus.subscribe(SessionEvent.Tool.Progress).pipe( Stream.filter((event) => event.data.sessionID === sessionID && event.data.callID === "call-location"), Stream.take(1), Stream.runCollect, @@ -997,9 +901,9 @@ describe("SessionRunnerLLM", () => { expect(contexts).toEqual([ { sessionID, - agent: AgentV2.ID.make("build"), + agent: Agent.ID.make("build"), messageID: expect.stringMatching(/^msg_/), - callID: "call-location", + callID: Tool.CallID.make("call-location"), progress: expect.any(Function), }, ]) @@ -1023,14 +927,17 @@ describe("SessionRunnerLLM", () => { it.effect("prefers failure outcome metadata over retained progress", () => Effect.gen(function* () { const session = yield* setup - const registry = yield* ToolRegistry.Service - const hooks = yield* ToolHooks.Service - yield* hooks.hook.after((event) => { - if (event.status === "error") event.metadata = { phase: "failed" } + const registry = yield* Tool.Service + const hooks = yield* PluginHooks.Service + yield* hooks.register("tool", "execute.after", (event) => { + if (event.status === "error") + event.error = new Tool.Error({ message: event.error.message, metadata: { phase: "failed" } }) + return Effect.void }) - yield* registry.register( + yield* transformTools(registry, { - failing_progress: Tool.make({ + failing_progress: ({ + name: "failing_progress", description: "Report progress and fail", input: Schema.Struct({}), output: Schema.Struct({}), @@ -1072,13 +979,13 @@ describe("SessionRunnerLLM", () => { it.effect("executes the tool advertised before a registry reload", () => Effect.gen(function* () { const session = yield* setup - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const scope = yield* Scope.make() const executions: string[] = [] - yield* registry - .register( + yield* transformTools(registry, { - reloaded: Tool.make({ + reloaded: ({ + name: "reloaded", description: "Record the advertised tool", input: Schema.Struct({}), output: Schema.Struct({ value: Schema.String }), @@ -1105,9 +1012,10 @@ describe("SessionRunnerLLM", () => { const run = yield* session.resume(sessionID).pipe(Effect.forkChild) yield* Deferred.await(streamStarted) yield* Scope.close(scope, Exit.void) - yield* registry.register( + yield* transformTools(registry, { - reloaded: Tool.make({ + reloaded: ({ + name: "reloaded", description: "Record the replacement tool", input: Schema.Struct({}), output: Schema.Struct({ value: Schema.String }), @@ -1182,7 +1090,7 @@ describe("SessionRunnerLLM", () => { }), ) - it.effect("streams one request with registry definitions from chronological V2 user history", () => + it.effect("streams one request with registry definitions from chronological user history", () => Effect.gen(function* () { const session = yield* setup yield* admit(session, "First") @@ -1204,9 +1112,9 @@ describe("SessionRunnerLLM", () => { it.effect("marks the initial instruction sync as baseline metadata", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service - const instructionEvents: EventV2.Payload[] = [] - const unsubscribe = yield* events.listen((event) => + const bus = yield* Bus.Service + const instructionEvents: Event.Payload[] = [] + const unsubscribe = yield* bus.listen((event) => Effect.sync(() => { if (event.type === "session.instructions.updated") instructionEvents.push(event) }), @@ -1260,12 +1168,12 @@ describe("SessionRunnerLLM", () => { it.effect("interrupts a source Location runner after a Session moves", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service yield* admit(session, "First") yield* session.resume(sessionID) - yield* events.publish(SessionEvent.Moved, { + yield* bus.publish(SessionEvent.Moved, { sessionID, location: Location.Ref.make({ directory: AbsolutePath.make("/moved") }), }) @@ -1313,16 +1221,16 @@ describe("SessionRunnerLLM", () => { expect(systemTexts(requests.at(-1)!)).toContain("Latest context") const { db } = yield* Database.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const recorded = yield* db .select() .from(EventTable) .where(eq(EventTable.aggregate_id, forked.id)) .orderBy(asc(EventTable.seq)) .all() - yield* events.remove(forked.id) + yield* bus.remove(forked.id) yield* db.delete(SessionTable).where(eq(SessionTable.id, forked.id)).run() - yield* events.replayAll( + yield* bus.replayAll( recorded.map((event) => ({ id: event.id, created: DateTime.makeUnsafe(event.created), @@ -1455,9 +1363,9 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { const session = yield* setup currentModel = Model.make({ id: "gpt-5", provider: "openai", route: OpenAIChat.route }) - const agent = yield* AgentV2.Service + const agent = yield* Agent.Service yield* agent.transform((editor) => - editor.update(AgentV2.ID.make("build"), (agent) => { + editor.update(Agent.ID.make("build"), (agent) => { agent.system = "" agent.mode = "primary" }), @@ -1477,9 +1385,9 @@ describe("SessionRunnerLLM", () => { it.effect("includes the effective default agent system before durable context", () => Effect.gen(function* () { const session = yield* setup - const agent = yield* AgentV2.Service + const agent = yield* Agent.Service yield* agent.transform((editor) => - editor.update(AgentV2.ID.make("build"), (agent) => { + editor.update(Agent.ID.make("build"), (agent) => { agent.system = "Build agent instructions" agent.mode = "primary" }), @@ -1496,17 +1404,17 @@ describe("SessionRunnerLLM", () => { it.effect("uses the configured default agent system for omitted-agent sessions", () => Effect.gen(function* () { const session = yield* setup - const agent = yield* AgentV2.Service + const agent = yield* Agent.Service yield* agent.transform((editor) => { - editor.update(AgentV2.ID.make("build"), (agent) => { + editor.update(Agent.ID.make("build"), (agent) => { agent.system = "Build agent instructions" agent.mode = "primary" }) - editor.update(AgentV2.ID.make("reviewer"), (agent) => { + editor.update(Agent.ID.make("reviewer"), (agent) => { agent.system = "Reviewer instructions" agent.mode = "primary" }) - editor.default(AgentV2.ID.make("reviewer")) + editor.default(Agent.ID.make("reviewer")) }) yield* admit(session, "First") @@ -1521,9 +1429,9 @@ describe("SessionRunnerLLM", () => { it.effect("uses only the agent prompt and initial instructions as system parts", () => Effect.gen(function* () { const session = yield* setup - const agent = yield* AgentV2.Service + const agent = yield* Agent.Service yield* agent.transform((editor) => - editor.update(AgentV2.ID.make("build"), (agent) => { + editor.update(Agent.ID.make("build"), (agent) => { agent.system = "Build agent instructions" agent.mode = "primary" }), @@ -1541,9 +1449,9 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { const session = yield* setup const { db } = yield* Database.Service - const agent = yield* AgentV2.Service + const agent = yield* Agent.Service yield* agent.transform((editor) => - editor.update(AgentV2.ID.make("reviewer"), (agent) => { + editor.update(Agent.ID.make("reviewer"), (agent) => { agent.system = "Reviewer instructions" agent.mode = "primary" }), @@ -1574,7 +1482,7 @@ describe("SessionRunnerLLM", () => { .where(eq(SessionTable.id, sessionID)) .run() .pipe(Effect.orDie) - const session = yield* SessionV2.Service + const session = yield* Session.Service yield* session.prompt({ sessionID, text: "Inspect files", resume: false }) requests.length = 0 @@ -1595,7 +1503,7 @@ describe("SessionRunnerLLM", () => { yield* setup const release = yield* Deferred.make() pluginFlushHook = Deferred.await(release) - const session = yield* SessionV2.Service + const session = yield* Session.Service yield* session.prompt({ sessionID, text: "Wait for plugins", resume: false }) requests.length = 0 @@ -1615,21 +1523,21 @@ describe("SessionRunnerLLM", () => { it.effect("updates selected-agent skill instructions after an agent switch", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service - const agents = yield* AgentV2.Service + const bus = yield* Bus.Service + const agents = yield* Agent.Service yield* agents.transform((draft) => - draft.update(AgentV2.ID.make("reviewer"), (agent) => { + draft.update(Agent.ID.make("reviewer"), (agent) => { agent.mode = "primary" }), ) - skillBaselines.set(AgentV2.ID.make("build"), "Build skills") + skillBaselines.set(Agent.ID.make("build"), "Build skills") yield* admit(session, "First") yield* session.resume(sessionID) - skillBaselines.set(AgentV2.ID.make("reviewer"), "Reviewer skills") - yield* events.publish(SessionEvent.AgentSelected, { + skillBaselines.set(Agent.ID.make("reviewer"), "Reviewer skills") + yield* bus.publish(SessionEvent.AgentSelected, { sessionID, - agent: AgentV2.ID.make("reviewer"), + agent: Agent.ID.make("reviewer"), }) yield* admit(session, "Second") yield* session.resume(sessionID) @@ -1645,17 +1553,17 @@ describe("SessionRunnerLLM", () => { it.effect("keeps the sampled agent when selection changes during observation", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service - skillBaselines.set(AgentV2.ID.make("build"), "Build skills") - skillBaselines.set(AgentV2.ID.make("reviewer"), "Reviewer skills") + const bus = yield* Bus.Service + skillBaselines.set(Agent.ID.make("build"), "Build skills") + skillBaselines.set(Agent.ID.make("reviewer"), "Reviewer skills") let switched = false systemLoadHook = Effect.suspend(() => { if (switched) return Effect.void switched = true - return events + return bus .publish(SessionEvent.AgentSelected, { sessionID, - agent: AgentV2.ID.make("reviewer"), + agent: Agent.ID.make("reviewer"), }) .pipe(Effect.asVoid) }) @@ -1672,15 +1580,15 @@ describe("SessionRunnerLLM", () => { it.effect("keeps the sampled model when selection changes during model resolution", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service let switched = false modelResolveHook = Effect.suspend(() => { if (switched) return Effect.void switched = true - return events + return bus .publish(SessionEvent.ModelSelected, { sessionID, - model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") }, + model: { id: ID.make("replacement"), providerID: Provider.ID.make("fake") }, }) .pipe(Effect.asVoid) }) @@ -1806,16 +1714,16 @@ describe("SessionRunnerLLM", () => { it.effect("keeps initial instructions and chronological updates after a model switch", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* admit(session, "First") yield* session.resume(sessionID) systemBaseline = "Changed context" yield* admit(session, "Second") yield* session.resume(sessionID) - yield* events.publish(SessionEvent.ModelSelected, { + yield* bus.publish(SessionEvent.ModelSelected, { sessionID, - model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") }, + model: { id: ID.make("replacement"), providerID: Provider.ID.make("fake") }, }) systemBaseline = "Replacement context" yield* admit(session, "Third") @@ -1844,13 +1752,13 @@ describe("SessionRunnerLLM", () => { it.effect("preserves instruction values while a source is temporarily unavailable", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* admit(session, "First") yield* session.resume(sessionID) - yield* events.publish(SessionEvent.ModelSelected, { + yield* bus.publish(SessionEvent.ModelSelected, { sessionID, - model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") }, + model: { id: ID.make("replacement"), providerID: Provider.ID.make("fake") }, }) systemUnavailable = true yield* admit(session, "Second") @@ -1871,16 +1779,16 @@ describe("SessionRunnerLLM", () => { it.effect("moves the epoch at compaction and narrates later changes", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* admit(session, "First") yield* session.resume(sessionID) - yield* events.publish(SessionEvent.Compaction.Started, { + yield* bus.publish(SessionEvent.Compaction.Started, { sessionID, reason: "manual", recent: "", }) - yield* events.publish(SessionEvent.Compaction.Ended, { + yield* bus.publish(SessionEvent.Compaction.Ended, { sessionID, reason: "manual", text: "summary", @@ -1987,7 +1895,7 @@ describe("SessionRunnerLLM", () => { }) expect( (yield* recordedEventTypes(sessionID)).filter( - (type) => type === EventV2.versionedType(SessionEvent.Compaction.Failed.type, 1), + (type) => type === Bus.versionedType(SessionEvent.Compaction.Failed.type, 1), ), ).toHaveLength(1) }), @@ -1996,7 +1904,7 @@ describe("SessionRunnerLLM", () => { it.effect("explains when manual compaction has no history", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const compaction = yield* session.compact({ sessionID }) modelResolveHook = Effect.die("model resolution should not run") @@ -2011,7 +1919,7 @@ describe("SessionRunnerLLM", () => { }) expect( (yield* recordedEventTypes(sessionID)).filter( - (type) => type === EventV2.versionedType(SessionEvent.Compaction.Failed.type, 1), + (type) => type === Bus.versionedType(SessionEvent.Compaction.Failed.type, 1), ), ).toHaveLength(1) }), @@ -2126,7 +2034,7 @@ describe("SessionRunnerLLM", () => { }) expect( (yield* recordedEventTypes(sessionID)).filter( - (type) => type === EventV2.versionedType(SessionEvent.Compaction.Failed.type, 1), + (type) => type === Bus.versionedType(SessionEvent.Compaction.Failed.type, 1), ), ).toHaveLength(1) }), @@ -2404,19 +2312,19 @@ describe("SessionRunnerLLM", () => { it.effect("uses epoch values after compaction while a source is unavailable", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* admit(session, "First") yield* session.resume(sessionID) systemBaseline = "Changed context" yield* admit(session, "Second") yield* session.resume(sessionID) - yield* events.publish(SessionEvent.Compaction.Started, { + yield* bus.publish(SessionEvent.Compaction.Started, { sessionID, reason: "manual", recent: "", }) - yield* events.publish(SessionEvent.Compaction.Ended, { + yield* bus.publish(SessionEvent.Compaction.Ended, { sessionID, reason: "manual", text: "summary", @@ -2574,7 +2482,7 @@ describe("SessionRunnerLLM", () => { it.effect("reloads a model switch before a tool-driven continuation step", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* admit(session, "Echo this") responses = [reply.tool("call-echo", "echo", { text: "hello" }), reply.stop()] @@ -2583,9 +2491,9 @@ describe("SessionRunnerLLM", () => { toolExecutionsReady = 1 const run = yield* Effect.forkChild(session.resume(sessionID)) yield* Deferred.await(toolExecutionsStarted) - yield* events.publish(SessionEvent.ModelSelected, { + yield* bus.publish(SessionEvent.ModelSelected, { sessionID, - model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") }, + model: { id: ID.make("replacement"), providerID: Provider.ID.make("fake") }, }) systemBaseline = "Replacement context" yield* Deferred.succeed(toolExecutionGate, undefined) @@ -3193,29 +3101,29 @@ describe("SessionRunnerLLM", () => { it.effect("durably fails local tools left running by a prior process before continuing", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* admit(session, "Recover interrupted tool") - yield* SessionPending.promote((yield* Database.Service).db, events, sessionID, "steer") + yield* SessionPending.promote((yield* Database.Service).db, bus, sessionID, "steer") const assistantMessageID = SessionMessage.ID.create() - yield* events.publish(SessionEvent.Step.Started, { + yield* bus.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID, - agent: AgentV2.ID.make("build"), - model: { id: ModelV2.ID.make("fake-model"), providerID: ProviderV2.ID.make("fake") }, + agent: Agent.ID.make("build"), + model: { id: ID.make("fake-model"), providerID: Provider.ID.make("fake") }, }) - yield* events.publish(SessionEvent.Tool.Input.Started, { + yield* bus.publish(SessionEvent.Tool.Input.Started, { sessionID, assistantMessageID, callID: "call-interrupted", name: "echo", }) - yield* events.publish(SessionEvent.Tool.Input.Ended, { + yield* bus.publish(SessionEvent.Tool.Input.Ended, { sessionID, assistantMessageID, callID: "call-interrupted", text: '{"text":"stale"}', }) - yield* events.publish(SessionEvent.Tool.Called, { + yield* bus.publish(SessionEvent.Tool.Called, { sessionID, assistantMessageID, callID: "call-interrupted", @@ -3250,29 +3158,29 @@ describe("SessionRunnerLLM", () => { it.effect("durably fails hosted tools left running by a prior process before continuing inline", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* admit(session, "Recover interrupted hosted tool") - yield* SessionPending.promote((yield* Database.Service).db, events, sessionID, "steer") + yield* SessionPending.promote((yield* Database.Service).db, bus, sessionID, "steer") const assistantMessageID = SessionMessage.ID.create() - yield* events.publish(SessionEvent.Step.Started, { + yield* bus.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID, - agent: AgentV2.ID.make("build"), - model: { id: ModelV2.ID.make("fake-model"), providerID: ProviderV2.ID.make("fake") }, + agent: Agent.ID.make("build"), + model: { id: ID.make("fake-model"), providerID: Provider.ID.make("fake") }, }) - yield* events.publish(SessionEvent.Tool.Input.Started, { + yield* bus.publish(SessionEvent.Tool.Input.Started, { sessionID, assistantMessageID, callID: "call-hosted-interrupted", name: "web_search", }) - yield* events.publish(SessionEvent.Tool.Input.Ended, { + yield* bus.publish(SessionEvent.Tool.Input.Ended, { sessionID, assistantMessageID, callID: "call-hosted-interrupted", text: '{"query":"stale"}', }) - yield* events.publish(SessionEvent.Tool.Called, { + yield* bus.publish(SessionEvent.Tool.Called, { sessionID, assistantMessageID, callID: "call-hosted-interrupted", @@ -3301,17 +3209,17 @@ describe("SessionRunnerLLM", () => { it.effect("durably fails pending tool input left by a prior process before continuing", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* admit(session, "Recover interrupted tool input") - yield* SessionPending.promote((yield* Database.Service).db, events, sessionID, "steer") + yield* SessionPending.promote((yield* Database.Service).db, bus, sessionID, "steer") const assistantMessageID = SessionMessage.ID.create() - yield* events.publish(SessionEvent.Step.Started, { + yield* bus.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID, - agent: AgentV2.ID.make("build"), - model: { id: ModelV2.ID.make("fake-model"), providerID: ProviderV2.ID.make("fake") }, + agent: Agent.ID.make("build"), + model: { id: ID.make("fake-model"), providerID: Provider.ID.make("fake") }, }) - yield* events.publish(SessionEvent.Tool.Input.Started, { + yield* bus.publish(SessionEvent.Tool.Input.Started, { sessionID, assistantMessageID, callID: "call-pending-interrupted", @@ -3351,10 +3259,10 @@ describe("SessionRunnerLLM", () => { it.effect("retries inbox input after prompt projection rolls back", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service const defect = new Error("fail after prompt promotion") let fail = true - yield* events.project(SessionEvent.InputPromoted, () => (fail ? Effect.die(defect) : Effect.void)) + yield* bus.project(SessionEvent.InputPromoted, () => (fail ? Effect.die(defect) : Effect.void)) yield* admit(session, "Recover promoted input") expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect) @@ -3372,8 +3280,8 @@ describe("SessionRunnerLLM", () => { it.effect("does not strand a committed promotion when a post-commit listener defects", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service - yield* events.listen((event) => + const bus = yield* Bus.Service + yield* bus.listen((event) => event.type === SessionEvent.InputPromoted.type ? Effect.die("fail after prompt promotion commits") : Effect.void, @@ -3408,7 +3316,7 @@ describe("SessionRunnerLLM", () => { it.effect("adds the parent session header to child model requests", () => Effect.gen(function* () { const session = yield* setup - const parentID = SessionV2.ID.make("ses_runner_parent") + const parentID = Session.ID.make("ses_runner_parent") const { db } = yield* Database.Service yield* db .update(SessionTable) @@ -3460,8 +3368,8 @@ describe("SessionRunnerLLM", () => { it.effect("bounds 64-character session prompt cache keys", () => Effect.gen(function* () { const session = yield* setup - const longSessionID = SessionV2.ID.make(`ses_${"a".repeat(64)}`) - const otherLongSessionID = SessionV2.ID.make(`ses_${"b".repeat(64)}`) + const longSessionID = Session.ID.make(`ses_${"a".repeat(64)}`) + const otherLongSessionID = Session.ID.make(`ses_${"b".repeat(64)}`) yield* insertSession(longSessionID) yield* insertSession(otherLongSessionID) yield* session.prompt({ @@ -3583,16 +3491,17 @@ describe("SessionRunnerLLM", () => { it.effect("returns tool-wrapped policy blocks to the model and continues", () => Effect.gen(function* () { const session = yield* setup - const registry = yield* ToolRegistry.Service - yield* registry.register( + const registry = yield* Tool.Service + yield* transformTools(registry, { - blocked: Tool.make({ + blocked: ({ + name: "blocked", description: "Fail because policy blocked execution", input: Schema.Struct({}), output: Schema.Struct({}), execute: () => - Effect.fail(new PermissionV2.BlockedError({ rules: [], permission: "blocked", resources: ["*"] })).pipe( - Effect.mapError(() => new Tool.Failure({ message: "Permission blocked" })), + Effect.fail(new Permission.BlockedError({ rules: [], permission: "blocked", resources: ["*"] })).pipe( + Effect.mapError(() => new Tool.Error({ message: "Permission blocked" })), ), }), }, @@ -3621,14 +3530,15 @@ describe("SessionRunnerLLM", () => { it.effect("interrupts runner continuation when permission approval is declined", () => Effect.gen(function* () { const session = yield* setup - const registry = yield* ToolRegistry.Service - yield* registry.register( + const registry = yield* Tool.Service + yield* transformTools(registry, { - declined: Tool.make({ + declined: ({ + name: "declined", description: "Fail because the user declined approval", input: Schema.Struct({}), output: Schema.Struct({}), - execute: () => Effect.die(new PermissionV2.DeclinedError()), + execute: () => Effect.die(new Permission.DeclinedError()), }), }, { codemode: false }, @@ -3661,16 +3571,17 @@ describe("SessionRunnerLLM", () => { it.effect("returns permission corrections to the model and continues", () => Effect.gen(function* () { const session = yield* setup - const registry = yield* ToolRegistry.Service - yield* registry.register( + const registry = yield* Tool.Service + yield* transformTools(registry, { - corrected: Tool.make({ + corrected: ({ + name: "corrected", description: "Fail with user correction feedback", input: Schema.Struct({}), output: Schema.Struct({}), execute: () => - Effect.fail(new PermissionV2.CorrectedError({ feedback: "Use another tool" })).pipe( - Effect.mapError(() => new Tool.Failure({ message: "Use another tool" })), + Effect.fail(new Permission.CorrectedError({ feedback: "Use another tool" })).pipe( + Effect.mapError(() => new Tool.Error({ message: "Use another tool" })), ), }), }, @@ -3734,8 +3645,8 @@ describe("SessionRunnerLLM", () => { it.effect("returns configured permission denials to the model and continues", () => Effect.gen(function* () { const session = yield* setup - const registry = yield* ToolRegistry.Service - yield* registry.register({ permissionfail: permissionFail }, { codemode: false }) + const registry = yield* Tool.Service + yield* transformTools(registry, { permissionfail: permissionFail }, { codemode: false }) yield* admit(session, "Reject permission") responses = [ reply.tool("call-permission", "permissionfail", {}), @@ -3772,10 +3683,11 @@ describe("SessionRunnerLLM", () => { it.effect("interrupts runner continuation when a question is cancelled", () => Effect.gen(function* () { const session = yield* setup - const registry = yield* ToolRegistry.Service - yield* registry.register( + const registry = yield* Tool.Service + yield* transformTools(registry, { - question: Tool.make({ + question: ({ + name: "question", description: "Ask the user", input: Schema.Struct({}), output: Schema.Struct({}), @@ -3976,9 +3888,9 @@ describe("SessionRunnerLLM", () => { it.effect("forces a text response on an agent's configured final step", () => Effect.gen(function* () { const session = yield* setup - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service yield* agents.transform((editor) => - editor.update(AgentV2.ID.make("build"), (agent) => { + editor.update(Agent.ID.make("build"), (agent) => { agent.steps = 2 }), ) @@ -4012,9 +3924,9 @@ describe("SessionRunnerLLM", () => { it.effect("resets the configured step allowance when steering input promotes", () => Effect.gen(function* () { const session = yield* setup - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service yield* agents.transform((editor) => - editor.update(AgentV2.ID.make("build"), (agent) => { + editor.update(Agent.ID.make("build"), (agent) => { agent.steps = 2 }), ) @@ -4136,15 +4048,15 @@ describe("SessionRunnerLLM", () => { toolExecutionsStarted = undefined const assistant = requireAssistant(yield* session.context(sessionID)) - const events = yield* recordedStepSettlementEvents(sessionID, assistant.id) - expect(events.map((event) => event.type)).toEqual([ + const bus = yield* recordedStepSettlementEvents(sessionID, assistant.id) + expect(bus.map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", "session.tool.success.2", "session.step.failed.1", ]) expect( - events.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"), + bus.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"), ).toHaveLength(1) }), ) @@ -4307,9 +4219,9 @@ describe("SessionRunnerLLM", () => { it.effect("retries a model call without consuming the logical agent step", () => Effect.gen(function* () { const session = yield* setup - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service yield* agents.transform((editor) => - editor.update(AgentV2.ID.make("build"), (agent) => { + editor.update(Agent.ID.make("build"), (agent) => { agent.steps = 2 }), ) @@ -4685,9 +4597,9 @@ describe("SessionRunnerLLM", () => { it.effect("does not continue malformed tool input past the agent step limit", () => Effect.gen(function* () { const session = yield* setup - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service yield* agents.transform((editor) => - editor.update(AgentV2.ID.make("build"), (agent) => { + editor.update(Agent.ID.make("build"), (agent) => { agent.steps = 2 }), ) @@ -4793,14 +4705,14 @@ describe("SessionRunnerLLM", () => { const context = yield* session.context(sessionID) const assistant = requireAssistant(context) - const events = yield* recordedStepSettlementEvents(sessionID, assistant.id) - expect(events.map((event) => event.type)).toEqual([ + const bus = yield* recordedStepSettlementEvents(sessionID, assistant.id) + expect(bus.map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", "session.tool.failed.2", "session.step.failed.1", ]) - expect(events[2]?.data.error).toMatchObject({ type: "unknown", message: "unexpected tool defect" }) + expect(bus[2]?.data.error).toMatchObject({ type: "unknown", message: "unexpected tool defect" }) }), ) @@ -4830,15 +4742,15 @@ describe("SessionRunnerLLM", () => { expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("Provider did not return a tool result") const assistant = requireAssistant(yield* session.context(sessionID)) - const events = yield* recordedStepSettlementEvents(sessionID, assistant.id) - expect(events.map((event) => event.type)).toEqual([ + const bus = yield* recordedStepSettlementEvents(sessionID, assistant.id) + expect(bus.map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", "session.tool.failed.2", "session.step.failed.1", ]) expect( - events.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"), + bus.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"), ).toHaveLength(1) yield* replaySessionProjection(sessionID) @@ -4868,15 +4780,15 @@ describe("SessionRunnerLLM", () => { yield* session.resume(sessionID) const assistant = requireAssistant(yield* session.context(sessionID)) - const events = yield* recordedStepSettlementEvents(sessionID, assistant.id) - expect(events.map((event) => event.type)).toEqual([ + const bus = yield* recordedStepSettlementEvents(sessionID, assistant.id) + expect(bus.map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", "session.tool.failed.2", "session.step.ended.1", ]) expect( - events.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"), + bus.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"), ).toHaveLength(1) }), ) @@ -4904,8 +4816,8 @@ describe("SessionRunnerLLM", () => { toolExecutionGate = undefined const assistant = requireAssistant(yield* session.context(sessionID)) - const events = yield* recordedStepSettlementEvents(sessionID, assistant.id) - expect(events.map((event) => ({ type: event.type, callID: event.data.callID }))).toEqual([ + const bus = yield* recordedStepSettlementEvents(sessionID, assistant.id) + expect(bus.map((event) => ({ type: event.type, callID: event.data.callID }))).toEqual([ { type: "session.step.started.1", callID: undefined }, { type: "session.tool.called.1", callID: "call-local-raw-failure" }, { type: "session.tool.called.1", callID: "call-hosted-raw-failure-pair" }, @@ -4914,7 +4826,7 @@ describe("SessionRunnerLLM", () => { { type: "session.step.failed.1", callID: undefined }, ]) expect( - events.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"), + bus.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"), ).toHaveLength(1) }), ) @@ -4932,15 +4844,15 @@ describe("SessionRunnerLLM", () => { expect(yield* session.resume(sessionID).pipe(Effect.flip)).toBe(failure) expect(requests).toHaveLength(1) const assistant = requireAssistant(yield* session.context(sessionID)) - const events = yield* recordedStepSettlementEvents(sessionID, assistant.id) - expect(events.map((event) => event.type)).toEqual([ + const bus = yield* recordedStepSettlementEvents(sessionID, assistant.id) + expect(bus.map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", "session.tool.failed.2", "session.step.failed.1", ]) expect( - events.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"), + bus.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"), ).toHaveLength(1) yield* replaySessionProjection(sessionID) expect(yield* session.context(sessionID)).toMatchObject([ diff --git a/packages/core/test/session-skill.test.ts b/packages/core/test/session-skill.test.ts index decf33a9a31b..2ab4a6555193 100644 --- a/packages/core/test/session-skill.test.ts +++ b/packages/core/test/session-skill.test.ts @@ -4,30 +4,30 @@ import { Effect, Layer, LayerMap } from "effect" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Location } from "@opencode-ai/core/location" import { LocationServiceMap } from "@opencode-ai/core/location-service-map" import type { LocationServices } from "@opencode-ai/core/location-services" -import { ProjectV2 } from "@opencode-ai/core/project" +import { Project } from "@opencode-ai/core/project" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionExecution } from "@opencode-ai/core/session/execution" import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionStore } from "@opencode-ai/core/session/store" -import { SkillV2 } from "@opencode-ai/core/skill" +import { Skill } from "@opencode-ai/core/skill" import { testEffect } from "./lib/effect" const location = Location.Ref.make({ directory: AbsolutePath.make("/project") }) -const projects = Layer.mock(ProjectV2.Service, { - resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }), +const projects = Layer.mock(Project.Service, { + resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory }), }) -const skills = Layer.mock(SkillV2.Service, { +const skills = Layer.mock(Skill.Service, { list: () => Effect.succeed([ - SkillV2.Info.make({ - id: SkillV2.ID.make("effect"), - name: SkillV2.Name.make("Effect"), + Skill.Info.make({ + id: Skill.ID.make("effect"), + name: Skill.Name.make("Effect"), description: "Effect guidance", location: AbsolutePath.make(path.resolve("/skills/effect/SKILL.md")), content: "Use Effect", @@ -45,23 +45,23 @@ const locations = Layer.effect( ) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]), + LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Session.node]), [ [LocationServiceMap.node, locations], - [ProjectV2.node, projects], + [Project.node, projects], [SessionExecution.node, SessionExecution.noopLayer], ], ), ) -describe("SessionV2.skill", () => { +describe("Session.skill", () => { it.effect("projects the caller-supplied message ID", () => Effect.gen(function* () { - const sessions = yield* SessionV2.Service + const sessions = yield* Session.Service const session = yield* sessions.create({ location }) const id = SessionMessage.ID.make("msg_caller_skill") - yield* sessions.skill({ id, sessionID: session.id, skill: SkillV2.ID.make("effect"), resume: false }) + yield* sessions.skill({ id, sessionID: session.id, skill: Skill.ID.make("effect"), resume: false }) expect(yield* sessions.messages({ sessionID: session.id })).toContainEqual( expect.objectContaining({ id, type: "skill", skill: "effect", name: "Effect", text: "Use Effect" }), diff --git a/packages/core/test/session-title.test.ts b/packages/core/test/session-title.test.ts index ce7f7f89bd35..b69abb77bc9b 100644 --- a/packages/core/test/session-title.test.ts +++ b/packages/core/test/session-title.test.ts @@ -1,12 +1,12 @@ import { expect } from "bun:test" import { LLMClient, LLMEvent, Model, type LLMRequest } from "@opencode-ai/ai" import { OpenAIChat } from "@opencode-ai/ai/protocols" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { llmClient } from "@opencode-ai/core/effect/app-node-platform" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionProjector } from "@opencode-ai/core/session/projector" @@ -14,7 +14,7 @@ import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" import { SessionTable } from "@opencode-ai/core/session/sql" import { SessionStore } from "@opencode-ai/core/session/store" import { SessionTitle } from "@opencode-ai/core/session/title" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { App } from "@opencode-ai/core/app" @@ -77,10 +77,10 @@ const it = testEffect( AppNodeBuilder.build( LayerNode.group([ Database.node, - EventV2.node, + Bus.node, SessionProjector.node, SessionStore.node, - AgentV2.node, + Agent.node, SessionTitle.node, ]), [ @@ -90,7 +90,7 @@ const it = testEffect( ), ) -const insertSession = (id: SessionV2.ID) => +const insertSession = (id: Session.ID) => Effect.gen(function* () { const { db } = yield* Database.Service yield* db @@ -114,16 +114,16 @@ const insertSession = (id: SessionV2.ID) => .pipe(Effect.orDie) }) -const prompt = (sessionID: SessionV2.ID, text: string) => +const prompt = (sessionID: Session.ID, text: string) => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const messageID = SessionMessage.ID.create() - yield* events.publish(SessionEvent.InputAdmitted, { + yield* bus.publish(SessionEvent.InputAdmitted, { sessionID, inputID: messageID, input: { type: "user", data: { text }, delivery: "steer" }, }) - yield* events.publish(SessionEvent.InputPromoted, { + yield* bus.publish(SessionEvent.InputPromoted, { sessionID, inputID: messageID, }) @@ -132,15 +132,15 @@ const prompt = (sessionID: SessionV2.ID, text: string) => it.effect("generates a title from the sole user message and renames the session", () => Effect.gen(function* () { requests = [] - const agentService = yield* AgentV2.Service + const agentService = yield* Agent.Service yield* agentService.transform((editor) => { - editor.update(AgentV2.ID.make("title"), (agent) => { + editor.update(Agent.ID.make("title"), (agent) => { agent.mode = "primary" agent.hidden = true agent.system = "You are a title generator." }) }) - const sessionID = SessionV2.ID.make("ses_title_generate") + const sessionID = Session.ID.make("ses_title_generate") yield* insertSession(sessionID) yield* prompt(sessionID, "Help me debug the failing build") @@ -171,15 +171,15 @@ it.effect("generates a title from the sole user message and renames the session" it.effect("does not generate once a second user message exists", () => Effect.gen(function* () { requests = [] - const agentService = yield* AgentV2.Service + const agentService = yield* Agent.Service yield* agentService.transform((editor) => { - editor.update(AgentV2.ID.make("title"), (agent) => { + editor.update(Agent.ID.make("title"), (agent) => { agent.mode = "primary" agent.hidden = true agent.system = "You are a title generator." }) }) - const sessionID = SessionV2.ID.make("ses_title_second_message") + const sessionID = Session.ID.make("ses_title_second_message") yield* insertSession(sessionID) yield* prompt(sessionID, "First message") yield* prompt(sessionID, "Second message") @@ -200,15 +200,15 @@ it.effect("does not generate once a second user message exists", () => it.effect("does not generate for a child session", () => Effect.gen(function* () { requests = [] - const agentService = yield* AgentV2.Service + const agentService = yield* Agent.Service yield* agentService.transform((editor) => { - editor.update(AgentV2.ID.make("title"), (agent) => { + editor.update(Agent.ID.make("title"), (agent) => { agent.mode = "primary" agent.hidden = true agent.system = "You are a title generator." }) }) - const sessionID = SessionV2.ID.make("ses_title_child") + const sessionID = Session.ID.make("ses_title_child") const { db } = yield* Database.Service yield* db .insert(ProjectTable) @@ -221,7 +221,7 @@ it.effect("does not generate for a child session", () => .values({ id: sessionID, project_id: Project.ID.global, - parent_id: SessionV2.ID.make("ses_title_parent"), + parent_id: Session.ID.make("ses_title_parent"), slug: sessionID, directory: "/project", title: "Child session - fake", @@ -246,7 +246,7 @@ it.effect("does not generate for a child session", () => it.effect("does not generate when the title agent is removed", () => Effect.gen(function* () { requests = [] - const sessionID = SessionV2.ID.make("ses_title_no_agent") + const sessionID = Session.ID.make("ses_title_no_agent") yield* insertSession(sessionID) yield* prompt(sessionID, "Help me debug the failing build") diff --git a/packages/core/test/session-tool-progress.test.ts b/packages/core/test/session-tool-progress.test.ts index 414e260e9840..fc57c92c52d0 100644 --- a/packages/core/test/session-tool-progress.test.ts +++ b/packages/core/test/session-tool-progress.test.ts @@ -3,33 +3,33 @@ import { asc, eq } from "drizzle-orm" import { DateTime, Effect, Schema } from "effect" import { Database } from "@opencode-ai/core/database/database" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Bus } from "@opencode-ai/core/bus" +import { Agent } from "@opencode-ai/core/agent" import { EventTable } from "@opencode-ai/core/event/sql" -import { ModelV2 } from "@opencode-ai/core/model" +import { Model } from "@opencode-ai/core/model" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionTable, SessionMessageTable } from "@opencode-ai/core/session/sql" import { testEffect } from "./lib/effect" -const it = testEffect(LayerNode.compile(LayerNode.group([Database.node, EventV2.node, SessionProjector.node]))) +const it = testEffect(LayerNode.compile(LayerNode.group([Database.node, Bus.node, SessionProjector.node]))) const timestamp = DateTime.makeUnsafe(1) -const model = { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") } +const model = { id: Model.ID.make("model"), providerID: Provider.ID.make("provider") } const content = (text: string) => [{ type: "text" as const, text }] as const -describe("Tool.Progress", () => { +describe("Tool.Metadata", () => { it.effect("keeps progress live-only and terminal settlements durable", () => Effect.gen(function* () { const { db } = yield* Database.Service - const service = yield* EventV2.Service - const sessionID = SessionV2.ID.make("ses_tool_progress_projector") + const service = yield* Bus.Service + const sessionID = Session.ID.make("ses_tool_progress_projector") yield* db .insert(ProjectTable) .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) @@ -52,7 +52,7 @@ describe("Tool.Progress", () => { yield* service.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID, - agent: AgentV2.ID.make("build"), + agent: Agent.ID.make("build"), model, }) const readAssistant = Effect.gen(function* () { @@ -144,9 +144,9 @@ describe("Tool.Progress", () => { .orderBy(asc(EventTable.seq)) .all() .pipe(Effect.orDie) - expect(rows.map((row) => row.type)).not.toContain(EventV2.versionedType(SessionEvent.Tool.Progress.type, 1)) - expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Success.type, 2)) - expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Failed.type, 2)) + expect(rows.map((row) => row.type)).not.toContain(Bus.versionedType(SessionEvent.Tool.Progress.type, 1)) + expect(rows.map((row) => row.type)).toContain(Bus.versionedType(SessionEvent.Tool.Success.type, 2)) + expect(rows.map((row) => row.type)).toContain(Bus.versionedType(SessionEvent.Tool.Failed.type, 2)) }), ) }) diff --git a/packages/core/test/session-wait.test.ts b/packages/core/test/session-wait.test.ts index b7c69c478002..b3861a29c3ef 100644 --- a/packages/core/test/session-wait.test.ts +++ b/packages/core/test/session-wait.test.ts @@ -3,40 +3,40 @@ import { Effect, Layer } from "effect" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Job } from "@opencode-ai/core/job" import { Location } from "@opencode-ai/core/location" -import { ProjectV2 } from "@opencode-ai/core/project" +import { Project } from "@opencode-ai/core/project" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionExecution } from "@opencode-ai/core/session/execution" import { SessionStore } from "@opencode-ai/core/session/store" import { testEffect } from "./lib/effect" const location = Location.Ref.make({ directory: AbsolutePath.make("/project") }) -const awaited: SessionV2.ID[] = [] -const projects = Layer.mock(ProjectV2.Service, { - resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }), +const awaited: Session.ID[] = [] +const projects = Layer.mock(Project.Service, { + resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory }), }) const execution = Layer.mock(SessionExecution.Service, { awaitIdle: (sessionID) => Effect.sync(() => awaited.push(sessionID)), }) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]), + LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Session.node]), [ - [ProjectV2.node, projects], + [Project.node, projects], [SessionExecution.node, execution], ], ), ) -describe("SessionV2.wait", () => { +describe("Session.wait", () => { it.effect("delegates to SessionExecution.awaitIdle", () => Effect.gen(function* () { awaited.length = 0 - const sessions = yield* SessionV2.Service + const sessions = yield* Session.Service const session = yield* sessions.create({ location }) yield* sessions.wait(session.id) diff --git a/packages/core/test/shared-schema.test.ts b/packages/core/test/shared-schema.test.ts index 937cd3e1f044..1e22da6c2898 100644 --- a/packages/core/test/shared-schema.test.ts +++ b/packages/core/test/shared-schema.test.ts @@ -1,9 +1,7 @@ import { expect, test } from "bun:test" import { Schema } from "effect" -import { AgentV2 } from "@opencode-ai/core/agent" -import { ModelV2 } from "@opencode-ai/core/model" -import { SessionV2 } from "@opencode-ai/core/session" -import { Agent } from "@opencode-ai/schema/agent" +import { Agent } from "@opencode-ai/core/agent" +import { Session } from "@opencode-ai/core/session" import { Location } from "@opencode-ai/schema/location" import { Model } from "@opencode-ai/schema/model" import { Provider } from "@opencode-ai/schema/provider" @@ -11,7 +9,6 @@ import { Project } from "@opencode-ai/schema/project" import { ProjectDirectories } from "@opencode-ai/schema/project-directories" import { PermissionV1 } from "@opencode-ai/schema/permission-v1" import { Prompt } from "@opencode-ai/schema/prompt" -import { Session } from "@opencode-ai/schema/session" import { SessionPending } from "@opencode-ai/schema/session-pending" import { SessionMessage } from "@opencode-ai/schema/session-message" import { Workspace } from "@opencode-ai/schema/workspace" @@ -26,9 +23,10 @@ import { Pty } from "@opencode-ai/schema/pty" import { Reference } from "@opencode-ai/schema/reference" import { Skill } from "@opencode-ai/schema/skill" import { AbsolutePath, DateTimeUtcFromMillis, optional, statics } from "@opencode-ai/schema/schema" -import { ProviderV2 } from "@opencode-ai/core/provider" test("Core reuses the canonical shared schemas", async () => { + const schemaAgent = await import("@opencode-ai/schema/agent") + const schemaSession = await import("@opencode-ai/schema/session") const [ coreCommand, coreConnection, @@ -37,16 +35,17 @@ test("Core reuses the canonical shared schemas", async () => { coreIntegration, coreLocation, coreLLM, + coreModel, corePermission, corePermissionV1, coreProjectCopy, corePty, coreProject, + coreProvider, coreReference, coreSessionPending, coreSessionMessage, coreSkill, - coreV2Schema, coreSchema, coreWorkspace, ] = await Promise.all([ @@ -57,25 +56,26 @@ test("Core reuses the canonical shared schemas", async () => { import("@opencode-ai/core/integration"), import("@opencode-ai/core/location"), import("@opencode-ai/ai"), + import("@opencode-ai/core/model"), import("@opencode-ai/core/permission"), import("@opencode-ai/core/v1/permission"), import("@opencode-ai/core/project/copy"), import("@opencode-ai/core/pty"), import("@opencode-ai/core/project/schema"), + import("@opencode-ai/core/provider"), import("@opencode-ai/core/reference"), import("@opencode-ai/core/session/pending"), import("@opencode-ai/core/session/message"), import("@opencode-ai/core/skill"), - import("@opencode-ai/core/v2-schema"), import("@opencode-ai/core/schema"), import("@opencode-ai/core/workspace"), ]) const schemas = [ - [AgentV2.ID, Agent.ID], - [AgentV2.Name, Agent.Name], - [AgentV2.Color, Agent.Color], - [AgentV2.Info, Agent.Info], + [Agent.ID, schemaAgent.Agent.ID], + [Agent.Name, schemaAgent.Agent.Name], + [Agent.Color, schemaAgent.Agent.Color], + [Agent.Info, schemaAgent.Agent.Info], [coreCommand.Info, Command.Info], [coreConnection.CredentialInfo, Connection.CredentialInfo], [coreConnection.EnvInfo, Connection.EnvInfo], @@ -102,19 +102,16 @@ test("Core reuses the canonical shared schemas", async () => { [coreLocation.Ref, Location.Ref], [coreLLM.ProviderMetadata, LLM.ProviderMetadata], [coreLLM.FinishReason, LLM.FinishReason], - [coreLLM.ToolTextContent, LLM.ToolTextContent], - [coreLLM.ToolFileContent, LLM.ToolFileContent], - [coreLLM.ToolContent, LLM.ToolContent], - [ModelV2.ID, Model.ID], - [ModelV2.VariantID, Model.VariantID], - [ModelV2.Ref, Model.Ref], - [ModelV2.Family, Model.Family], - [ModelV2.Capabilities, Model.Capabilities], - [ModelV2.Cost, Model.Cost], - [ModelV2.Info, Model.Info], - [ProviderV2.ID, Provider.ID], - [ProviderV2.Request, Provider.Request], - [ProviderV2.Info, Provider.Info], + [coreModel.ID, Model.ID], + [coreModel.VariantID, Model.VariantID], + [coreModel.Ref, Model.Ref], + [coreModel.Family, Model.Family], + [coreModel.Capabilities, Model.Capabilities], + [coreModel.Cost, Model.Cost], + [coreModel.Info, Model.Info], + [coreProvider.ID, Provider.ID], + [coreProvider.Request, Provider.Request], + [coreProvider.Info, Provider.Info], [corePermission.Effect, Permission.Effect], [corePermission.Rule, Permission.Rule], [corePermission.Ruleset, Permission.Ruleset], @@ -130,9 +127,9 @@ test("Core reuses the canonical shared schemas", async () => { [coreReference.LocalSource, Reference.LocalSource], [coreReference.GitSource, Reference.GitSource], [coreReference.Source, Reference.Source], - [SessionV2.ID, Session.ID], - [SessionV2.Info, Session.Info], - [SessionV2.ListAnchor, Session.ListAnchor], + [Session.ID, schemaSession.Session.ID], + [Session.Info, schemaSession.Session.Info], + [Session.ListAnchor, schemaSession.Session.ListAnchor], [coreSessionPending.Delivery, SessionPending.Delivery], [coreSessionPending.Message, SessionPending.Message], [coreSessionPending.User, SessionPending.User], @@ -162,18 +159,17 @@ test("Core reuses the canonical shared schemas", async () => { [coreSkill.EmbeddedSource, Skill.EmbeddedSource], [coreSkill.Source, Skill.Source], [coreSkill.Info, Skill.Info], - [coreV2Schema.DateTimeUtcFromMillis, DateTimeUtcFromMillis], [coreSchema.optional, optional], [coreSchema.statics, statics], [coreWorkspace.ID, Workspace.ID], ] for (const [core, shared] of schemas) expect(core).toBe(shared) - expect(Agent.Info.empty(Agent.ID.make("test"))).toEqual(AgentV2.Info.empty(AgentV2.ID.make("test"))) - expect(Model.Info.default(Provider.ID.make("test"), Model.ID.make("model"))).toEqual( - ModelV2.Info.default(ProviderV2.ID.make("test"), ModelV2.ID.make("model")), + expect(Agent.Info.empty(Agent.ID.make("test"))).toEqual(Agent.Info.empty(Agent.ID.make("test"))) + expect(coreModel.Info.default(coreProvider.ID.make("test"), coreModel.ID.make("model"))).toEqual( + Model.Info.default(Provider.ID.make("test"), Model.ID.make("model")), ) - expect(Provider.Info.empty(Provider.ID.make("test"))).toEqual(ProviderV2.Info.empty(ProviderV2.ID.make("test"))) + expect(coreProvider.Info.empty(coreProvider.ID.make("test"))).toEqual(Provider.Info.empty(Provider.ID.make("test"))) expect(Skill.Source.key(Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make("/tmp") }))).toBe( "directory:/tmp", ) diff --git a/packages/core/test/skill.test.ts b/packages/core/test/skill.test.ts index 06df3dc83954..07e29ff37804 100644 --- a/packages/core/test/skill.test.ts +++ b/packages/core/test/skill.test.ts @@ -2,13 +2,13 @@ import fs from "fs/promises" import path from "path" import { describe, expect } from "bun:test" import { Deferred, Effect, Fiber, Layer, Stream } from "effect" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { FSUtil } from "@opencode-ai/util/fs-util" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SkillV2 } from "@opencode-ai/core/skill" +import { Skill } from "@opencode-ai/core/skill" import { SkillDiscovery } from "@opencode-ai/core/skill/discovery" import { FileSystem } from "@opencode-ai/schema/filesystem" import { tmpdir } from "./fixture/tmpdir" @@ -26,7 +26,7 @@ const discovery = Layer.succeed( }), ) const it = testEffect( - AppNodeBuilder.build(LayerNode.group([SkillV2.node, AgentV2.node, EventV2.node]), [[SkillDiscovery.node, discovery]]), + AppNodeBuilder.build(LayerNode.group([Skill.node, Agent.node, Bus.node]), [[SkillDiscovery.node, discovery]]), ) function write(directory: string, name: string, description: string) { @@ -42,9 +42,9 @@ description: ${description} function waitForSkillUpdate() { return Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const deferred = yield* Deferred.make() - const fiber = yield* events.subscribe(SkillV2.Event.Updated).pipe( + const fiber = yield* bus.subscribe(Skill.Event.Updated).pipe( Stream.runForEach(() => Deferred.succeed(deferred, undefined).pipe(Effect.asVoid)), Effect.forkScoped, ) @@ -53,10 +53,10 @@ function waitForSkillUpdate() { }) } -describe("SkillV2", () => { +describe("Skill", () => { it.live("publishes updates when skill sources change", () => Effect.gen(function* () { - const skill = yield* SkillV2.Service + const skill = yield* Skill.Service yield* Effect.acquireUseRelease( waitForSkillUpdate(), @@ -88,7 +88,7 @@ describe("SkillV2", () => { await fs.writeFile(path.join(first, "foo.md"), "---\nslash: true\n---\n# foo") }) - const skill = yield* SkillV2.Service + const skill = yield* Skill.Service yield* skill.transform((editor) => { editor.source({ type: "directory", path: AbsolutePath.make(first) }) editor.source({ type: "directory", path: AbsolutePath.make(first) }) @@ -104,16 +104,16 @@ describe("SkillV2", () => { { type: "directory", path: AbsolutePath.make(second) }, ]) expect(yield* skill.list()).toEqual([ - SkillV2.Info.make({ - id: SkillV2.ID.make("foo"), - name: SkillV2.Name.make("foo"), + Skill.Info.make({ + id: Skill.ID.make("foo"), + name: Skill.Name.make("foo"), slash: true, location: AbsolutePath.make(path.join(first, "foo.md")), content: "# foo", }), { - id: SkillV2.ID.make("review"), - name: SkillV2.Name.make("review"), + id: Skill.ID.make("review"), + name: Skill.Name.make("review"), description: "Second", location: AbsolutePath.make(path.join(second, "review", "SKILL.md")), content: "# review", @@ -138,20 +138,20 @@ describe("SkillV2", () => { pulls = 0 urls.set("https://example.test/skills/", [AbsolutePath.make(tmp.path)]) - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service yield* agents.transform((editor) => - editor.update(AgentV2.ID.make("reviewer"), (agent) => { + editor.update(Agent.ID.make("reviewer"), (agent) => { agent.permissions.push({ action: "skill", resource: "deploy", effect: "deny" }) }), ) - const skill = yield* SkillV2.Service + const skill = yield* Skill.Service yield* skill.transform((editor) => editor.source({ type: "url", url: "https://example.test/skills/" })) - expect((yield* skill.list()).map((item) => item.name)).toEqual([SkillV2.Name.make("deploy")]) - expect((yield* skill.list()).map((item) => item.name)).toEqual([SkillV2.Name.make("deploy")]) + expect((yield* skill.list()).map((item) => item.name)).toEqual([Skill.Name.make("deploy")]) + expect((yield* skill.list()).map((item) => item.name)).toEqual([Skill.Name.make("deploy")]) expect(pulls).toBe(1) - expect(SkillV2.available(yield* skill.list(), (yield* agents.get(AgentV2.ID.make("reviewer")))!)).toEqual([]) + expect(Skill.available(yield* skill.list(), (yield* agents.get(Agent.ID.make("reviewer")))!)).toEqual([]) }), ), ), @@ -179,13 +179,13 @@ metadata: ) }) - const skill = yield* SkillV2.Service + const skill = yield* Skill.Service yield* skill.transform((editor) => editor.source({ type: "directory", path: AbsolutePath.make(tmp.path) })) expect(yield* skill.list()).toEqual([ { - id: SkillV2.ID.make("manual"), - name: SkillV2.Name.make("manual"), + id: Skill.ID.make("manual"), + name: Skill.Name.make("manual"), description: "Manual only", slash: true, autoinvoke: false, @@ -210,8 +210,8 @@ metadata: await write(tmp.path, "deploy", "Initial deploy") }) - const events = yield* EventV2.Service - const skill = yield* SkillV2.Service + const bus = yield* Bus.Service + const skill = yield* Skill.Service yield* skill.transform((editor) => editor.source({ type: "directory", path: AbsolutePath.make(tmp.path) })) expect((yield* skill.list()).find((item) => item.name === "deploy")?.description).toBe("Initial deploy") @@ -223,7 +223,7 @@ metadata: yield* Effect.acquireUseRelease( waitForSkillUpdate(), ({ deferred }) => - events + bus .publish(FileSystem.Event.Changed, { file, event: "change" }) .pipe(Effect.andThen(Deferred.await(deferred)), Effect.timeout("1 second")), ({ fiber }) => Fiber.interrupt(fiber), diff --git a/packages/core/test/skill/instructions.test.ts b/packages/core/test/skill/instructions.test.ts index 9ca431bc6b5c..a062c10f6add 100644 --- a/packages/core/test/skill/instructions.test.ts +++ b/packages/core/test/skill/instructions.test.ts @@ -1,53 +1,53 @@ import path from "path" import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SkillV2 } from "@opencode-ai/core/skill" +import { Skill } from "@opencode-ai/core/skill" import { SkillInstructions } from "@opencode-ai/core/skill/instructions" import { it } from "../lib/effect" import { readInitial, readUpdate } from "../lib/instructions" -const build = AgentV2.ID.make("build") -const effect = SkillV2.Info.make({ - id: SkillV2.ID.make("effect"), - name: SkillV2.Name.make("Effect"), +const build = Agent.ID.make("build") +const effect = Skill.Info.make({ + id: Skill.ID.make("effect"), + name: Skill.Name.make("Effect"), description: "Build applications with Effect", location: AbsolutePath.make(path.resolve("/skills/effect/SKILL.md")), content: "Effect guidance", }) -const hidden = SkillV2.Info.make({ - id: SkillV2.ID.make("hidden"), - name: SkillV2.Name.make("Hidden"), +const hidden = Skill.Info.make({ + id: Skill.ID.make("hidden"), + name: Skill.Name.make("Hidden"), location: AbsolutePath.make(path.resolve("/skills/hidden/SKILL.md")), content: "Undescribed guidance", }) -const denied = SkillV2.Info.make({ - id: SkillV2.ID.make("denied"), - name: SkillV2.Name.make("Denied"), +const denied = Skill.Info.make({ + id: Skill.ID.make("denied"), + name: Skill.Name.make("Denied"), description: "Must not be advertised", location: AbsolutePath.make(path.resolve("/skills/denied/SKILL.md")), content: "Denied guidance", }) -const manual = SkillV2.Info.make({ - id: SkillV2.ID.make("manual"), - name: SkillV2.Name.make("Manual"), +const manual = Skill.Info.make({ + id: Skill.ID.make("manual"), + name: Skill.Name.make("Manual"), description: "Load only when explicitly selected", autoinvoke: false, location: AbsolutePath.make(path.resolve("/skills/manual/SKILL.md")), content: "Manual guidance", }) -const layer = (list: () => SkillV2.Info[]) => +const layer = (list: () => Skill.Info[]) => AppNodeBuilder.build(SkillInstructions.node, [ - [SkillV2.node, Layer.mock(SkillV2.Service, { list: () => Effect.succeed(list()) })], + [Skill.node, Layer.mock(Skill.Service, { list: () => Effect.succeed(list()) })], ]) describe("SkillInstructions", () => { it.effect("renders described agent skills and updates the complete available list", () => { - const agent = AgentV2.Info.make({ - ...AgentV2.Info.empty(build), + const agent = Agent.Info.make({ + ...Agent.Info.empty(build), permissions: [{ action: "skill", resource: "denied", effect: "deny" }], }) let skills = [hidden, denied, manual, effect] @@ -80,10 +80,10 @@ describe("SkillInstructions", () => { }) it.effect("announces added and removed skills as deltas without restating the list", () => { - const agent = AgentV2.Info.make(AgentV2.Info.empty(build)) - const debugging = SkillV2.Info.make({ - id: SkillV2.ID.make("debugging"), - name: SkillV2.Name.make("Debugging"), + const agent = Agent.Info.make(Agent.Info.empty(build)) + const debugging = Skill.Info.make({ + id: Skill.ID.make("debugging"), + name: Skill.Name.make("Debugging"), description: "Diagnose hard bugs", location: AbsolutePath.make(path.resolve("/skills/debugging/SKILL.md")), content: "Debugging guidance", @@ -117,13 +117,13 @@ describe("SkillInstructions", () => { }) it.effect("restates the full skill list when a description changes", () => { - const agent = AgentV2.Info.make(AgentV2.Info.empty(build)) + const agent = Agent.Info.make(Agent.Info.empty(build)) let skills = [effect] return Effect.gen(function* () { const instructions = yield* SkillInstructions.Service const initialized = yield* instructions.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(readInitial)) - skills = [SkillV2.Info.make({ ...effect, description: "Build applications with Effect v4" })] + skills = [Skill.Info.make({ ...effect, description: "Build applications with Effect v4" })] expect( yield* instructions .load({ id: agent.id, info: agent }) @@ -137,8 +137,8 @@ describe("SkillInstructions", () => { }) it.effect("omits instructions when the selected agent denies all skills", () => { - const agent = AgentV2.Info.make({ - ...AgentV2.Info.empty(build), + const agent = Agent.Info.make({ + ...Agent.Info.empty(build), permissions: [{ action: "skill", resource: "*", effect: "deny" }], }) return Effect.gen(function* () { @@ -148,8 +148,8 @@ describe("SkillInstructions", () => { }) it.effect("omits instructions when a resource-specific denial follows the global denial", () => { - const agent = AgentV2.Info.make({ - ...AgentV2.Info.empty(build), + const agent = Agent.Info.make({ + ...Agent.Info.empty(build), permissions: [ { action: "skill", resource: "*", effect: "deny" }, { action: "skill", resource: "hidden", effect: "deny" }, @@ -162,8 +162,8 @@ describe("SkillInstructions", () => { }) it.effect("retains specifically allowed skills after a global denial", () => { - const agent = AgentV2.Info.make({ - ...AgentV2.Info.empty(build), + const agent = Agent.Info.make({ + ...Agent.Info.empty(build), permissions: [ { action: "skill", resource: "*", effect: "deny" }, { action: "skill", resource: "effect", effect: "allow" }, @@ -178,8 +178,8 @@ describe("SkillInstructions", () => { }) it.effect("omits instructions when a specifically allowed skill is denied again", () => { - const agent = AgentV2.Info.make({ - ...AgentV2.Info.empty(build), + const agent = Agent.Info.make({ + ...Agent.Info.empty(build), permissions: [ { action: "skill", resource: "*", effect: "deny" }, { action: "skill", resource: "effect", effect: "allow" }, diff --git a/packages/core/test/tool-edit.test.ts b/packages/core/test/tool-edit.test.ts index 8eaa9c3fbb84..a22ba21f3938 100644 --- a/packages/core/test/tool-edit.test.ts +++ b/packages/core/test/tool-edit.test.ts @@ -8,12 +8,11 @@ import { FileMutation } from "@opencode-ai/core/file-mutation" import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "@opencode-ai/core/location" import { LocationMutation } from "@opencode-ai/core/location-mutation" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" -import { EditTool } from "@opencode-ai/core/tool/edit" +import { Session } from "@opencode-ai/core/session" +import { Tool } from "@opencode-ai/core/tool" +import { EditTool } from "@opencode-ai/core/tool/plugin/edit" import { location } from "./fixture/location" import { tmpdir } from "./fixture/tmpdir" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" @@ -23,25 +22,25 @@ import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from " const editToolNode = makeLocationNode({ name: "test/edit-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(EditTool.Plugin)), - deps: [ToolRegistry.toolsNode, LocationMutation.node, FileMutation.node, FSUtil.node, PermissionV2.node], + deps: [Tool.node, LocationMutation.node, FileMutation.node, FSUtil.node, Permission.node], }) -const sessionID = SessionV2.ID.make("ses_edit_tool_test") -const assertions: PermissionV2.AssertInput[] = [] +const sessionID = Session.ID.make("ses_edit_tool_test") +const assertions: Permission.AssertInput[] = [] const writes: string[] = [] let reads = 0 let denyAction: string | undefined let afterRead = (_target: string, _content: Uint8Array): Effect.Effect => Effect.void const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: (input) => Effect.sync(() => assertions.push(input)).pipe( Effect.andThen( input.action === denyAction ? Effect.fail( - new PermissionV2.BlockedError({ + new Permission.BlockedError({ rules: [], permission: input.action, resources: input.resources, @@ -90,19 +89,19 @@ const filesystem = Layer.effect( }), ).pipe(Layer.provide(LayerNode.compile(FSUtil.node))) -const withTool = (directory: string, body: (registry: ToolRegistry.Interface) => Effect.Effect) => { +const withTool = (directory: string, body: (registry: Tool.Interface) => Effect.Effect) => { const activeLocation = Layer.succeed( Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) })), ) return Effect.gen(function* () { - return yield* body(yield* ToolRegistry.Service) + return yield* body(yield* Tool.Service) }).pipe( Effect.provide( AppNodeBuilder.build( LayerNode.group([ - ToolRegistry.node, - ToolRegistry.toolsNode, + Tool.node, + Tool.node, LocationMutation.node, FileMutation.node, editToolNode, @@ -110,8 +109,7 @@ const withTool = (directory: string, body: (registry: ToolRegistry.Inte [ [FSUtil.node, filesystem], [Location.node, activeLocation], - [PermissionV2.node, permission], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + [Permission.node, permission], ], ), ), diff --git a/packages/core/test/tool-execute.test.ts b/packages/core/test/tool-execute.test.ts index 1175c2ea4305..d679f317fe6c 100644 --- a/packages/core/test/tool-execute.test.ts +++ b/packages/core/test/tool-execute.test.ts @@ -1,21 +1,26 @@ import { expect, test } from "bun:test" -import { ExecuteTool } from "@opencode-ai/core/tool/execute" -import { Tool } from "@opencode-ai/core/tool/tool" +import { CodeModeTool } from "@opencode-ai/core/codemode/tool" +import { Tool } from "@opencode-ai/core/tool" +import { execute } from "@opencode-ai/core/tool/runtime" import { Agent } from "@opencode-ai/schema/agent" import { Session } from "@opencode-ai/schema/session" import { SessionMessage } from "@opencode-ai/schema/session-message" -import { Deferred, Effect, Fiber, Schema } from "effect" +import type { Info } from "@opencode-ai/schema/tool" +import { Effect, Schema } from "effect" const context = { sessionID: Session.ID.make("ses_execute"), agent: Agent.ID.make("build"), messageID: SessionMessage.ID.make("msg_execute"), - callID: "call_execute", + callID: Tool.CallID.make("call_execute"), progress: () => Effect.void, } +const createCodeMode = (tools: ReadonlyMap) => + CodeModeTool.create(tools, (_, tool, input, context) => execute(tool, input, context)) + test("execute describes invariant Code Mode behavior", () => { - expect(ExecuteTool.create(new Map()).description).toBe( + expect(createCodeMode(new Map()).description).toBe( [ "Run JavaScript to orchestrate tool calls and compose their results through `{ code }` in a confined Code Mode runtime.", "Imports, direct filesystem access, and timers are unavailable. Do not use `fetch`; all external access goes through `tools`.", @@ -28,109 +33,92 @@ test("execute describes invariant Code Mode behavior", () => { }) test("canonical execution distinguishes declared, model-only, and raw schema outputs", async () => { - const declared = Tool.make({ + const declared: Info = ({ + name: "declared", description: "Declared", input: Schema.Struct({ value: Schema.String }), output: Schema.Struct({ value: Schema.String }), execute: ({ value }) => Effect.succeed({ output: { value } }), }) - const modelOnly = Tool.make({ + const modelOnlyInput = Schema.Struct({}) + const modelOnly = ({ + name: "model_only", description: "Model only", - input: Schema.Struct({}), + input: modelOnlyInput, execute: () => Effect.succeed({ content: "visible only", metadata: { kind: "model" } }), - }) - const raw = Tool.make({ + }) satisfies Info + const raw: Info = ({ + name: "raw", description: "Raw", input: {}, output: {}, execute: (input) => Effect.succeed({ output: input, content: "raw" }), }) - expect(await Effect.runPromise(Tool.execute(declared, { value: "encoded" }, context))).toEqual({ + expect(await Effect.runPromise(execute(declared, { value: "encoded" }, context))).toEqual({ output: { value: "encoded" }, content: [{ type: "text", text: '{"value":"encoded"}' }], }) - expect(await Effect.runPromise(Tool.execute(modelOnly, {}, context))).toEqual({ + expect(await Effect.runPromise(execute(modelOnly, {}, context))).toEqual({ + output: undefined, content: [{ type: "text", text: "visible only" }], metadata: { kind: "model" }, }) - expect(await Effect.runPromise(Tool.execute(raw, { unchecked: true }, context))).toEqual({ + expect(await Effect.runPromise(execute(raw, { unchecked: true }, context))).toEqual({ output: { unchecked: true }, content: [{ type: "text", text: "raw" }], }) }) test("declared outputs cannot bypass validation and raw outputs stay JSON-compatible", async () => { - const missing: Tool.Any = { + const missing: Info = { + name: "missing", description: "Missing output", input: Schema.Struct({}), output: Schema.String, execute: () => Effect.succeed({ content: "not an output" }), } - const invalid: Tool.Any = { + const invalid: Info = { + name: "invalid", description: "Invalid raw output", input: {}, output: {}, execute: () => Effect.succeed({ output: 1n, content: "not JSON" }), } - expect((await Effect.runPromiseExit(Tool.execute(missing, {}, context))).toString()).toContain( + expect((await Effect.runPromiseExit(execute(missing, {}, context))).toString()).toContain( "Tool did not return its declared output", ) - expect((await Effect.runPromiseExit(Tool.execute(invalid, {}, context))).toString()).toContain( + expect((await Effect.runPromiseExit(execute(invalid, {}, context))).toString()).toContain( "Tool returned a non-JSON value", ) }) -test("execute preserves successful results with visible unhandled rejections", async () => { - const child = Tool.make({ - description: "Always fail", - input: Schema.Struct({}), - output: Schema.String, - execute: () => Effect.fail(new Tool.Failure({ message: "Lookup refused" })), - }) - const execute = ExecuteTool.create(new Map([["fail", { tool: child, name: "fail", permission: "fail" }]])) - const result = await Effect.runPromise(Tool.execute(execute, { code: `tools.fail({}); return "done"` }, context)) - - expect(result.metadata).toEqual({ toolCalls: [{ tool: "fail", status: "error" }] }) - expect(result.content).toEqual([ - { - type: "text", - text: [ - "done", - "", - "Warnings:", - "- [ToolFailure] Unhandled rejection from an un-awaited promise: Lookup refused", - ].join("\n"), - }, - ]) -}) - test("execute supports callable namespace tools", async () => { - const callable = Tool.make({ + const callable: Info = ({ + name: "admin", description: "Administer Slack", input: Schema.Struct({}), output: Schema.String, + options: { namespace: "slack" }, execute: () => Effect.succeed({ output: "admin" }), }) - const child = Tool.make({ + const child: Info = ({ + name: "create", description: "Create a Slack resource", input: Schema.Struct({}), output: Schema.String, + options: { namespace: "slack.admin" }, execute: () => Effect.succeed({ output: "created" }), }) - const execute = ExecuteTool.create( + const codeMode = createCodeMode( new Map([ - ["slack_admin", { tool: callable, name: "admin", namespace: "slack", permission: "slack_admin" }], - [ - "slack_admin_create", - { tool: child, name: "create", namespace: "slack.admin", permission: "slack_admin_create" }, - ], + ["slack_admin", callable], + ["slack_admin_create", child], ]), ) const result = await Effect.runPromise( - Tool.execute( - execute, + codeMode.execute( { code: "return [await tools.slack.admin({}), await tools.slack.admin.create({})]" }, context, ), @@ -144,46 +132,3 @@ test("execute supports callable namespace tools", async () => { }) expect(result.content).toEqual([{ type: "text", text: '[\n "admin",\n "created"\n]' }]) }) - -test("execute marks every admitted child call failed when interrupted", async () => { - const child = Tool.make({ - description: "Wait forever", - input: Schema.Struct({ id: Schema.Number }), - output: Schema.String, - execute: () => Effect.never, - }) - const execute = ExecuteTool.create(new Map([["wait", { tool: child, name: "wait", permission: "wait" }]])) - const updates: Tool.Metadata[] = [] - - await Effect.runPromise( - Effect.gen(function* () { - const started = yield* Deferred.make() - const fiber = yield* Tool.execute( - execute, - { code: "return await Promise.all([tools.wait({ id: 1 }), tools.wait({ id: 2 })])" }, - { - ...context, - progress: (update) => - Effect.gen(function* () { - updates.push(update) - if (updates.length > 1) return - yield* Deferred.succeed(started, undefined) - yield* Effect.never - }), - }, - ).pipe(Effect.forkChild) - yield* Deferred.await(started) - yield* Effect.yieldNow - yield* Effect.yieldNow - yield* Fiber.interrupt(fiber) - }), - ) - - expect(updates[0]).toEqual({ toolCalls: [{ tool: "wait", status: "running", input: { id: 1 } }] }) - expect(updates.at(-1)).toEqual({ - toolCalls: [ - { tool: "wait", status: "error", input: { id: 1 } }, - { tool: "wait", status: "error", input: { id: 2 } }, - ], - }) -}) diff --git a/packages/core/test/tool-output-store.test.ts b/packages/core/test/tool-output-store.test.ts deleted file mode 100644 index 2e22402e104c..000000000000 --- a/packages/core/test/tool-output-store.test.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { describe, expect } from "bun:test" -import path from "path" -import { Cause, Effect, Exit, Fiber, Layer, Option } from "effect" -import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { FSUtil } from "@opencode-ai/util/fs-util" -import { Global } from "@opencode-ai/util/global" -import { Config } from "@opencode-ai/core/config" -import { ConfigToolOutput } from "@opencode-ai/core/config/tool-output" -import { SessionV2 } from "@opencode-ai/core/session" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" -import { testEffect } from "./lib/effect" -import { tmpdir } from "./fixture/tmpdir" - -const sessionID = SessionV2.ID.make("ses_tool_output_store") - -const withStore = ( - body: (input: { root: string; store: ToolOutputStore.Interface; fs: FSUtil.Interface }) => Effect.Effect, - config?: Config.Info, -) => - Effect.acquireUseRelease( - Effect.promise(() => tmpdir()), - (tmp) => { - const global = Global.layerWith({ data: tmp.path }) - const configured = config - ? Layer.succeed( - Config.Service, - Config.Service.of({ - entries: () => Effect.succeed([new Config.Document({ type: "document", info: config })]), - }), - ) - : Layer.empty - - const store = AppNodeBuilder.build(LayerNode.group([ToolOutputStore.node, FSUtil.node]), [ - [Global.node, global], - [Config.node, configured], - ]) - return Effect.gen(function* () { - return yield* body({ root: tmp.path, store: yield* ToolOutputStore.Service, fs: yield* FSUtil.Service }) - }).pipe(Effect.provide(store)) - }, - (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), - ) - -const it = testEffect(Layer.empty) - -describe("ToolOutputStore", () => { - it.live("bounds the provider-facing text channel with one managed file", () => - withStore(({ store, fs }) => - Effect.gen(function* () { - const first = "HEAD-" + "x".repeat(30_000) - const second = "y".repeat(30_000) + "-TAIL" - const result = yield* store.bound({ - sessionID, - callID: "call-aggregate", - content: [ - { type: "text", text: first }, - { type: "text", text: second }, - ], - }) - expect(result.outputPaths).toHaveLength(1) - expect(yield* fs.readFileString(result.outputPaths[0])).toBe(first + second) - if (result.content[0]?.type !== "text") throw new Error("expected text preview") - expect(Buffer.byteLength(result.content[0].text)).toBeLessThanOrEqual(ToolOutputStore.MAX_BYTES) - }), - ), - ) - - it.live("preserves native media without applying an execution media limit", () => - withStore(({ store }) => - Effect.gen(function* () { - const data = "a".repeat(6 * 1024 * 1024) - const result = yield* store.bound({ - sessionID, - callID: "call-file", - content: [{ type: "file", uri: `data:image/png;base64,${data}`, mime: "image/png", name: "pixel.png" }], - }) - expect(result.outputPaths).toEqual([]) - expect(result.content).toHaveLength(1) - expect(result.content[0]).toEqual({ - type: "file", - uri: `data:image/png;base64,${data}`, - mime: "image/png", - name: "pixel.png", - }) - }), - ), - ) - - it.live("preserves native media when bounding text", () => - withStore(({ store, fs }) => - Effect.gen(function* () { - const text = "x".repeat(ToolOutputStore.MAX_BYTES + 1) - const media = { - type: "file" as const, - uri: "data:image/png;base64,aGVsbG8=", - mime: "image/png", - name: "pixel.png", - } - const result = yield* store.bound({ - sessionID, - callID: "call-text-and-media", - content: [{ type: "text", text }, media], - }) - - expect(result.content[1]).toEqual(media) - expect(yield* fs.readFileString(result.outputPaths[0])).toBe(text) - }), - ), - ) - - it.live("returns content within the limits unchanged", () => - withStore(({ store }) => - Effect.gen(function* () { - const text = "x".repeat(30_000) - const content = [{ type: "text" as const, text }] - expect(yield* store.bound({ sessionID, callID: "call-duplicated", content })).toEqual({ - content, - outputPaths: [], - }) - }), - ), - ) - - it.live("fails oversized execution when complete retention cannot be written", () => - withStore(({ root, store, fs }) => - Effect.gen(function* () { - yield* fs.writeFileString(path.join(root, "tool-output"), "not a directory") - const exit = yield* store - .bound({ - sessionID, - callID: "call-lossy", - content: [{ type: "text", text: "x".repeat(ToolOutputStore.MAX_BYTES + 1) }], - }) - .pipe(Effect.exit) - expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) - expect(Option.getOrUndefined(Cause.findErrorOption(exit.cause))?._tag).toBe("ToolOutputStore.StorageError") - }), - ), - ) - - it.live("preserves interruption while retaining complete output", () => - Effect.gen(function* () { - const root = yield* Effect.promise(() => tmpdir()) - const blockedFilesystem = Layer.effect( - FSUtil.Service, - Effect.gen(function* () { - const fs = yield* FSUtil.Service - return FSUtil.Service.of({ - ...fs, - ensureDir: () => Effect.void, - writeFileString: () => Effect.never, - }) - }), - ).pipe(Layer.provide(LayerNode.compile(FSUtil.node))) - const store = AppNodeBuilder.build(ToolOutputStore.nodeWithoutConfig, [ - [Global.node, Global.layerWith({ data: root.path })], - [FSUtil.node, blockedFilesystem], - ]) - const exit = yield* Effect.gen(function* () { - const service = yield* ToolOutputStore.Service - const fiber = yield* service - .bound({ - sessionID, - callID: "call-interrupted", - content: [{ type: "text", text: "x".repeat(ToolOutputStore.MAX_BYTES + 1) }], - }) - .pipe(Effect.forkChild) - yield* Fiber.interrupt(fiber) - return yield* Fiber.await(fiber) - }).pipe(Effect.provide(store)) - expect(Exit.isFailure(exit) && Cause.hasInterrupts(exit.cause)).toBe(true) - yield* Effect.promise(() => root[Symbol.asyncDispose]()) - }), - ) - - it.live("honors configured limits", () => - withStore( - ({ store }) => - Effect.gen(function* () { - expect(yield* store.limits()).toEqual({ maxLines: 2, maxBytes: 1_000 }) - const result = yield* store.bound({ - sessionID, - callID: "call-config", - content: [{ type: "text", text: "one\ntwo\nthree" }], - }) - expect(result.outputPaths).toHaveLength(1) - }), - new Config.Info({ tool_output: new ConfigToolOutput.Info({ max_lines: 2, max_bytes: 1_000 }) }), - ), - ) - - it.live("cleans expired managed files and preserves unrelated files", () => - withStore(({ root, store, fs }) => - Effect.gen(function* () { - const old = path.join(root, "tool-output", "tool_old") - const recent = path.join(root, "tool-output", "tool_recent") - const unrelated = path.join(root, "tool-output", "keep.txt") - yield* fs.ensureDir(path.join(root, "tool-output")) - yield* fs.writeFileString(old, "old") - yield* fs.writeFileString(recent, "recent") - yield* fs.writeFileString(unrelated, "keep") - const expired = new Date(Date.now() - 8 * 24 * 60 * 60 * 1_000) - yield* fs.utimes(old, expired, expired) - yield* store.cleanup() - expect(yield* fs.exists(old)).toBe(false) - expect(yield* fs.exists(recent)).toBe(true) - expect(yield* fs.exists(unrelated)).toBe(true) - }), - ), - ) -}) diff --git a/packages/core/test/tool-patch.test.ts b/packages/core/test/tool-patch.test.ts index e573edb19b46..471bf9e795c3 100644 --- a/packages/core/test/tool-patch.test.ts +++ b/packages/core/test/tool-patch.test.ts @@ -7,12 +7,11 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "@opencode-ai/core/location" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" -import { PatchTool } from "@opencode-ai/core/tool/patch" +import { Session } from "@opencode-ai/core/session" +import { Tool } from "@opencode-ai/core/tool" +import { PatchTool } from "@opencode-ai/core/tool/plugin/patch" import { location } from "./fixture/location" import { tmpdir } from "./fixture/tmpdir" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" @@ -22,11 +21,11 @@ import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from " const patchToolNode = makeLocationNode({ name: "test/patch-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(PatchTool.Plugin)), - deps: [ToolRegistry.toolsNode, FSUtil.node, Location.node, PermissionV2.node], + deps: [Tool.node, FSUtil.node, Location.node, Permission.node], }) -const sessionID = SessionV2.ID.make("ses_patch_tool_test") -const assertions: PermissionV2.AssertInput[] = [] +const sessionID = Session.ID.make("ses_patch_tool_test") +const assertions: Permission.AssertInput[] = [] let denyAction: string | undefined let failRemoveTarget: string | undefined let failRemoveErrorTarget: string | undefined @@ -36,8 +35,8 @@ let editApproved = false let afterEditApproval = (): Effect.Effect => Effect.void const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: (input) => Effect.sync(() => { assertions.push(input) @@ -47,7 +46,7 @@ const permission = Layer.succeed( Effect.andThen( input.action === denyAction ? Effect.fail( - new PermissionV2.BlockedError({ + new Permission.BlockedError({ rules: [], permission: input.action, resources: input.resources, @@ -120,7 +119,7 @@ const filesystem = Layer.effect( const withTool = ( directory: string, - body: (registry: ToolRegistry.Interface) => Effect.Effect, + body: (registry: Tool.Interface) => Effect.Effect, projectDirectory = directory, ) => { const activeLocation = Layer.succeed( @@ -130,14 +129,13 @@ const withTool = ( ), ) return Effect.gen(function* () { - return yield* body(yield* ToolRegistry.Service) + return yield* body(yield* Tool.Service) }).pipe( Effect.provide( - AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, patchToolNode]), [ + AppNodeBuilder.build(LayerNode.group([Tool.node, patchToolNode]), [ [FSUtil.node, filesystem], [Location.node, activeLocation], - [PermissionV2.node, permission], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + [Permission.node, permission], ]), ), ) @@ -157,7 +155,7 @@ const exists = (target: string) => ), ) const it = testEffect(Layer.empty) -const withTempTool = (body: (directory: string, registry: ToolRegistry.Interface) => Effect.Effect) => +const withTempTool = (body: (directory: string, registry: Tool.Interface) => Effect.Effect) => Effect.acquireUseRelease( Effect.promise(() => tmpdir()), (tmp) => { @@ -196,7 +194,7 @@ describe("PatchTool", () => { text: "Success. Updated the following files:\nA nested/new.txt\nM update.txt\nD remove.txt", }, ]) - const modelText = settled.content[0]?.type === "text" ? settled.content[0].text : "" + const modelText = settled.content?.[0]?.type === "text" ? settled.content[0].text : "" if (process.platform === "win32") expect(modelText).not.toContain("\\") expect(settled.output).toMatchObject({ applied: [ diff --git a/packages/core/test/tool-question.test.ts b/packages/core/test/tool-question.test.ts index f63e7f6d2d72..8cbda1118d54 100644 --- a/packages/core/test/tool-question.test.ts +++ b/packages/core/test/tool-question.test.ts @@ -3,19 +3,18 @@ import { Cause, Effect, Exit, Fiber, Layer } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Form } from "@opencode-ai/core/form" -import { PermissionV2 } from "@opencode-ai/core/permission" -import { SessionV2 } from "@opencode-ai/core/session" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { QuestionTool } from "@opencode-ai/core/tool/question" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { Permission } from "@opencode-ai/core/permission" +import { Session } from "@opencode-ai/core/session" +import { Tool } from "@opencode-ai/core/tool" +import { QuestionTool } from "@opencode-ai/core/tool/plugin/question" import { Image } from "@opencode-ai/core/image" import { testEffect } from "./lib/effect" import { imagePassthrough } from "./lib/image" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from "./lib/tool" -const sessionID = SessionV2.ID.make("ses_question_tool_test") -const assertions: PermissionV2.AssertInput[] = [] +const sessionID = Session.ID.make("ses_question_tool_test") +const assertions: Permission.AssertInput[] = [] let captured: Form.CreateInput | undefined let reject = false let deny = false @@ -30,14 +29,14 @@ const questionInput = { ], } const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: (input) => Effect.sync(() => assertions.push(input)).pipe( Effect.andThen( deny ? Effect.fail( - new PermissionV2.BlockedError({ + new Permission.BlockedError({ rules: [], permission: input.action, resources: input.resources, @@ -78,14 +77,13 @@ const form = Layer.succeed( const questionToolNode = makeLocationNode({ name: "test/question-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(QuestionTool.Plugin)), - deps: [ToolRegistry.toolsNode, PermissionV2.node, Form.node], + deps: [Tool.node, Permission.node, Form.node], }) const it = testEffect( - AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, questionToolNode]), [ - [PermissionV2.node, permission], + AppNodeBuilder.build(LayerNode.group([Tool.node, questionToolNode]), [ + [Permission.node, permission], [Form.node, form], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], [Image.node, imagePassthrough], ]), ) @@ -95,7 +93,7 @@ describe("QuestionTool", () => { Effect.gen(function* () { captured = undefined deny = true - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( (yield* toolDefinitions(registry, [{ action: "question", resource: "*", effect: "deny" }])).map( @@ -126,7 +124,7 @@ describe("QuestionTool", () => { captured = undefined reject = false deny = false - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const questions = [ { question: "What should happen?", @@ -204,7 +202,7 @@ describe("QuestionTool", () => { captured = undefined reject = false deny = false - const registryService = yield* ToolRegistry.Service + const registryService = yield* Tool.Service yield* executeTool(registryService, { sessionID, @@ -234,7 +232,7 @@ describe("QuestionTool", () => { captured = undefined reject = true deny = false - const registryService = yield* ToolRegistry.Service + const registryService = yield* Tool.Service const fiber = yield* executeTool(registryService, { sessionID, ...toolIdentity, diff --git a/packages/core/test/tool-read.test.ts b/packages/core/test/tool-read.test.ts index a176b3b0c87c..3e8427c447b4 100644 --- a/packages/core/test/tool-read.test.ts +++ b/packages/core/test/tool-read.test.ts @@ -9,15 +9,14 @@ import { FileSystem } from "@opencode-ai/core/filesystem" import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "@opencode-ai/core/location" import { Image } from "@opencode-ai/core/image" -import { PermissionV2 } from "@opencode-ai/core/permission" -import { SessionV2 } from "@opencode-ai/core/session" +import { Permission } from "@opencode-ai/core/permission" +import { Session } from "@opencode-ai/core/session" import { AbsolutePath } from "@opencode-ai/core/schema" import { Global } from "@opencode-ai/util/global" import { LocationMutation } from "@opencode-ai/core/location-mutation" import { location } from "./fixture/location" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" -import { ReadTool } from "@opencode-ai/core/tool/read" +import { Tool } from "@opencode-ai/core/tool" +import { ReadTool } from "@opencode-ai/core/tool/plugin/read" import { ReadToolFileSystem } from "@opencode-ai/core/tool/read-filesystem" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { SessionInstructions } from "@opencode-ai/core/session/instructions" @@ -28,18 +27,18 @@ const readToolNode = makeLocationNode({ name: "test/read-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(ReadTool.Plugin)), deps: [ - ToolRegistry.toolsNode, + Tool.node, ReadToolFileSystem.node, LocationMutation.node, Image.node, - PermissionV2.node, + Permission.node, SessionInstructions.node, FSUtil.node, Location.node, ], }) -const assertions: PermissionV2.AssertInput[] = [] +const assertions: Permission.AssertInput[] = [] const missingPath = "__missing_read_target__.txt" const missingAbsolutePath = path.join(process.cwd(), missingPath) const readCalls: { @@ -76,8 +75,8 @@ const reader = Layer.succeed( ) let allow = true const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: (input) => Effect.sync(() => { assertions.push(input) @@ -86,7 +85,7 @@ const permission = Layer.succeed( allow ? Effect.void : Effect.fail( - new PermissionV2.BlockedError({ + new Permission.BlockedError({ rules: [], permission: input.action, resources: input.resources, @@ -159,20 +158,19 @@ const unavailableImage = Layer.succeed( Image.Service.of({ normalize: () => Effect.fail(new Image.ResizerUnavailableError()) }), ) const readLayer = (imageLayer: Layer.Layer) => - AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, readToolNode]), [ + AppNodeBuilder.build(LayerNode.group([Tool.node, readToolNode]), [ [ReadToolFileSystem.node, reader], - [PermissionV2.node, permission], + [Permission.node, permission], [Config.node, config], [Image.node, imageLayer], [LocationMutation.node, mutation], [FSUtil.node, testFileSystem], [Location.node, locationLayer], [Global.node, Global.layerWith({ data: Global.Path.data })], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], ]) const it = testEffect(readLayer(imageLayer)) const itWithoutResizer = testEffect(readLayer(unavailableImage)) -const sessionID = SessionV2.ID.make("ses_read_tool_test") +const sessionID = Session.ID.make("ses_read_tool_test") describe("ReadTool", () => { beforeEach(() => { @@ -195,7 +193,7 @@ describe("ReadTool", () => { it.effect("registers, authorizes, and reads through the location filesystem", () => Effect.gen(function* () { - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["read", "execute"]) expect( @@ -229,7 +227,7 @@ describe("ReadTool", () => { it.effect("asks for external_directory approval before reading an external absolute path", () => Effect.gen(function* () { - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const external = path.join(path.parse(process.cwd()).root, "external-read", "notes.txt") expect( @@ -261,7 +259,7 @@ describe("ReadTool", () => { encoding: "base64", mime: "image/png", } - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const execution = yield* executeTool(registry, { sessionID, @@ -313,7 +311,7 @@ describe("ReadTool", () => { encoding: "base64", mime: "image/png", } - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const settled = yield* executeTool(registry, { sessionID, @@ -321,7 +319,6 @@ describe("ReadTool", () => { call: { type: "tool-call", id: "call-large-image", name: "read", input: { path: "large.png" } }, }) - expect(settled.outputPaths).toBeUndefined() expect(settled.status).toBe("completed") if (settled.status !== "completed") return expect(settled.output).toMatchObject({ @@ -347,7 +344,7 @@ describe("ReadTool", () => { encoding: "base64", mime: "image/png", } - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -371,7 +368,7 @@ describe("ReadTool", () => { encoding: "base64", mime: "image/png", } - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -412,7 +409,7 @@ describe("ReadTool", () => { }), }), ] - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -451,7 +448,7 @@ describe("ReadTool", () => { }), }), ] - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const result = yield* executeTool(registry, { sessionID, ...toolIdentity, @@ -460,7 +457,7 @@ describe("ReadTool", () => { expect(result.status).toBe("completed") if (result.status !== "completed") return - const media = result.content[1] + const media = result.content?.[1] expect(media?.type).toBe("file") if (media?.type !== "file") return const resized = photon.PhotonImage.new_from_byteslice(Buffer.from(media.uri.split(",")[1] ?? "", "base64")) @@ -490,7 +487,7 @@ describe("ReadTool", () => { }), }), ] - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -518,7 +515,7 @@ describe("ReadTool", () => { encoding: "base64", mime: "image/png", } - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -536,7 +533,7 @@ describe("ReadTool", () => { it.effect("returns expected filesystem failures to the model", () => Effect.gen(function* () { readFailure = new ReadToolFileSystem.BinaryFileError({ resource: "archive.dat" }) - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -559,7 +556,7 @@ describe("ReadTool", () => { it.effect("preserves unexpected filesystem defects", () => Effect.gen(function* () { resolveFailure = new Error("unexpected") - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( Exit.isFailure( @@ -576,7 +573,7 @@ describe("ReadTool", () => { it.effect("does not read when permission is denied", () => Effect.gen(function* () { allow = false - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -591,7 +588,7 @@ describe("ReadTool", () => { it.effect("returns missing paths as model-visible tool failures", () => Effect.gen(function* () { - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -610,7 +607,7 @@ describe("ReadTool", () => { it.effect("lists a bounded directory page through read", () => Effect.gen(function* () { resolvedType = "directory" - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -633,7 +630,7 @@ describe("ReadTool", () => { Effect.gen(function* () { allow = false resolvedType = "directory" - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -648,7 +645,7 @@ describe("ReadTool", () => { it.effect("preserves unexpected resolution defects", () => Effect.gen(function* () { - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service resolveFailure = new Error("missing") expect( @@ -675,7 +672,7 @@ describe("ReadTool", () => { truncated: true, next: 3, }) - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -707,7 +704,7 @@ describe("ReadTool", () => { encoding: "base64", mime: "application/octet-stream", } - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { diff --git a/packages/plugin/test/tool.test.ts b/packages/core/test/tool-schema.test.ts similarity index 68% rename from packages/plugin/test/tool.test.ts rename to packages/core/test/tool-schema.test.ts index 19017cab20e1..72e053ab8bbf 100644 --- a/packages/plugin/test/tool.test.ts +++ b/packages/core/test/tool-schema.test.ts @@ -1,18 +1,20 @@ import { expect, test } from "bun:test" import { Effect, Schema } from "effect" -import * as Tool from "../src/v2/effect/tool" +import type { Info } from "@opencode-ai/schema/tool" +import { Tool } from "../src/tool" +import { definition, execute } from "../src/tool/runtime" -test("tools remain valid across separate module instances", async () => { - const ForeignTool = await import(`${new URL("../src/v2/effect/tool.ts", import.meta.url).href}?foreign`) +test("tools are structural values", async () => { const config = { + name: "foreign", description: "Foreign tool", input: Schema.Struct({ value: Schema.String }), output: Schema.Struct({ ok: Schema.Boolean }), execute: () => Effect.succeed({ output: { ok: true } }), } - const tool = ForeignTool.make(config) + const tool: Info = config - expect(Tool.toLLMDefinition("foreign", tool)).toEqual({ + expect(definition(tool)).toEqual({ name: "foreign", description: "Foreign tool", inputSchema: { @@ -28,15 +30,14 @@ test("tools remain valid across separate module instances", async () => { additionalProperties: false, }, }) - expect(await Effect.runPromise(Tool.decodeInput(tool.input, { value: "input" }))).toEqual({ value: "input" }) }) test("portable schemas validate and describe typed tools", async () => { - const input: Tool.StandardSchemaType<{ count: string }, { count: number }> = { + const input = { "~standard": { version: 1, vendor: "test", - validate: (value) => { + validate: (value: unknown) => { if (typeof value !== "object" || value === null || !("count" in value) || typeof value.count !== "string") return { issues: [{ message: "count must be numeric" }] } const count = Number(value.count) @@ -48,41 +49,41 @@ test("portable schemas validate and describe typed tools", async () => { }, }, } - const output: Tool.StandardSchemaType = { + const output = { "~standard": { version: 1, vendor: "test", - validate: (value) => ({ value: String(value) }), + validate: (value: unknown) => ({ value: String(value) }), jsonSchema: { input: () => ({ type: "number" }), output: () => ({ type: "string" }), }, }, } - const tool = Tool.make({ + const tool: Info = ({ + name: "portable", description: "Portable tool", input, output, execute: ({ count }) => Effect.succeed({ output: count + 1 }), }) - expect(Tool.toLLMDefinition("portable", tool)).toEqual({ + expect(definition(tool)).toEqual({ name: "portable", description: "Portable tool", inputSchema: { type: "object", properties: { count: { type: "string" } } }, outputSchema: { type: "string" }, }) - const decoded = await Effect.runPromise(Tool.decodeInput(tool.input, { count: "41" })) - expect(decoded).toEqual({ count: 41 }) - expect(await Effect.runPromise(Tool.encodeOutput(tool.output, 42))).toBe("42") + const result = await Effect.runPromise(execute(tool, { count: "41" }, {} as Tool.Context)) + expect(result.output).toBe("42") }) test("portable schema failures become tool failures", async () => { - const input: Tool.StandardSchemaType = { + const input = { "~standard": { version: 1, vendor: "test", - validate: () => ({ issues: [{ message: "expected a string" }] }), + validate: (_value: unknown) => ({ issues: [{ message: "expected a string" }] }), jsonSchema: { input: () => ({ type: "string" }), output: () => ({ type: "string" }), @@ -90,14 +91,26 @@ test("portable schema failures become tool failures", async () => { }, } - const error = await Effect.runPromiseExit(Tool.decodeInput(input, 1)) + const error = await Effect.runPromiseExit( + execute( + { + name: "invalid", + description: "Invalid", + input, + execute: () => Effect.succeed({ content: "unused" }), + }, + 1, + {} as Tool.Context, + ), + ) expect(error.toString()).toContain("Invalid tool input: expected a string") }) test("canonical results carry metadata with typed output", async () => { const input = Schema.Struct({ value: Schema.String }) const output = Schema.Struct({ value: Schema.String, internal: Schema.Boolean }) - const tool = Tool.make({ + const tool: Info = ({ + name: "annotated", description: "Annotated tool", input, output, @@ -112,16 +125,21 @@ test("canonical results carry metadata with typed output", async () => { }) test("raw JSON schemas are render-only and omitted output means model-only", async () => { - const tool = Tool.make({ + const input = { type: "object", properties: { value: { type: "string" } } } + const tool: Info = ({ + name: "raw", description: "Raw tool", - input: { type: "object", properties: { value: { type: "string" } } }, + input, execute: (input) => Effect.succeed({ content: JSON.stringify(input) }), }) - expect(Tool.toLLMDefinition("raw", tool)).toEqual({ + expect(definition(tool)).toEqual({ name: "raw", description: "Raw tool", inputSchema: { type: "object", properties: { value: { type: "string" } } }, }) - expect(await Effect.runPromise(Tool.decodeInput(tool.input, { value: 1 }))).toEqual({ value: 1 }) + expect(await Effect.runPromise(execute(tool, { value: 1 }, {} as Tool.Context))).toEqual({ + output: undefined, + content: [{ type: "text", text: '{"value":1}' }], + }) }) diff --git a/packages/core/test/tool-search.test.ts b/packages/core/test/tool-search.test.ts index 8b688a23ffd4..abe59a75dfbb 100644 --- a/packages/core/test/tool-search.test.ts +++ b/packages/core/test/tool-search.test.ts @@ -9,14 +9,13 @@ import { FileSystem } from "@opencode-ai/core/filesystem" import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "@opencode-ai/core/location" import { LocationMutation } from "@opencode-ai/core/location-mutation" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { Ripgrep } from "@opencode-ai/core/ripgrep" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" -import { GlobTool } from "@opencode-ai/core/tool/glob" -import { GrepTool } from "@opencode-ai/core/tool/grep" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { Session } from "@opencode-ai/core/session" +import { GlobTool } from "@opencode-ai/core/tool/plugin/glob" +import { GrepTool } from "@opencode-ai/core/tool/plugin/grep" +import { Tool } from "@opencode-ai/core/tool" import { location } from "./fixture/location" import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" @@ -26,47 +25,40 @@ const globToolNode = makeLocationNode({ name: "test/glob-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(GlobTool.Plugin)), deps: [ - ToolRegistry.toolsNode, + Tool.node, FSUtil.node, Ripgrep.node, Location.node, LocationMutation.node, - PermissionV2.node, + Permission.node, ], }) const grepToolNode = makeLocationNode({ name: "test/grep-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(GrepTool.Plugin)), - deps: [ - ToolRegistry.toolsNode, - FSUtil.node, - Ripgrep.node, - Location.node, - LocationMutation.node, - PermissionV2.node, - ], + deps: [Tool.node, FSUtil.node, Ripgrep.node, Location.node, LocationMutation.node, Permission.node], }) -const sessionID = SessionV2.ID.make("ses_search_tool_test") +const sessionID = Session.ID.make("ses_search_tool_test") const withTools = ( directory: string, - body: (registry: ToolRegistry.Interface) => Effect.Effect, - assertions?: PermissionV2.AssertInput[], + body: (registry: Tool.Interface) => Effect.Effect, + assertions?: Permission.AssertInput[], ) => Effect.gen(function* () { - return yield* body(yield* ToolRegistry.Service) + return yield* body(yield* Tool.Service) }).pipe( Effect.provide( - AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, globToolNode, grepToolNode]), [ + AppNodeBuilder.build(LayerNode.group([Tool.node, globToolNode, grepToolNode]), [ [ Location.node, Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))), ], [ - PermissionV2.node, + Permission.node, Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: (input) => Effect.sync(() => { assertions?.push(input) @@ -79,7 +71,6 @@ const withTools = ( }), ), ], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], ]), ), ) @@ -236,7 +227,7 @@ describe("search tools", () => { Effect.acquireUseRelease( Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), ([active, outside]) => { - const assertions: PermissionV2.AssertInput[] = [] + const assertions: Permission.AssertInput[] = [] return Effect.promise(() => fs.writeFile(path.join(outside.path, "outside.txt"), "needle\n")).pipe( Effect.andThen( withTools( @@ -327,7 +318,7 @@ describe("search tools", () => { Effect.acquireUseRelease( Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), ([active, outside]) => { - const assertions: PermissionV2.AssertInput[] = [] + const assertions: Permission.AssertInput[] = [] return Effect.promise(() => fs.writeFile(path.join(outside.path, "outside.txt"), "outside\n")).pipe( Effect.andThen( withTools( @@ -359,7 +350,7 @@ describe("search tools", () => { Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), ([active, outside]) => { if (process.platform === "win32") return Effect.void - const assertions: PermissionV2.AssertInput[] = [] + const assertions: Permission.AssertInput[] = [] return Effect.promise(async () => { await fs.writeFile(path.join(outside.path, "outside.txt"), "outside\n") await fs.symlink(outside.path, path.join(active.path, "linked")) diff --git a/packages/core/test/tool-shell.test.ts b/packages/core/test/tool-shell.test.ts index 69af78d5693a..b64058a65b80 100644 --- a/packages/core/test/tool-shell.test.ts +++ b/packages/core/test/tool-shell.test.ts @@ -9,48 +9,47 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { filesystem } from "@opencode-ai/util/effect/app-node-platform" import { Database } from "@opencode-ai/core/database/database" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { FSUtil } from "@opencode-ai/util/fs-util" import { Global } from "@opencode-ai/util/global" import { Location } from "@opencode-ai/core/location" import { LocationServiceMap } from "@opencode-ai/core/location-service-map" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Job } from "@opencode-ai/core/job" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionExecution } from "@opencode-ai/core/session/execution" import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionStore } from "@opencode-ai/core/session/store" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { PluginRuntime } from "@opencode-ai/core/plugin/runtime" import { Shell } from "@opencode-ai/core/shell" import { Shell as ShellSchema } from "@opencode-ai/schema/shell" -import { ShellTool } from "@opencode-ai/core/tool/shell" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { ShellTool } from "@opencode-ai/core/tool/plugin/shell" +import { Tool } from "@opencode-ai/core/tool" import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" import { toolIdentity, executeTool, toolDefinitions, waitForTool } from "./lib/tool" -const sessionID = SessionV2.ID.make("ses_shell_tool_test") -const sessionModel = ModelV2.Ref.make({ id: ModelV2.ID.make("test"), providerID: ProviderV2.ID.make("test") }) -const assertions: PermissionV2.AssertInput[] = [] +const sessionID = Session.ID.make("ses_shell_tool_test") +const sessionModel = Model.Ref.make({ id: Model.ID.make("test"), providerID: Provider.ID.make("test") }) +const assertions: Permission.AssertInput[] = [] let denyAction: string | undefined -let afterPermission = (_input: PermissionV2.AssertInput): Effect.Effect => Effect.void +let afterPermission = (_input: Permission.AssertInput): Effect.Effect => Effect.void const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: (input) => Effect.sync(() => assertions.push(input)).pipe( Effect.andThen(Effect.suspend(() => afterPermission(input))), Effect.andThen( input.action === denyAction ? Effect.fail( - new PermissionV2.BlockedError({ + new Permission.BlockedError({ rules: [], permission: input.action, resources: input.resources, @@ -78,30 +77,30 @@ const executionNode = makeGlobalNode({ layer: Layer.effect( SessionExecution.Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const store = yield* SessionStore.Service - const complete = Effect.fn("ShellTest.complete")(function* (id: SessionV2.ID) { + const complete = Effect.fn("ShellTest.complete")(function* (id: Session.ID) { const session = yield* store.get(id) if (!session) return const assistantMessageID = SessionMessage.ID.create() - yield* events.publish(SessionEvent.Step.Started, { + yield* bus.publish(SessionEvent.Step.Started, { sessionID: id, assistantMessageID, - agent: session.agent ?? AgentV2.ID.make("code"), + agent: session.agent ?? Agent.ID.make("code"), model: sessionModel, }) - yield* events.publish(SessionEvent.Text.Started, { + yield* bus.publish(SessionEvent.Text.Started, { sessionID: id, assistantMessageID, ordinal: 0, }) - yield* events.publish(SessionEvent.Text.Ended, { + yield* bus.publish(SessionEvent.Text.Ended, { sessionID: id, assistantMessageID, ordinal: 0, text: "ok", }) - yield* events.publish(SessionEvent.Step.Ended, { + yield* bus.publish(SessionEvent.Step.Ended, { sessionID: id, assistantMessageID, finish: "stop", @@ -118,16 +117,15 @@ const executionNode = makeGlobalNode({ }) }), ), - deps: [EventV2.node, SessionStore.node], + deps: [Bus.node, SessionStore.node], }) const layer = AppNodeBuilder.build( LayerNode.group([ Database.node, - EventV2.node, + Bus.node, Job.node, - ToolOutputStore.cleanupNode, - SessionV2.node, + Session.node, SessionExecution.node, PluginRuntime.providerNode, LocationServiceMap.node, @@ -137,7 +135,7 @@ const layer = AppNodeBuilder.build( ]), [ [SessionExecution.node, executionNode], - [PermissionV2.node, permission], + [Permission.node, permission], ], ) @@ -174,9 +172,9 @@ const progressOverflowCommand = (bytes: number, release: string) => ? `[Console]::Out.Write(('x' * ${bytes})); while (!(Test-Path -LiteralPath '${release}')) { Start-Sleep -Milliseconds 50 }` : `head -c ${bytes} /dev/zero | tr '\\0' 'x'; while [ ! -e '${release}' ]; do sleep 0.05; done` -const withSession = (directory: string, body: (registry: ToolRegistry.Interface) => Effect.Effect) => +const withSession = (directory: string, body: (registry: Tool.Interface) => Effect.Effect) => Effect.gen(function* () { - const sessions = yield* SessionV2.Service + const sessions = yield* Session.Service const location = Location.Ref.make({ directory: AbsolutePath.make(directory) }) yield* sessions.create({ id: sessionID, @@ -187,7 +185,7 @@ const withSession = (directory: string, body: (registry: ToolRegistry.I const locations = yield* LocationServiceMap.Service const locationLayer = locations.get(location) return yield* Effect.gen(function* () { - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service yield* waitForTool(registry, ShellTool.name) return yield* body(registry) }).pipe(Effect.provide(locationLayer), Effect.ensuring(locations.invalidate(location))) @@ -466,7 +464,7 @@ describe("ShellTool", () => { reset() return withSession(tmp.path, (registry) => Effect.gen(function* () { - const updates: ToolRegistry.Progress[] = [] + const updates: Tool.Metadata[] = [] yield* executeTool(registry, { ...call({ command: steadyProgressCommand }, "call-steady-progress"), progress: (update) => Effect.sync(() => updates.push(update)), @@ -511,8 +509,8 @@ describe("ShellTool", () => { reset() return withSession(tmp.path, (registry) => Effect.gen(function* () { - const events = yield* EventV2.Service - const admitted = yield* events.subscribe(SessionEvent.InputAdmitted).pipe( + const bus = yield* Bus.Service + const admitted = yield* bus.subscribe(SessionEvent.InputAdmitted).pipe( Stream.filter((event) => event.data.sessionID === sessionID && event.data.input.type === "synthetic"), Stream.runHead, Effect.forkScoped({ startImmediately: true }), diff --git a/packages/core/test/tool-skill.test.ts b/packages/core/test/tool-skill.test.ts index eab58f75d71d..842b5ea6aae9 100644 --- a/packages/core/test/tool-skill.test.ts +++ b/packages/core/test/tool-skill.test.ts @@ -4,13 +4,12 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" -import { SkillV2 } from "@opencode-ai/core/skill" -import { SkillTool } from "@opencode-ai/core/tool/skill" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { Session } from "@opencode-ai/core/session" +import { Skill } from "@opencode-ai/core/skill" +import { SkillTool } from "@opencode-ai/core/tool/plugin/skill" +import { Tool } from "@opencode-ai/core/tool" import { tmpdir } from "./fixture/tmpdir" import { Image } from "@opencode-ai/core/image" import { it } from "./lib/effect" @@ -22,10 +21,10 @@ import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from " const skillToolNode = makeLocationNode({ name: "test/skill-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(SkillTool.Plugin)), - deps: [ToolRegistry.toolsNode, FSUtil.node, SkillV2.node, PermissionV2.node], + deps: [Tool.node, FSUtil.node, Skill.node, Permission.node], }) -const sessionID = SessionV2.ID.make("ses_skill_tool_test") +const sessionID = Session.ID.make("ses_skill_tool_test") describe("SkillTool", () => { it.live("lists available skills, authorizes the selected ID, and loads model-facing content", () => @@ -43,25 +42,25 @@ describe("SkillTool", () => { Promise.all([fs.writeFile(location, "unused"), fs.writeFile(reference, "reference")]), ) - const info: SkillV2.Info = { - id: SkillV2.ID.make("effect"), - name: SkillV2.Name.make("Effect"), + const info: Skill.Info = { + id: Skill.ID.make("effect"), + name: Skill.Name.make("Effect"), description: "Use Effect", location: AbsolutePath.make(location), content: "# Effect\n\nGuidance", } let current = [info] - const assertions: PermissionV2.AssertInput[] = [] + const assertions: Permission.AssertInput[] = [] let deny = false const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: (input) => Effect.sync(() => assertions.push(input)).pipe( Effect.andThen( deny ? Effect.fail( - new PermissionV2.BlockedError({ + new Permission.BlockedError({ rules: [], permission: input.action, resources: input.resources, @@ -78,8 +77,8 @@ describe("SkillTool", () => { }), ) const skills = Layer.succeed( - SkillV2.Service, - SkillV2.Service.of({ + Skill.Service, + Skill.Service.of({ transform: (_transform) => Effect.die("unused"), reload: () => Effect.die("unused"), sources: () => Effect.die("unused"), @@ -87,17 +86,16 @@ describe("SkillTool", () => { }), ) const skillToolLayer = AppNodeBuilder.build( - LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, skillToolNode]), + LayerNode.group([Tool.node, skillToolNode]), [ - [PermissionV2.node, permission], - [SkillV2.node, skills], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + [Permission.node, permission], + [Skill.node, skills], [Image.node, imagePassthrough], ], ) return yield* Effect.gen(function* () { - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect((yield* toolDefinitions(registry))[0]).toMatchObject({ name: "skill", description: SkillTool.description, @@ -151,9 +149,9 @@ describe("SkillTool", () => { error: { type: "permission.rejected", message: "Permission denied: skill" }, }) deny = false - const flat = SkillV2.Info.make({ - id: SkillV2.ID.make("public"), - name: SkillV2.Name.make("Public"), + const flat = Skill.Info.make({ + id: Skill.ID.make("public"), + name: Skill.Name.make("Public"), description: "Public guidance", location: AbsolutePath.make(path.join(tmp.path, "public.md")), content: "Public", diff --git a/packages/core/test/tool-subagent.test.ts b/packages/core/test/tool-subagent.test.ts index 03705eee217b..73299769f6b2 100644 --- a/packages/core/test/tool-subagent.test.ts +++ b/packages/core/test/tool-subagent.test.ts @@ -6,15 +6,15 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { Database } from "@opencode-ai/core/database/database" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Location } from "@opencode-ai/core/location" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Job } from "@opencode-ai/core/job" import { LocationServiceMap } from "@opencode-ai/core/location-service-map" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionExecution } from "@opencode-ai/core/session/execution" import { SessionPending } from "@opencode-ai/core/session/pending" @@ -23,30 +23,29 @@ import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" import { SessionStore } from "@opencode-ai/core/session/store" import { PluginRuntime } from "@opencode-ai/core/plugin/runtime" import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" -import { SubagentTool } from "@opencode-ai/core/tool/subagent" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { SubagentTool } from "@opencode-ai/core/tool/plugin/subagent" +import { Tool } from "@opencode-ai/core/tool" import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" import { executeTool, toolIdentity, waitForTool } from "./lib/tool" const childText = "child final response" -const childModel = ModelV2.Ref.make({ id: ModelV2.ID.make("child"), providerID: ProviderV2.ID.make("test") }) -const parentModel = ModelV2.Ref.make({ id: ModelV2.ID.make("parent"), providerID: ProviderV2.ID.make("test") }) +const childModel = Model.Ref.make({ id: Model.ID.make("child"), providerID: Provider.ID.make("test") }) +const parentModel = Model.Ref.make({ id: Model.ID.make("parent"), providerID: Provider.ID.make("test") }) const tokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } } const outputSessionID = (value: unknown) => - Schema.decodeUnknownSync(Schema.Struct({ sessionID: SessionV2.ID }))(value).sessionID + Schema.decodeUnknownSync(Schema.Struct({ sessionID: Session.ID }))(value).sessionID const executionNode = makeGlobalNode({ service: SessionExecution.Service, layer: Layer.effect( SessionExecution.Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const store = yield* SessionStore.Service - const completed = new Set() - const complete = Effect.fn("SubagentTest.complete")(function* (sessionID: SessionV2.ID) { + const completed = new Set() + const complete = Effect.fn("SubagentTest.complete")(function* (sessionID: Session.ID) { if (completed.has(sessionID)) return if ((yield* store.get(sessionID))?.title.includes("fail")) { yield* new SessionRunnerModel.ModelNotSelectedError({ sessionID }) @@ -54,24 +53,24 @@ const executionNode = makeGlobalNode({ } completed.add(sessionID) const assistantMessageID = SessionMessage.ID.create() - yield* events.publish(SessionEvent.Step.Started, { + yield* bus.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID, - agent: AgentV2.ID.make("reviewer"), + agent: Agent.ID.make("reviewer"), model: childModel, }) - yield* events.publish(SessionEvent.Text.Started, { + yield* bus.publish(SessionEvent.Text.Started, { sessionID, assistantMessageID, ordinal: 0, }) - yield* events.publish(SessionEvent.Text.Ended, { + yield* bus.publish(SessionEvent.Text.Ended, { sessionID, assistantMessageID, ordinal: 0, text: childText, }) - yield* events.publish(SessionEvent.Step.Ended, { + yield* bus.publish(SessionEvent.Step.Ended, { sessionID, assistantMessageID, finish: "stop", @@ -88,16 +87,15 @@ const executionNode = makeGlobalNode({ }) }), ), - deps: [EventV2.node, SessionStore.node], + deps: [Bus.node, SessionStore.node], }) const layer = AppNodeBuilder.build( LayerNode.group([ Database.node, - EventV2.node, + Bus.node, Job.node, - ToolOutputStore.cleanupNode, - SessionV2.node, + Session.node, SessionExecution.node, PluginRuntime.providerNode, LocationServiceMap.node, @@ -111,21 +109,21 @@ const withSubagent = (location: Location.Ref) => Effect.gen(function* () { const locations = yield* LocationServiceMap.Service yield* PluginSupervisor.Service.use((supervisor) => supervisor.flush).pipe(Effect.provide(locations.get(location))) - yield* AgentV2.Service.use((agents) => + yield* Agent.Service.use((agents) => agents.transform((draft) => { // The caller identity used by executeTool; subagent permission asserts against it. draft.update(toolIdentity.agent, (agent) => { agent.mode = "primary" agent.permissions.push({ action: "*", resource: "*", effect: "allow" }) }) - draft.update(AgentV2.ID.make("reviewer"), (agent) => { + draft.update(Agent.ID.make("reviewer"), (agent) => { agent.mode = "subagent" agent.model = childModel }) - draft.update(AgentV2.ID.make("fallback"), (agent) => { + draft.update(Agent.ID.make("fallback"), (agent) => { agent.mode = "subagent" }) - draft.update(AgentV2.ID.make("primary"), (agent) => { + draft.update(Agent.ID.make("primary"), (agent) => { agent.mode = "primary" }) }), @@ -141,12 +139,12 @@ describe("SubagentTool", () => { Effect.flatMap((dir) => Effect.gen(function* () { const location = Location.Ref.make({ directory: AbsolutePath.make(dir.path) }) - const session = yield* SessionV2.Service + const session = yield* Session.Service const parent = yield* session.create({ location }) yield* withSubagent(parent.location) const locations = yield* LocationServiceMap.Service - const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location))) + const registry = yield* Tool.Service.pipe(Effect.provide(locations.get(parent.location))) yield* waitForTool(registry, SubagentTool.name) expect((yield* registry.snapshot()).definitions.map((tool) => tool.name)).toContain(SubagentTool.name) expect( @@ -177,12 +175,12 @@ describe("SubagentTool", () => { Effect.flatMap((dir) => Effect.gen(function* () { const location = Location.Ref.make({ directory: AbsolutePath.make(dir.path) }) - const sessions = yield* SessionV2.Service + const sessions = yield* Session.Service const root = yield* sessions.create({ location }) const parent = yield* sessions.create({ parentID: root.id, title: "parent" }) yield* withSubagent(parent.location) const locations = yield* LocationServiceMap.Service - const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location))) + const registry = yield* Tool.Service.pipe(Effect.provide(locations.get(parent.location))) yield* waitForTool(registry, SubagentTool.name) expect( @@ -220,12 +218,12 @@ describe("SubagentTool", () => { Bun.write(path.join(dir.path, "opencode.json"), JSON.stringify({ experimental: { subagent_depth: 2 } })), ) const location = Location.Ref.make({ directory: AbsolutePath.make(dir.path) }) - const sessions = yield* SessionV2.Service + const sessions = yield* Session.Service const root = yield* sessions.create({ location }) const parent = yield* sessions.create({ parentID: root.id, title: "parent", model: parentModel }) yield* withSubagent(parent.location) const locations = yield* LocationServiceMap.Service - const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location))) + const registry = yield* Tool.Service.pipe(Effect.provide(locations.get(parent.location))) yield* waitForTool(registry, SubagentTool.name) const settled = yield* executeTool(registry, { @@ -262,13 +260,13 @@ describe("SubagentTool", () => { Effect.flatMap((dir) => Effect.gen(function* () { const location = Location.Ref.make({ directory: AbsolutePath.make(dir.path) }) - const sessions = yield* SessionV2.Service + const sessions = yield* Session.Service const parent = yield* sessions.create({ location, model: parentModel }) yield* withSubagent(parent.location) const locations = yield* LocationServiceMap.Service - const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location))) + const registry = yield* Tool.Service.pipe(Effect.provide(locations.get(parent.location))) yield* waitForTool(registry, SubagentTool.name) - const progress: ToolRegistry.Progress[] = [] + const progress: Tool.Metadata[] = [] const settled = yield* executeTool(registry, { sessionID: parent.id, @@ -325,11 +323,11 @@ describe("SubagentTool", () => { Effect.flatMap((dir) => Effect.gen(function* () { const location = Location.Ref.make({ directory: AbsolutePath.make(dir.path) }) - const sessions = yield* SessionV2.Service + const sessions = yield* Session.Service const parent = yield* sessions.create({ location }) yield* withSubagent(parent.location) const locations = yield* LocationServiceMap.Service - const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location))) + const registry = yield* Tool.Service.pipe(Effect.provide(locations.get(parent.location))) yield* waitForTool(registry, SubagentTool.name) expect( @@ -363,14 +361,14 @@ describe("SubagentTool", () => { Effect.flatMap((dir) => Effect.gen(function* () { const location = Location.Ref.make({ directory: AbsolutePath.make(dir.path) }) - const sessions = yield* SessionV2.Service + const sessions = yield* Session.Service const parent = yield* sessions.create({ location }) yield* withSubagent(parent.location) const locations = yield* LocationServiceMap.Service - const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location))) + const registry = yield* Tool.Service.pipe(Effect.provide(locations.get(parent.location))) yield* waitForTool(registry, SubagentTool.name) - const events = yield* EventV2.Service - const admitted = yield* events.subscribe(SessionEvent.InputAdmitted).pipe( + const bus = yield* Bus.Service + const admitted = yield* bus.subscribe(SessionEvent.InputAdmitted).pipe( Stream.filter((event) => event.data.sessionID === parent.id && event.data.input.type === "synthetic"), Stream.take(1), Stream.runCollect, @@ -406,7 +404,7 @@ describe("SubagentTool", () => { }, }) const database = yield* Database.Service - yield* SessionPending.promote(database.db, events, parent.id, "steer") + yield* SessionPending.promote(database.db, bus, parent.id, "steer") const synthetic = (yield* sessions.context(parent.id)).filter((message) => message.type === "synthetic") expect(synthetic).toHaveLength(1) expect(synthetic[0]?.text).toContain(` }> = [] -const assertions: PermissionV2.AssertInput[] = [] +const assertions: Permission.AssertInput[] = [] let respond = (_request: HttpClientRequest.HttpClientRequest) => Effect.succeed(new Response("hello", { headers: { "content-type": "text/plain" } })) @@ -38,8 +37,8 @@ const http = Layer.succeed( ), ) const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: (input) => Effect.sync(() => assertions.push(input)), ask: () => Effect.die("unused"), reply: () => Effect.die("unused"), @@ -49,9 +48,8 @@ const permission = Layer.succeed( }), ) const toolLayer = (replacements: LayerNode.Replacements = []) => - AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, webFetchToolNode]), [ - [PermissionV2.node, permission], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + AppNodeBuilder.build(LayerNode.group([Tool.node, webFetchToolNode]), [ + [Permission.node, permission], [Image.node, imagePassthrough], ...replacements, ]) @@ -89,7 +87,7 @@ describe("WebFetchTool registration", () => { it.effect("registers and fetches an ordinary hostname HTTP URL without rewriting it", () => Effect.gen(function* () { reset() - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const url = "http://example.com/public" expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["webfetch", "execute"]) @@ -109,7 +107,7 @@ describe("WebFetchTool registration", () => { it.effect("accepts localhost URLs with the same requested-URL permission check", () => Effect.gen(function* () { reset() - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const url = "http://localhost/private" expect(yield* executeTool(registry, call({ url, format: "text" }))).toMatchObject({ @@ -137,7 +135,7 @@ describe("WebFetchTool registration", () => { (server) => Effect.gen(function* () { reset() - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const url = new URL("/redirect", server.url).toString() expect(yield* executeTool(registry, call({ url, format: "text" }))).toMatchObject({ @@ -155,7 +153,7 @@ describe("WebFetchTool registration", () => { it.effect("rejects non-HTTP schemes before permission or transport", () => Effect.gen(function* () { reset() - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service // toSessionError unwraps the "Unable to fetch " ToolFailure to its cause message. expect(yield* executeTool(registry, call({ url: "file:///etc/passwd", format: "text" }))).toEqual({ @@ -176,7 +174,7 @@ describe("WebFetchTool registration", () => { headers: { "content-type": "text/html; charset=utf-8" }, }), ) - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect(yield* executeTool(registry, call({ url: "https://1.1.1.1", format: "markdown" }))).toMatchObject({ status: "completed", @@ -198,7 +196,7 @@ describe("WebFetchTool registration", () => { headers: { "content-type": "text/html" }, }), ) - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const url = "https://1.1.1.1/deep-html" expect(yield* executeTool(registry, call({ url, format: "markdown" }))).toMatchObject({ @@ -211,7 +209,7 @@ describe("WebFetchTool registration", () => { it.effect("rejects declared and streamed oversized bodies", () => Effect.gen(function* () { reset() - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service respond = () => Effect.succeed( new Response("small", { @@ -243,7 +241,7 @@ describe("WebFetchTool registration", () => { it.effect("keeps images and files unsupported until typed outcomes can carry attachments", () => Effect.gen(function* () { reset() - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service respond = () => Effect.succeed(new Response("png", { headers: { "content-type": "image/png" } })) expect(yield* executeTool(registry, call({ url: "https://1.1.1.1/image", format: "html" }))).toEqual({ status: "error", @@ -268,7 +266,7 @@ describe("WebFetchTool registration", () => { ? new Response("challenge", { status: 403, headers: { "cf-mitigated": "challenge" } }) : new Response("ok", { headers: { "content-type": "text/plain" } }), ) - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect(yield* executeTool(registry, call({ url: "https://1.1.1.1", format: "text" }))).toMatchObject({ status: "completed", @@ -284,7 +282,7 @@ describe("WebFetchTool registration", () => { Effect.gen(function* () { reset() respond = () => Effect.never - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const fiber = yield* executeTool( registry, call({ url: "https://1.1.1.1/slow", format: "text", timeout: 1 }), diff --git a/packages/core/test/tool-websearch.test.ts b/packages/core/test/tool-websearch.test.ts index 6c1889680867..9bd6815f22ae 100644 --- a/packages/core/test/tool-websearch.test.ts +++ b/packages/core/test/tool-websearch.test.ts @@ -2,14 +2,13 @@ import { beforeEach, describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { Form } from "@opencode-ai/core/form" import { KV } from "@opencode-ai/core/kv" import { WebSearch } from "@opencode-ai/core/websearch" -import { SessionV2 } from "@opencode-ai/core/session" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { WebSearchTool } from "@opencode-ai/core/tool/websearch" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { Session } from "@opencode-ai/core/session" +import { Tool } from "@opencode-ai/core/tool" +import { WebSearchTool } from "@opencode-ai/core/tool/plugin/websearch" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Image } from "@opencode-ai/core/image" import { testEffect } from "./lib/effect" @@ -25,11 +24,11 @@ const webSearchToolNode = makeLocationNode({ yield* registerToolPlugin(WebSearchTool.Plugin, { websearch: webSearchHost(websearch) }) }), ), - deps: [ToolRegistry.toolsNode, PermissionV2.node, WebSearch.node, Form.node, KV.node], + deps: [Tool.node, Permission.node, WebSearch.node, Form.node, KV.node], }) -const sessionID = SessionV2.ID.make("ses_websearch_test") -const assertions: PermissionV2.AssertInput[] = [] +const sessionID = Session.ID.make("ses_websearch_test") +const assertions: Permission.AssertInput[] = [] const queries: WebSearch.Input[] = [] let result = new WebSearch.Response({ providerID: WebSearch.ID.make("exa"), @@ -46,8 +45,8 @@ beforeEach(() => { }) const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: (input) => Effect.sync(() => assertions.push(input)), ask: () => Effect.die("unused"), reply: () => Effect.die("unused"), @@ -92,13 +91,12 @@ const kv = Layer.succeed( ) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, WebSearch.node, webSearchToolNode]), + LayerNode.group([Tool.node, WebSearch.node, webSearchToolNode]), [ - [PermissionV2.node, permission], + [Permission.node, permission], [WebSearch.node, websearch], [Form.node, form], [KV.node, kv], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], [Image.node, imagePassthrough], ], ), @@ -107,7 +105,7 @@ const it = testEffect( describe("WebSearchTool registration", () => { it.effect("asserts permission before delegating to WebSearch", () => Effect.gen(function* () { - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["websearch", "execute"]) expect( @@ -155,7 +153,7 @@ describe("WebSearchTool registration", () => { }, ], }) - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -190,7 +188,7 @@ describe("WebSearchTool registration", () => { it.effect("uses the concise no-results fallback", () => Effect.gen(function* () { result = new WebSearch.Response({ providerID: WebSearch.ID.make("exa"), results: [] }) - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { diff --git a/packages/core/test/tool-write.test.ts b/packages/core/test/tool-write.test.ts index f786fc934800..cd2781189f39 100644 --- a/packages/core/test/tool-write.test.ts +++ b/packages/core/test/tool-write.test.ts @@ -8,12 +8,11 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "@opencode-ai/core/location" import { LocationMutation } from "@opencode-ai/core/location-mutation" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" -import { WriteTool } from "@opencode-ai/core/tool/write" +import { Session } from "@opencode-ai/core/session" +import { Tool } from "@opencode-ai/core/tool" +import { WriteTool } from "@opencode-ai/core/tool/plugin/write" import { location } from "./fixture/location" import { tmpdir } from "./fixture/tmpdir" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" @@ -23,23 +22,23 @@ import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from " const writeToolNode = makeLocationNode({ name: "test/write-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(WriteTool.Plugin)), - deps: [ToolRegistry.toolsNode, LocationMutation.node, FileMutation.node, PermissionV2.node], + deps: [Tool.node, LocationMutation.node, FileMutation.node, Permission.node], }) -const sessionID = SessionV2.ID.make("ses_write_tool_test") -const assertions: PermissionV2.AssertInput[] = [] +const sessionID = Session.ID.make("ses_write_tool_test") +const assertions: Permission.AssertInput[] = [] const writes: string[] = [] let denyAction: string | undefined const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: (input) => Effect.sync(() => assertions.push(input)).pipe( Effect.andThen( input.action === denyAction ? Effect.fail( - new PermissionV2.BlockedError({ + new Permission.BlockedError({ rules: [], permission: input.action, resources: input.resources, @@ -74,19 +73,19 @@ const filesystem = Layer.effect( }), ).pipe(Layer.provide(LayerNode.compile(FSUtil.node))) -const withTool = (directory: string, body: (registry: ToolRegistry.Interface) => Effect.Effect) => { +const withTool = (directory: string, body: (registry: Tool.Interface) => Effect.Effect) => { const activeLocation = Layer.succeed( Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) })), ) return Effect.gen(function* () { - return yield* body(yield* ToolRegistry.Service) + return yield* body(yield* Tool.Service) }).pipe( Effect.provide( AppNodeBuilder.build( LayerNode.group([ - ToolRegistry.node, - ToolRegistry.toolsNode, + Tool.node, + Tool.node, LocationMutation.node, FileMutation.node, writeToolNode, @@ -94,8 +93,7 @@ const withTool = (directory: string, body: (registry: ToolRegistry.Inte [ [FSUtil.node, filesystem], [Location.node, activeLocation], - [PermissionV2.node, permission], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + [Permission.node, permission], ], ), ), diff --git a/packages/core/test/websearch.test.ts b/packages/core/test/websearch.test.ts index 104c62860604..73c19b1c593a 100644 --- a/packages/core/test/websearch.test.ts +++ b/packages/core/test/websearch.test.ts @@ -2,12 +2,12 @@ import { describe, expect } from "bun:test" import { Effect, Exit, Scope } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { KV } from "@opencode-ai/core/kv" import { WebSearch } from "@opencode-ai/core/websearch" import { testEffect } from "./lib/effect" -const it = testEffect(AppNodeBuilder.build(LayerNode.group([WebSearch.node, EventV2.node, KV.node]))) +const it = testEffect(AppNodeBuilder.build(LayerNode.group([WebSearch.node, Bus.node, KV.node]))) const register = (id: string) => Effect.gen(function* () { diff --git a/packages/core/test/wellknown.test.ts b/packages/core/test/wellknown.test.ts index 266e96f7546c..9a2e4e106776 100644 --- a/packages/core/test/wellknown.test.ts +++ b/packages/core/test/wellknown.test.ts @@ -3,12 +3,12 @@ import { Effect, Fiber, Stream } from "effect" import { FetchHttpClient } from "effect/unstable/http" import { KV } from "@opencode-ai/core/kv" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { WellKnown } from "@opencode-ai/core/wellknown" import { testEffect } from "./lib/effect" const it = testEffect(FetchHttpClient.layer) -const serviceIt = testEffect(LayerNode.compile(LayerNode.group([WellKnown.node, KV.node, EventV2.node]))) +const serviceIt = testEffect(LayerNode.compile(LayerNode.group([WellKnown.node, KV.node, Bus.node]))) it.live("loads embedded and remote configuration", () => Effect.acquireUseRelease( @@ -66,8 +66,8 @@ serviceIt.live("persists sources in one KV value", () => Effect.gen(function* () { const wellknown = yield* WellKnown.Service const kv = yield* KV.Service - const events = yield* EventV2.Service - const changed = yield* events + const bus = yield* Bus.Service + const changed = yield* bus .subscribe(WellKnown.Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped({ startImmediately: true })) const entry = yield* wellknown.add(`${server.url.origin}/`) @@ -102,11 +102,11 @@ serviceIt.live("refreshes changed manifests", () => ({ server, update }) => Effect.gen(function* () { const wellknown = yield* WellKnown.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* wellknown.add(server.url.origin) expect(yield* wellknown.refresh()).toBe(false) - const changed = yield* events + const changed = yield* bus .subscribe(WellKnown.Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped({ startImmediately: true })) update() diff --git a/packages/http-recorder/README.md b/packages/http-recorder/README.md index 84c539081ecc..10f828b8c474 100644 --- a/packages/http-recorder/README.md +++ b/packages/http-recorder/README.md @@ -9,13 +9,13 @@ Use it for provider integrations, retries, polling, multi-step flows, and any te ## Install ```sh -bun add effect@4.0.0-beta.83 -bun add -d @opencode-ai/http-recorder @effect/vitest@4.0.0-beta.83 vitest@^4 +bun add effect@4.0.0-beta.101 +bun add -d @opencode-ai/http-recorder @effect/vitest@4.0.0-beta.101 vitest@^4 ``` The package supports Node.js 22+ and Bun. It is not intended for browsers, workers, or Deno. -Effect `4.0.0-beta.83` currently contains unresolved symbols in its published declarations. Until those upstream declarations are fixed, TypeScript consumers need: +Effect `4.0.0-beta.101` currently contains unresolved symbols in its published declarations. Until those upstream declarations are fixed, TypeScript consumers need: ```json { diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index fd00a153cea9..792f03c019b0 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -52,7 +52,7 @@ "typescript": "catalog:" }, "dependencies": { - "@effect/platform-node-shared": "4.0.0-beta.98" + "@effect/platform-node-shared": "4.0.0-beta.101" }, "peerDependencies": { "effect": "catalog:" diff --git a/packages/httpapi-codegen/src/index.ts b/packages/httpapi-codegen/src/index.ts index 4de8068d56c9..90ce130d1d7e 100644 --- a/packages/httpapi-codegen/src/index.ts +++ b/packages/httpapi-codegen/src/index.ts @@ -35,6 +35,19 @@ export type Contract = { readonly groups: ReadonlyArray } +export type EffectTypeReference = { + readonly schema: Schema.Top + readonly name: string + readonly import: string +} + +export type EffectOutputType = { + readonly name: string + readonly import: string +} + +type ResolvedEffectTypeReference = Omit & { readonly ast: SchemaAST.AST } + export class GenerationError extends Schema.TaggedErrorClass()("GenerationError", { reason: Schema.String, }) { @@ -280,9 +293,13 @@ export function emitEffect(contract: Contract): Output { export function emitEffectImported( contract: Contract, options: - | { readonly module: string; readonly api: string } - | { readonly module: string; readonly group: string } - | { readonly module: string; readonly endpoints: Readonly> }, + | { readonly module: string; readonly api: string; readonly shapeModule?: string } + | { readonly module: string; readonly group: string; readonly shapeModule?: string } + | { + readonly module: string + readonly endpoints: Readonly> + readonly shapeModule?: string + }, ): Output { return { operations: operations(contract.groups), @@ -292,11 +309,19 @@ export function emitEffectImported( export function emitEffectShape( contract: Contract, - options: { readonly module: string; readonly api: string }, + options?: { + readonly typeReferences?: ReadonlyArray + readonly outputTypes?: Readonly> + }, ): Output { return { operations: operations(contract.groups), - files: [{ path: "api.ts", content: renderEffectShape(contract.groups, options) }], + files: [ + { + path: "api.ts", + content: renderEffectShape(contract.groups, options?.typeReferences ?? [], options?.outputTypes), + }, + ], } } @@ -332,28 +357,31 @@ export function emitPromise( } } -function renderEffectShape(groups: ReadonlyArray, options: { readonly module: string; readonly api: string }) { +function renderEffectShape( + groups: ReadonlyArray, + typeReferences: ReadonlyArray, + outputTypes?: Readonly>, +) { + const references = effectTypeReferences(typeReferences) + const imports = new Set() const endpointTypes = groups.map((group, groupIndex) => { - const rawGroup = group.endpoints[0]?.topLevel ? "RawClient" : `RawClient[${JSON.stringify(group.sourceIdentifier)}]` const endpoints = group.endpoints.map((endpoint, endpointIndex) => { const prefix = `Endpoint${groupIndex}_${endpointIndex}` - const request = - endpoint.operation.inputMode === "none" - ? "" - : `type ${prefix}Request = Parameters<${rawGroup}[${JSON.stringify(endpoint.endpoint.identifier)}]>[0]` const input = endpoint.input - .map( - (field) => - `readonly ${JSON.stringify(field.name)}${field.optional ? "?" : ""}: ${prefix}Request[${JSON.stringify(field.source)}]${isOpaquePayload(endpoint) && field.source === "payload" ? "" : `[${JSON.stringify(field.name)}]`}`, - ) + .map((field) => { + const schema = effectInputSchema(endpoint, field) + if (schema === undefined) { + throw new GenerationError({ reason: `Missing Effect input schema: ${endpoint.group}.${endpoint.endpoint.identifier}.${field.name}` }) + } + return `readonly ${JSON.stringify(field.name)}${field.optional ? "?" : ""}: ${effectType(schema, references, imports)}` + }) .join("; ") const inputType = endpoint.operation.inputMode === "none" ? "" : `export type ${prefix}Input = { ${input} }` - const rawOutput = `EffectValue>` - const outputType = isStreamSchema(endpoint.successes[0]) - ? `export type ${prefix}Output = StreamValue<${rawOutput}>` - : `export type ${prefix}Output = ${endpoint.unwrapData ? `(${rawOutput})["data"]` : rawOutput}` + const output = effectOutputSchema(endpoint) + const override = outputTypes?.[clientOperationKey(group, endpoint)] + if (override !== undefined) imports.add(override.import) + const outputType = `export type ${prefix}Output = ${override?.name ?? (output === undefined ? "void" : effectType(output, references, imports))}` return [ - request, endpoint.operation.inputMode === "none" ? "" : inputType, outputType, `export type ${groupShapeTypeName(group, endpoint)} = (${endpoint.operation.inputMode === "none" ? "" : `input${endpoint.operation.inputMode === "optional" ? "?" : ""}: ${prefix}Input`}) => ${endpoint.operation.success === "stream" ? `Stream.Stream<${prefix}Output, E>` : `Effect.Effect<${prefix}Output, E>`}`, @@ -383,12 +411,7 @@ function renderEffectShape(groups: ReadonlyArray, options: { readonly mod ) return `// Generated by @opencode-ai/httpapi-codegen. Do not edit. import type { Effect, Stream } from "effect" -import type { HttpApiClient } from "effect/unstable/httpapi" -import type { ${options.api} } from ${JSON.stringify(options.module)} - -type RawClient = HttpApiClient.ForApi -type EffectValue = A extends Effect.Effect ? Success : never -type StreamValue = A extends Stream.Stream ? Success : never +${[...imports].join("\n")} ${endpointTypes.join("\n\n")} @@ -398,6 +421,112 @@ ${clientFields.join("\n")} ` } +function effectTypeReferences(input: ReadonlyArray) { + const names = new Map() + const asts = new Map() + const brands = new Map() + for (const reference of input) { + const value = { name: reference.name, import: reference.import, ast: reference.schema.ast } + const document = SchemaRepresentation.toCodeDocument( + SchemaRepresentation.fromASTs([Schema.toType(reference.schema).ast]), + ) + const name = document.codes[0]?.Type + const type = + name === undefined + ? undefined + : (document.references.nonRecursives.find((item) => item.$ref === name)?.code.Type ?? name) + if (type?.includes("Brand.Brand<") && !brands.has(type)) brands.set(type, value) + if (SchemaAST.resolveIdentifier(reference.schema.ast) !== undefined || type?.includes("Brand.Brand<")) { + asts.set(reference.schema.ast, value) + asts.set(Schema.toType(reference.schema).ast, value) + } + if (name === undefined || !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) continue + const previous = names.get(name) + if (previous !== undefined) { + if (previous.ast !== reference.schema.ast) { + throw new GenerationError({ reason: `Conflicting Effect type reference: ${name}` }) + } + continue + } + names.set(name, value) + } + return { names, asts, brands } +} + +function effectType( + schema: Schema.Top, + references: ReturnType, + imports: Set, +) { + const projected = Schema.toType(schema) + const direct = references.asts.get(schema.ast) ?? references.asts.get(projected.ast) + if (direct !== undefined) { + imports.add(direct.import) + return direct.name + } + const document = SchemaRepresentation.toCodeDocument(SchemaRepresentation.fromASTs([projected.ast])) + const source = new Map(document.references.nonRecursives.map((reference) => [reference.$ref, reference.code.Type])) + const expand = (type: string, seen = new Set()): string => { + for (const [name, value] of source) { + const pattern = new RegExp( + `(? property.name === field.name) + return property === undefined ? undefined : Schema.make(property.type) +} + +function effectOutputSchema(endpoint: Endpoint): Schema.Top | undefined { + const schema = endpoint.successes[0] + if (HttpApiSchema.isNoContent(schema.ast)) return undefined + if (isStreamSchema(schema)) { + if (schema._tag === "StreamUint8Array") return Schema.Uint8Array + return schema.sseMode === "data" ? streamDataSchema(schema) : Schema.make(schema.events.ast) + } + if (!endpoint.unwrapData) return schema + const ast = Schema.toType(schema).ast + if (!SchemaAST.isObjects(ast)) return undefined + const data = ast.propertySignatures.find((property) => property.name === "data") + return data === undefined ? undefined : Schema.make(data.type) +} + function groupShapeName(group: Group) { return `${identifierPart(group.identifier)}Api` } @@ -474,9 +603,13 @@ function renderEffectFiles(groups: ReadonlyArray): Output["files"] { function renderImportedEffectFiles( groups: ReadonlyArray, options: - | { readonly module: string; readonly api: string } - | { readonly module: string; readonly group: string } - | { readonly module: string; readonly endpoints: Readonly> }, + | { readonly module: string; readonly api: string; readonly shapeModule?: string } + | { readonly module: string; readonly group: string; readonly shapeModule?: string } + | { + readonly module: string + readonly endpoints: Readonly> + readonly shapeModule?: string + }, ): Output["files"] { const adapters = groups.map((group, groupIndex) => { const rawGroup = group.endpoints[0]?.topLevel ? "RawClient" : `RawClient[${JSON.stringify(group.sourceIdentifier)}]` @@ -514,7 +647,21 @@ function renderImportedEffectFiles( // produces one object containing a union value. The shapes are equivalent but TypeScript cannot correlate them. const rawCall = `raw[${JSON.stringify(item.endpoint.identifier)}]({ ${request} }${isOpaquePayload(item) ? ` as ${prefix}Request` : ""})` const mapped = `${rawCall}.pipe(Effect.mapError(mapClientError)${item.unwrapData ? ", Effect.map((value) => value.data)" : ""})` - return `${item.operation.inputMode === "none" ? "" : `type ${prefix}Request = Parameters<${rawGroup}[${JSON.stringify(item.endpoint.identifier)}]>[0]\ntype ${prefix}Input = { ${input} }\n`}const ${prefix} = (raw: ${rawGroup}) => (${argument}) => ${item.operation.success === "stream" ? `Stream.unwrap(${rawCall}.pipe(Effect.mapError(mapClientError), Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError)))))` : mapped}` + const result = + item.operation.success === "stream" + ? `Stream.unwrap(${rawCall}.pipe(Effect.mapError(mapClientError), Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError)))))` + : mapped + const output = + options.shapeModule === undefined + ? result + : `${item.operation.success === "stream" ? "preserveStream" : "preserveEffect"}<${prefix}Output>()(${result})` + const declarations = + options.shapeModule === undefined && item.operation.inputMode !== "none" + ? `type ${prefix}Request = Parameters<${rawGroup}[${JSON.stringify(item.endpoint.identifier)}]>[0]\ntype ${prefix}Input = { ${input} }\n` + : isOpaquePayload(item) + ? `type ${prefix}Request = Parameters<${rawGroup}[${JSON.stringify(item.endpoint.identifier)}]>[0]\n` + : "" + return `${declarations}const ${prefix} = (raw: ${rawGroup}) => (${argument}) => ${output}` }) const fields = renderClientTree( group.endpoints, @@ -542,7 +689,21 @@ function renderImportedEffectFiles( ? `import { ${api} } from ${JSON.stringify(options.module)}` : `import { HttpApi, HttpApiClient${"endpoints" in options ? ", HttpApiGroup" : ""} } from "effect/unstable/httpapi"\nimport { ${projection.imports.join(", ")} } from ${JSON.stringify(options.module)}` const httpApiImport = projection === undefined ? 'import { HttpApiClient } from "effect/unstable/httpapi"\n' : "" - const client = `// Generated by @opencode-ai/httpapi-codegen. Do not edit.\nimport { Effect${usesStream ? ", Stream" : ""}, Schema } from "effect"\nimport { Sse } from "effect/unstable/encoding"\nimport { HttpClientError } from "effect/unstable/http"\n${httpApiImport}${imports}\nimport { ClientError } from "./client-error"\n\n${projection?.source ?? ""}type RawClient = HttpApiClient.ForApi\n\nconst mapClientError = (error: E) => HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error) ? new ClientError({ cause: error }) : error\n\n${adapters.join("\n\n")}\n\nconst adaptClient = (raw: RawClient) => ({ ${fields.join(", ")} })\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) => HttpApiClient.make(${api}, options).pipe(Effect.map(adaptClient))\n` + const shapeTypes = groups.flatMap((group, groupIndex) => + group.endpoints.flatMap((endpoint, endpointIndex) => [ + ...(endpoint.operation.inputMode === "none" ? [] : [`Endpoint${groupIndex}_${endpointIndex}Input`]), + `Endpoint${groupIndex}_${endpointIndex}Output`, + ]), + ) + const shapeImport = + options.shapeModule === undefined + ? "" + : `import type { ${shapeTypes.join(", ")} } from ${JSON.stringify(options.shapeModule)}\n` + const preserve = + options.shapeModule === undefined + ? "" + : `const preserveEffect = () => (effect: Effect.Effect) => effect\n${usesStream ? "const preserveStream = () => (stream: Stream.Stream) => stream\n" : ""}\n` + const client = `// Generated by @opencode-ai/httpapi-codegen. Do not edit.\nimport { Effect${usesStream ? ", Stream" : ""}, Schema } from "effect"\nimport { Sse } from "effect/unstable/encoding"\nimport { HttpClientError } from "effect/unstable/http"\n${httpApiImport}${imports}\n${shapeImport}import { ClientError } from "./client-error"\n\n${projection?.source ?? ""}type RawClient = HttpApiClient.ForApi\n\nconst mapClientError = (error: E) => HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error) ? new ClientError({ cause: error }) : error\n\n${preserve}${adapters.join("\n\n")}\n\nconst adaptClient = (raw: RawClient) => ({ ${fields.join(", ")} })\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) => HttpApiClient.make(${api}, options).pipe(Effect.map(adaptClient))\n` return [ { path: "client-error.ts", diff --git a/packages/httpapi-codegen/test/generate.test.ts b/packages/httpapi-codegen/test/generate.test.ts index 7374bd549034..32a400d471c2 100644 --- a/packages/httpapi-codegen/test/generate.test.ts +++ b/packages/httpapi-codegen/test/generate.test.ts @@ -96,6 +96,65 @@ describe("HttpApiCodegen.generate", () => { ) }) + test("generates Effect API types from schemas instead of the imported API", () => { + const Info = Schema.Struct({ id: Schema.String }).annotate({ identifier: "Session.Info" }) + const output = emitEffectShape( + compileContract( + api( + HttpApiEndpoint.get("get", "/session/:id", { + params: { id: Schema.String }, + success: Schema.Struct({ data: Info }), + }), + ), + ), + { + typeReferences: [ + { + schema: Info, + name: "Session.Info", + import: 'import type { Session } from "@example/schema/session"', + }, + ], + }, + ) + const source = output.files[0]?.content + + expect(source).toContain('import type { Session } from "@example/schema/session"') + expect(source).toContain('export type Endpoint0_0Input = { readonly "id": string }') + expect(source).toContain("export type Endpoint0_0Output = Session.Info") + expect(source).not.toContain("HttpApiClient") + expect(source).not.toContain("@example/api") + }) + + test("allows composed Effect outputs to use an authoritative named type", () => { + const output = emitEffectShape( + compileContract(api(HttpApiEndpoint.get("events", "/event", { success: Schema.Unknown }))), + { + outputTypes: { + "session.events": { + name: "OpenCodeEvent", + import: 'import type { OpenCodeEvent } from "@example/protocol/event"', + }, + }, + }, + ) + const source = output.files[0]?.content + + expect(source).toContain('import type { OpenCodeEvent } from "@example/protocol/event"') + expect(source).toContain("export type Endpoint0_0Output = OpenCodeEvent") + }) + + test("exposes an imported Effect client through its generated shape", () => { + const output = emitEffectImported( + compileContract(api(HttpApiEndpoint.get("get", "/session", { success: Schema.String }))), + { module: "@example/api", api: "Api", shapeModule: "../api" }, + ) + const source = output.files.find((file) => file.path === "client.ts")?.content + + expect(source).toContain('import type { Endpoint0_0Output } from "../api"') + expect(source).toContain("preserveEffect()") + }) + test("projects imported endpoint constants into a generated API", () => { const output = emitEffectImported( compileContract( @@ -220,7 +279,7 @@ describe("HttpApiCodegen.generate", () => { '"instructions": { "list": Endpoint0_0(raw), "put": Endpoint0_1(raw), "remove": Endpoint0_2(raw) }', ) - const shape = emitEffectShape(contract, { module: "@example/api", api: "Api" }) + const shape = emitEffectShape(contract) const apiShape = shape.files.find((file) => file.path === "api.ts")?.content expect(apiShape).toContain('readonly "instructions": { readonly "list": SessionInstructionsListOperation') expect(apiShape).toContain('readonly "put": SessionInstructionsPutOperation') @@ -1033,7 +1092,7 @@ describe("HttpApiCodegen.generate", () => { const contract = compileContract(source) const effect = emitEffect(contract) const imported = emitEffectImported(contract, { module: "@example/api", api: "Api" }) - const shape = emitEffectShape(contract, { module: "@example/api", api: "Api" }) + const shape = emitEffectShape(contract) const promise = emitPromise(contract) expect(effect.operations[0]).toMatchObject({ @@ -1042,7 +1101,9 @@ describe("HttpApiCodegen.generate", () => { }) expect(effect.files.find((file) => file.path === "session.ts")?.content).toContain('payload: input["payload"]') expect(imported.files.find((file) => file.path === "client.ts")?.content).toContain('payload: input["payload"]') - expect(shape.files[0]?.content).toContain('Endpoint0_0Request["payload"]') + expect(shape.files[0]?.content).toContain( + 'readonly "payload": { readonly "type": "local", readonly "command": ReadonlyArray }', + ) expect(promise.files.find((file) => file.path === "types.ts")?.content).toContain( 'readonly "payload": { readonly "type": "local", readonly "command": ReadonlyArray } | { readonly "type": "remote", readonly "url": string }', ) diff --git a/packages/plugin/AGENTS.md b/packages/plugin/AGENTS.md new file mode 100644 index 000000000000..0d98cecc8886 --- /dev/null +++ b/packages/plugin/AGENTS.md @@ -0,0 +1,6 @@ +# Plugin Package Guide + +- The plugin package has two versions: Effect and Promise. +- In the Effect version, every domain must extend the corresponding Effect API client interface from `@opencode-ai/client/effect/api`. +- Do not redefine functions that already exist on the Effect API client interface. +- Plugin domains add only the additional functions that make sense in the plugin context. diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 942de6460a6a..9973af86154d 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -10,15 +10,11 @@ "build": "tsc -p tsconfig.build.json" }, "exports": { - ".": "./src/index.ts", - "./tool": "./src/tool.ts", - "./tui": "./src/tui.ts", - "./v2/effect": "./src/v2/effect/index.ts", - "./v2/effect/*": "./src/v2/effect/*.ts", - "./v2/tui": "./src/v2/tui/index.ts", - "./v2/tui/*": "./src/v2/tui/*.ts", - "./v2": "./src/v2/promise/index.ts", - "./v2/*": "./src/v2/promise/*.ts" + ".": "./src/promise/index.ts", + "./effect": "./src/effect/index.ts", + "./tui": "./src/tui/index.ts", + "./v1": "./src/v1/index.ts", + "./*": "./src/*.ts" }, "files": [ "dist" @@ -29,7 +25,7 @@ "@opencode-ai/client": "workspace:*", "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "1.18.5", - "@standard-schema/spec": "^1.1.0", + "@standard-schema/spec": "catalog:", "effect": "catalog:", "zod": "catalog:" }, diff --git a/packages/plugin/src/v2/promise/README.md b/packages/plugin/src/README.md similarity index 91% rename from packages/plugin/src/v2/promise/README.md rename to packages/plugin/src/README.md index c7ffbfd251c4..2063922f85ab 100644 --- a/packages/plugin/src/v2/promise/README.md +++ b/packages/plugin/src/README.md @@ -1,6 +1,6 @@ # OpenCode V2 Promise Plugin API -The Promise plugin API at `@opencode-ai/plugin/v2` is the async/await equivalent of `@opencode-ai/plugin/v2/effect`. It grants plugins the same two in-process capabilities: +The Promise plugin API at `@opencode-ai/plugin` is the async/await equivalent of `@opencode-ai/plugin/effect`. It grants plugins the same two in-process capabilities: - `hook` installs behavior at an OpenCode extension point. - `reload` reruns every transform hook for a stateful domain. @@ -10,7 +10,7 @@ The only difference from the Effect API is the async boundary: hook callbacks, h ## Defining A Plugin ```ts -import { Plugin } from "@opencode-ai/plugin/v2" +import { Plugin } from "@opencode-ai/plugin" export default Plugin.define({ id: "example", @@ -99,17 +99,16 @@ supplies the tool's name and options separately: ```ts import { Schema } from "effect" -import { Tool } from "@opencode-ai/plugin/v2/tool" await ctx.tool.transform((tools) => { tools.add( "echo", - Tool.make({ + { description: "Echo text", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), execute: async ({ text }) => ({ output: { text }, content: text }), - }), + }, ) }) ``` diff --git a/packages/plugin/src/v2/app.ts b/packages/plugin/src/app.ts similarity index 100% rename from packages/plugin/src/v2/app.ts rename to packages/plugin/src/app.ts diff --git a/packages/plugin/src/v2/effect/PLAN.md b/packages/plugin/src/effect/PLAN.md similarity index 98% rename from packages/plugin/src/v2/effect/PLAN.md rename to packages/plugin/src/effect/PLAN.md index 12cac7deea44..286ba2e9861e 100644 --- a/packages/plugin/src/v2/effect/PLAN.md +++ b/packages/plugin/src/effect/PLAN.md @@ -7,7 +7,7 @@ This document describes the agreed target design for the V2 plugin system. It is ## Goals - Internal and external plugins use the same public plugin API. -- Effect plugins import `@opencode-ai/plugin/v2/effect`, not `@opencode-ai/core`. +- Effect plugins import `@opencode-ai/plugin/effect`, not `@opencode-ai/core`. - Public domain values use generated `@opencode-ai/sdk` types. - Core may retain branded IDs, decoded Effect schemas, and internal service types. - Plugins may register replayable domain transforms and runtime hooks imperatively during setup. @@ -208,7 +208,7 @@ type EventMap = { } ``` -Core resolves the public event type string to its internal event definition and delegates to `EventV2.Service.subscribe`. +Core resolves the public event type string to its internal event definition and delegates to `Event.Service.subscribe`. ## Domain State Model @@ -304,7 +304,7 @@ export const ModelsDevPlugin = define({ effect: (ctx) => Effect.gen(function* () { const modelsDev = yield* ModelsDev.Service - const event = yield* EventV2.Service + const event = yield* Event.Service yield* ctx.integration.transform( Effect.fn(function* (integration) { @@ -424,7 +424,7 @@ The Effect implementation remains the canonical runtime. Promise and embedding w ### 1. Define Public Contracts -- Define `PluginHost` domain capabilities in `@opencode-ai/plugin/v2/effect`. +- Define `PluginHost` domain capabilities in `@opencode-ai/plugin/effect`. - Define SDK-typed editors for agent, catalog, command, integration, reference, skill, and tool. - Define typed runtime hook maps per domain. - Define `Registration`. @@ -483,7 +483,7 @@ The Effect implementation remains the canonical runtime. Promise and embedding w ### 8. Add Event Adapter - Build the SDK event discriminant map. -- Resolve public type strings to internal EventV2 definitions. +- Resolve public type strings to internal Event definitions. - Return typed Effect streams. ### 9. Verification diff --git a/packages/plugin/src/v2/effect/README.md b/packages/plugin/src/effect/README.md similarity index 98% rename from packages/plugin/src/v2/effect/README.md rename to packages/plugin/src/effect/README.md index 8ce7ddb41450..492513338d48 100644 --- a/packages/plugin/src/v2/effect/README.md +++ b/packages/plugin/src/effect/README.md @@ -8,7 +8,7 @@ The Effect plugin API grants plugins two in-process capabilities: ## Defining A Plugin ```ts -import { Plugin } from "@opencode-ai/plugin/v2/effect" +import { Plugin } from "@opencode-ai/plugin/effect" import { Effect } from "effect" export default Plugin.define({ diff --git a/packages/plugin/src/effect/agent.ts b/packages/plugin/src/effect/agent.ts new file mode 100644 index 000000000000..81bd1d03141c --- /dev/null +++ b/packages/plugin/src/effect/agent.ts @@ -0,0 +1,17 @@ +import type { AgentApi } from "@opencode-ai/client/effect/api" +import type { Agent } from "@opencode-ai/schema/agent" +import type { Effect, Types } from "effect" +import type { Transform } from "./registration.js" + +export interface AgentDraft { + list(): readonly Types.DeepMutable[] + get(id: string): Types.DeepMutable | undefined + default(id: string | undefined): void + update(id: string, update: (agent: Types.DeepMutable) => void): void + remove(id: string): void +} + +export interface AgentDomain extends AgentApi { + readonly transform: Transform + readonly reload: () => Effect.Effect +} diff --git a/packages/plugin/src/v2/effect/aisdk.ts b/packages/plugin/src/effect/aisdk.ts similarity index 100% rename from packages/plugin/src/v2/effect/aisdk.ts rename to packages/plugin/src/effect/aisdk.ts diff --git a/packages/plugin/src/v2/effect/catalog.ts b/packages/plugin/src/effect/catalog.ts similarity index 51% rename from packages/plugin/src/v2/effect/catalog.ts rename to packages/plugin/src/effect/catalog.ts index 2d2f6777c6b0..42395e0eb6d2 100644 --- a/packages/plugin/src/v2/effect/catalog.ts +++ b/packages/plugin/src/effect/catalog.ts @@ -1,26 +1,24 @@ -import type { ModelInfo, ProviderV2Info } from "@opencode-ai/sdk/v2/types" import type { CatalogApi } from "@opencode-ai/client/effect/api" -import type { Model } from "@opencode-ai/schema/model" -import type { Effect } from "effect" +import { Model } from "@opencode-ai/schema/model" +import { Provider } from "@opencode-ai/schema/provider" +import type { Effect, Types } from "effect" import type { Transform } from "./registration.js" -type CatalogModel = ModelInfo & { compatibility?: Model.Compatibility } - export interface CatalogProviderRecord { - readonly provider: ProviderV2Info - readonly models: ReadonlyMap + readonly provider: Types.DeepMutable + readonly models: ReadonlyMap> } export interface CatalogDraft { readonly provider: { list(): readonly CatalogProviderRecord[] get(providerID: string): CatalogProviderRecord | undefined - update(providerID: string, update: (provider: ProviderV2Info) => void): void + update(providerID: string, update: (provider: Types.DeepMutable) => void): void remove(providerID: string): void } readonly model: { - get(providerID: string, modelID: string): CatalogModel | undefined - update(providerID: string, modelID: string, update: (model: CatalogModel) => void): void + get(providerID: string, modelID: string): Types.DeepMutable | undefined + update(providerID: string, modelID: string, update: (model: Types.DeepMutable) => void): void remove(providerID: string, modelID: string): void readonly default: { get(): { providerID: string; modelID: string } | undefined @@ -30,11 +28,6 @@ export interface CatalogDraft { } export interface CatalogDomain extends CatalogApi { - readonly model: CatalogApi["model"] & { - readonly get: (providerID: string, modelID: string) => Effect.Effect - } readonly transform: Transform readonly reload: () => Effect.Effect } - -type ModelGetOutput = Effect.Success["model"]["list"]>>["data"][number] diff --git a/packages/plugin/src/v2/effect/command.ts b/packages/plugin/src/effect/command.ts similarity index 89% rename from packages/plugin/src/v2/effect/command.ts rename to packages/plugin/src/effect/command.ts index 6e4764a4855c..59be027df912 100644 --- a/packages/plugin/src/v2/effect/command.ts +++ b/packages/plugin/src/effect/command.ts @@ -1,5 +1,5 @@ -import type { CommandInfo } from "@opencode-ai/sdk/v2/types" import type { CommandApi } from "@opencode-ai/client/effect/api" +import type { CommandInfo } from "@opencode-ai/client" import type { Effect } from "effect" import type { Transform } from "./registration.js" diff --git a/packages/plugin/src/v2/effect/event.ts b/packages/plugin/src/effect/event.ts similarity index 100% rename from packages/plugin/src/v2/effect/event.ts rename to packages/plugin/src/effect/event.ts diff --git a/packages/plugin/src/v2/effect/index.ts b/packages/plugin/src/effect/index.ts similarity index 100% rename from packages/plugin/src/v2/effect/index.ts rename to packages/plugin/src/effect/index.ts diff --git a/packages/plugin/src/v2/effect/integration.ts b/packages/plugin/src/effect/integration.ts similarity index 80% rename from packages/plugin/src/v2/effect/integration.ts rename to packages/plugin/src/effect/integration.ts index 2cbfe24612ac..d4c9c76b276f 100644 --- a/packages/plugin/src/v2/effect/integration.ts +++ b/packages/plugin/src/effect/integration.ts @@ -1,19 +1,19 @@ import type { ConnectionInfo, - CredentialOAuth, - CredentialValue, IntegrationCommandMethod, IntegrationEnvMethod, - IntegrationInputs, IntegrationKeyMethod, IntegrationMethod, IntegrationOAuthMethod, - IntegrationRef, -} from "@opencode-ai/sdk/v2/types" +} from "@opencode-ai/client" import type { IntegrationApi } from "@opencode-ai/client/effect/api" +import { Credential } from "@opencode-ai/schema/credential" import type { Effect, Scope } from "effect" import type { Transform } from "./registration.js" +type IntegrationInputs = Record +type IntegrationRef = { id: string; name: string } + export type IntegrationOAuthAuthorization = { readonly url: string readonly instructions: string @@ -21,19 +21,19 @@ export type IntegrationOAuthAuthorization = { } & ( | { readonly mode: "auto" - readonly callback: Effect.Effect + readonly callback: Effect.Effect } | { readonly mode: "code" - readonly callback: (code: string) => Effect.Effect + readonly callback: (code: string) => Effect.Effect } ) export type IntegrationOAuthMethodRegistration = { readonly integrationID: string readonly method: IntegrationOAuthMethod readonly authorize: (inputs: IntegrationInputs) => Effect.Effect - readonly refresh?: (credential: CredentialOAuth) => Effect.Effect - readonly label?: (credential: CredentialOAuth) => string | undefined + readonly refresh?: (credential: Credential.OAuth) => Effect.Effect + readonly label?: (credential: Credential.OAuth) => string | undefined } export type IntegrationMethodRegistration = | IntegrationOAuthMethodRegistration @@ -67,6 +67,6 @@ export interface IntegrationDomain extends Omit, "wellkn readonly reload: () => Effect.Effect readonly connection: { readonly active: (integrationID: string) => Effect.Effect - readonly resolve: (connection: ConnectionInfo) => Effect.Effect + readonly resolve: (connection: ConnectionInfo) => Effect.Effect } } diff --git a/packages/plugin/src/v2/effect/plugin.ts b/packages/plugin/src/effect/plugin.ts similarity index 100% rename from packages/plugin/src/v2/effect/plugin.ts rename to packages/plugin/src/effect/plugin.ts diff --git a/packages/plugin/src/v2/effect/reference.ts b/packages/plugin/src/effect/reference.ts similarity index 95% rename from packages/plugin/src/v2/effect/reference.ts rename to packages/plugin/src/effect/reference.ts index 085ae0de5e80..f9f92a9cf960 100644 --- a/packages/plugin/src/v2/effect/reference.ts +++ b/packages/plugin/src/effect/reference.ts @@ -1,4 +1,4 @@ -import type { ReferenceGitSource, ReferenceLocalSource } from "@opencode-ai/sdk/v2/types" +import type { ReferenceGitSource, ReferenceLocalSource } from "@opencode-ai/client" import type { ReferenceApi } from "@opencode-ai/client/effect/api" import type { Effect } from "effect" import type { Transform } from "./registration.js" diff --git a/packages/plugin/src/v2/effect/registration.ts b/packages/plugin/src/effect/registration.ts similarity index 100% rename from packages/plugin/src/v2/effect/registration.ts rename to packages/plugin/src/effect/registration.ts diff --git a/packages/plugin/src/v2/effect/session.ts b/packages/plugin/src/effect/session.ts similarity index 100% rename from packages/plugin/src/v2/effect/session.ts rename to packages/plugin/src/effect/session.ts diff --git a/packages/plugin/src/v2/effect/skill.ts b/packages/plugin/src/effect/skill.ts similarity index 71% rename from packages/plugin/src/v2/effect/skill.ts rename to packages/plugin/src/effect/skill.ts index 32daf0fb0a57..26439bcfc918 100644 --- a/packages/plugin/src/v2/effect/skill.ts +++ b/packages/plugin/src/effect/skill.ts @@ -1,11 +1,11 @@ -import type { SkillSource } from "@opencode-ai/sdk/v2/types" import type { SkillApi } from "@opencode-ai/client/effect/api" +import { Skill } from "@opencode-ai/schema/skill" import type { Effect } from "effect" import type { Transform } from "./registration.js" export interface SkillDraft { - source(source: SkillSource): void - list(): readonly SkillSource[] + source(source: Skill.Source): void + list(): readonly Skill.Source[] } export interface SkillDomain extends SkillApi { diff --git a/packages/plugin/src/effect/tool.ts b/packages/plugin/src/effect/tool.ts new file mode 100644 index 000000000000..3672496d6207 --- /dev/null +++ b/packages/plugin/src/effect/tool.ts @@ -0,0 +1,45 @@ +import { Tool } from "@opencode-ai/schema/tool" +import type { Agent } from "@opencode-ai/schema/agent" +import type { Session } from "@opencode-ai/schema/session" +import type { SessionMessage } from "@opencode-ai/schema/session-message" +import type { Hooks, Transform } from "./registration.js" + +interface ToolDraft { + add< + Input extends Tool.ValueSchema, + Output extends Tool.ValueSchema | undefined, + >(tool: Tool.Info): void +} + +export interface ToolHooks { + readonly "execute.before": { + readonly tool: string + readonly sessionID: Session.ID + readonly agent: Agent.ID + readonly messageID: SessionMessage.ID + readonly callID: Tool.CallID + input: unknown + } + readonly "execute.after": { + readonly tool: string + readonly sessionID: Session.ID + readonly agent: Agent.ID + readonly messageID: SessionMessage.ID + readonly callID: Tool.CallID + readonly input: unknown + } & ( + | { + readonly status: "completed" + result: Tool.Result + } + | { + readonly status: "error" + error: Tool.Error + } + ) +} + +export interface ToolDomain { + readonly transform: Transform + readonly hook: Hooks +} diff --git a/packages/plugin/src/v2/effect/websearch.ts b/packages/plugin/src/effect/websearch.ts similarity index 100% rename from packages/plugin/src/v2/effect/websearch.ts rename to packages/plugin/src/effect/websearch.ts diff --git a/packages/plugin/src/example-workspace.ts b/packages/plugin/src/example-workspace.ts deleted file mode 100644 index 9253284507ff..000000000000 --- a/packages/plugin/src/example-workspace.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Plugin } from "@opencode-ai/plugin" -import { mkdir, rm } from "node:fs/promises" - -export const FolderWorkspacePlugin: Plugin = async ({ experimental_workspace }) => { - experimental_workspace.register("folder", { - name: "Folder", - description: "Create a blank folder", - configure(config) { - const rand = "" + Math.random() - - return { - ...config, - directory: `/tmp/folder/folder-${rand}`, - } - }, - async create(config) { - if (!config.directory) return - await mkdir(config.directory, { recursive: true }) - }, - async remove(config) { - await rm(config.directory!, { recursive: true, force: true }) - }, - target(config) { - return { - type: "local", - directory: config.directory!, - } - }, - }) - - return {} -} - -export default FolderWorkspacePlugin diff --git a/packages/plugin/src/example.ts b/packages/plugin/src/example.ts deleted file mode 100644 index bf79ab511e40..000000000000 --- a/packages/plugin/src/example.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Plugin } from "./index.js" -import { tool } from "./tool.js" - -export const ExamplePlugin: Plugin = async (_ctx) => { - return { - tool: { - mytool: tool({ - description: "This is a custom tool", - args: { - foo: tool.schema.string().describe("foo"), - }, - async execute(args) { - return `Hello ${args.foo}!` - }, - }), - }, - } -} diff --git a/packages/plugin/src/v2/options.ts b/packages/plugin/src/options.ts similarity index 100% rename from packages/plugin/src/v2/options.ts rename to packages/plugin/src/options.ts diff --git a/packages/plugin/src/promise/agent.ts b/packages/plugin/src/promise/agent.ts new file mode 100644 index 000000000000..178d2bac3391 --- /dev/null +++ b/packages/plugin/src/promise/agent.ts @@ -0,0 +1,17 @@ +import type { AgentApi } from "@opencode-ai/client/promise/api" +import type { Agent } from "@opencode-ai/schema/agent" +import type { Transform } from "./registration.js" +import type { DeepMutable } from "./types.js" + +export interface AgentDraft { + list(): readonly DeepMutable[] + get(id: string): DeepMutable | undefined + default(id: string | undefined): void + update(id: string, update: (agent: DeepMutable) => void): void + remove(id: string): void +} + +export interface AgentDomain extends AgentApi { + readonly transform: Transform + readonly reload: () => Promise +} diff --git a/packages/plugin/src/v2/promise/aisdk.ts b/packages/plugin/src/promise/aisdk.ts similarity index 100% rename from packages/plugin/src/v2/promise/aisdk.ts rename to packages/plugin/src/promise/aisdk.ts diff --git a/packages/plugin/src/promise/catalog.ts b/packages/plugin/src/promise/catalog.ts new file mode 100644 index 000000000000..49f9b0b48d72 --- /dev/null +++ b/packages/plugin/src/promise/catalog.ts @@ -0,0 +1,33 @@ +import type { CatalogApi } from "@opencode-ai/client/promise/api" +import type { Model } from "@opencode-ai/schema/model" +import type { Provider } from "@opencode-ai/schema/provider" +import type { Transform } from "./registration.js" +import type { DeepMutable } from "./types.js" + +export interface CatalogProviderRecord { + readonly provider: DeepMutable + readonly models: ReadonlyMap> +} + +export interface CatalogDraft { + readonly provider: { + list(): readonly CatalogProviderRecord[] + get(providerID: string): CatalogProviderRecord | undefined + update(providerID: string, update: (provider: DeepMutable) => void): void + remove(providerID: string): void + } + readonly model: { + get(providerID: string, modelID: string): DeepMutable | undefined + update(providerID: string, modelID: string, update: (model: DeepMutable) => void): void + remove(providerID: string, modelID: string): void + readonly default: { + get(): { providerID: string; modelID: string } | undefined + set(providerID: string, modelID: string): void + } + } +} + +export interface CatalogDomain extends CatalogApi { + readonly transform: Transform + readonly reload: () => Promise +} diff --git a/packages/plugin/src/promise/command.ts b/packages/plugin/src/promise/command.ts new file mode 100644 index 000000000000..cc9de3f3cb91 --- /dev/null +++ b/packages/plugin/src/promise/command.ts @@ -0,0 +1,15 @@ +import type { CommandApi } from "@opencode-ai/client/promise/api" +import type { CommandInfo } from "@opencode-ai/client" +import type { Transform } from "./registration.js" + +export interface CommandDraft { + list(): readonly CommandInfo[] + get(name: string): CommandInfo | undefined + update(name: string, update: (command: CommandInfo) => void): void + remove(name: string): void +} + +export interface CommandDomain extends CommandApi { + readonly transform: Transform + readonly reload: () => Promise +} diff --git a/packages/plugin/src/v2/promise/event.ts b/packages/plugin/src/promise/event.ts similarity index 100% rename from packages/plugin/src/v2/promise/event.ts rename to packages/plugin/src/promise/event.ts diff --git a/packages/plugin/src/v2/promise/index.ts b/packages/plugin/src/promise/index.ts similarity index 100% rename from packages/plugin/src/v2/promise/index.ts rename to packages/plugin/src/promise/index.ts diff --git a/packages/plugin/src/promise/integration.ts b/packages/plugin/src/promise/integration.ts new file mode 100644 index 000000000000..8e598fc8dcd3 --- /dev/null +++ b/packages/plugin/src/promise/integration.ts @@ -0,0 +1,64 @@ +import type { + ConnectionInfo, + IntegrationCommandMethod, + IntegrationEnvMethod, + IntegrationKeyMethod, + IntegrationMethod, + IntegrationOAuthMethod, +} from "@opencode-ai/client" +import type { IntegrationApi } from "@opencode-ai/client/promise/api" +import { Credential } from "@opencode-ai/schema/credential" +import type { Transform } from "./registration.js" + +type IntegrationInputs = Record +type IntegrationRef = { id: string; name: string } + +export type IntegrationOAuthAuthorization = { + readonly url: string + readonly instructions: string + readonly expiresAt?: number +} & ( + | { + readonly mode: "auto" + readonly callback: Promise + } + | { + readonly mode: "code" + readonly callback: (code: string) => Promise + } +) + +export type IntegrationOAuthMethodRegistration = { + readonly integrationID: string + readonly method: IntegrationOAuthMethod + readonly authorize: (inputs: IntegrationInputs) => Promise + readonly refresh?: (credential: Credential.OAuth) => Promise + readonly label?: (credential: Credential.OAuth) => string | undefined +} + +export type IntegrationMethodRegistration = + | IntegrationOAuthMethodRegistration + | { readonly integrationID: string; readonly method: IntegrationCommandMethod } + | { readonly integrationID: string; readonly method: IntegrationKeyMethod } + | { readonly integrationID: string; readonly method: IntegrationEnvMethod } + +export interface IntegrationDraft { + list(): readonly IntegrationRef[] + get(id: string): IntegrationRef | undefined + update(id: string, update: (integration: IntegrationRef) => void): void + remove(id: string): void + readonly method: { + list(integrationID: string): readonly IntegrationMethod[] + update(input: IntegrationMethodRegistration): void + remove(integrationID: string, method: IntegrationMethod): void + } +} + +export interface IntegrationDomain extends Omit { + readonly transform: Transform + readonly reload: () => Promise + readonly connection: { + readonly active: (integrationID: string) => Promise + readonly resolve: (connection: ConnectionInfo) => Promise + } +} diff --git a/packages/plugin/src/v2/promise/plugin.ts b/packages/plugin/src/promise/plugin.ts similarity index 100% rename from packages/plugin/src/v2/promise/plugin.ts rename to packages/plugin/src/promise/plugin.ts diff --git a/packages/plugin/src/promise/reference.ts b/packages/plugin/src/promise/reference.ts new file mode 100644 index 000000000000..7f5c43bf67c0 --- /dev/null +++ b/packages/plugin/src/promise/reference.ts @@ -0,0 +1,14 @@ +import type { ReferenceApi } from "@opencode-ai/client/promise/api" +import type { ReferenceGitSource, ReferenceLocalSource } from "@opencode-ai/client" +import type { Transform } from "./registration.js" + +export interface ReferenceDraft { + add(name: string, source: ReferenceLocalSource | ReferenceGitSource): void + remove(name: string): void + list(): readonly (readonly [string, ReferenceLocalSource | ReferenceGitSource])[] +} + +export interface ReferenceDomain extends ReferenceApi { + readonly transform: Transform + readonly reload: () => Promise +} diff --git a/packages/plugin/src/v2/promise/registration.ts b/packages/plugin/src/promise/registration.ts similarity index 100% rename from packages/plugin/src/v2/promise/registration.ts rename to packages/plugin/src/promise/registration.ts diff --git a/packages/plugin/src/v2/promise/session.ts b/packages/plugin/src/promise/session.ts similarity index 100% rename from packages/plugin/src/v2/promise/session.ts rename to packages/plugin/src/promise/session.ts diff --git a/packages/plugin/src/v2/promise/skill.ts b/packages/plugin/src/promise/skill.ts similarity index 61% rename from packages/plugin/src/v2/promise/skill.ts rename to packages/plugin/src/promise/skill.ts index cbc6459a339e..e237bc788ffc 100644 --- a/packages/plugin/src/v2/promise/skill.ts +++ b/packages/plugin/src/promise/skill.ts @@ -1,8 +1,11 @@ import type { SkillApi } from "@opencode-ai/client/promise/api" -import type { SkillDraft } from "../effect/skill.js" +import type { Skill } from "@opencode-ai/schema/skill" import type { Transform } from "./registration.js" -export type { SkillDraft } +export interface SkillDraft { + source(source: Skill.Source): void + list(): readonly Skill.Source[] +} export interface SkillDomain extends SkillApi { readonly transform: Transform diff --git a/packages/plugin/src/promise/tool.ts b/packages/plugin/src/promise/tool.ts new file mode 100644 index 000000000000..4070a4a60078 --- /dev/null +++ b/packages/plugin/src/promise/tool.ts @@ -0,0 +1,62 @@ +export { CallID, Error } from "@opencode-ai/schema/tool" +export type { Metadata, Options, Result } from "@opencode-ai/schema/tool" + +import { Tool } from "@opencode-ai/schema/tool" +import type { Agent } from "@opencode-ai/schema/agent" +import type { Session } from "@opencode-ai/schema/session" +import type { SessionMessage } from "@opencode-ai/schema/session-message" +import type { Hooks, Transform } from "./registration.js" + +export interface ToolContext extends Omit { + readonly progress: (update: Tool.Metadata) => Promise +} + +export type Info< + Input extends Tool.ValueSchema = Tool.ValueSchema, + Output extends Tool.ValueSchema | undefined = Tool.ValueSchema | undefined, +> = Omit, "execute"> & { + readonly execute: ( + input: Parameters["execute"]>[0], + context: ToolContext, + ) => Promise> +} + +interface ToolDraft { + add< + Input extends Tool.ValueSchema, + Output extends Tool.ValueSchema | undefined, + >(tool: Info): void +} + +interface ToolHooks { + readonly "execute.before": { + readonly tool: string + readonly sessionID: Session.ID + readonly agent: Agent.ID + readonly messageID: SessionMessage.ID + readonly callID: Tool.CallID + input: unknown + } + readonly "execute.after": { + readonly tool: string + readonly sessionID: Session.ID + readonly agent: Agent.ID + readonly messageID: SessionMessage.ID + readonly callID: Tool.CallID + readonly input: unknown + } & ( + | { + readonly status: "completed" + result: Tool.Result + } + | { + readonly status: "error" + error: Tool.Error + } + ) +} + +export interface ToolDomain { + readonly transform: Transform + readonly hook: Hooks +} diff --git a/packages/plugin/src/promise/types.ts b/packages/plugin/src/promise/types.ts new file mode 100644 index 000000000000..418fa6c2439d --- /dev/null +++ b/packages/plugin/src/promise/types.ts @@ -0,0 +1,9 @@ +export type DeepMutable = A extends (...args: never[]) => unknown + ? A + : A extends ReadonlyMap + ? Map, DeepMutable> + : A extends ReadonlyArray + ? DeepMutable[] + : A extends object + ? { -readonly [K in keyof A]: DeepMutable } + : A diff --git a/packages/plugin/src/v2/promise/websearch.ts b/packages/plugin/src/promise/websearch.ts similarity index 100% rename from packages/plugin/src/v2/promise/websearch.ts rename to packages/plugin/src/promise/websearch.ts diff --git a/packages/plugin/src/v2/tui/context.ts b/packages/plugin/src/tui/context.ts similarity index 97% rename from packages/plugin/src/v2/tui/context.ts rename to packages/plugin/src/tui/context.ts index 8d7c99003a85..34cb863536c2 100644 --- a/packages/plugin/src/v2/tui/context.ts +++ b/packages/plugin/src/tui/context.ts @@ -10,8 +10,8 @@ import type { OpenCodeClient, OpenCodeEvent, PermissionSavedInfo, - PermissionV2Request, - ProviderV2Info, + PermissionRequest, + ProviderInfo, ReferenceInfo, SessionInfo, SessionMessageInfo, @@ -55,7 +55,7 @@ export interface Data { invalidate(sessionID: string): void } readonly permission: { - list(sessionID: string): PermissionV2Request[] | undefined + list(sessionID: string): PermissionRequest[] | undefined sync(sessionID: string): Promise invalidate(sessionID: string): void } @@ -90,7 +90,7 @@ export interface Data { readonly resource: LocationCollection } readonly model: LocationCollection - readonly provider: LocationCollection + readonly provider: LocationCollection readonly reference: LocationCollection readonly skill: LocationCollection } diff --git a/packages/plugin/src/v2/tui/index.ts b/packages/plugin/src/tui/index.ts similarity index 100% rename from packages/plugin/src/v2/tui/index.ts rename to packages/plugin/src/tui/index.ts diff --git a/packages/plugin/src/v2/tui/plugin.ts b/packages/plugin/src/tui/plugin.ts similarity index 100% rename from packages/plugin/src/v2/tui/plugin.ts rename to packages/plugin/src/tui/plugin.ts diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/v1/index.ts similarity index 100% rename from packages/plugin/src/index.ts rename to packages/plugin/src/v1/index.ts diff --git a/packages/plugin/src/shell.ts b/packages/plugin/src/v1/shell.ts similarity index 100% rename from packages/plugin/src/shell.ts rename to packages/plugin/src/v1/shell.ts diff --git a/packages/plugin/src/tool.ts b/packages/plugin/src/v1/tool.ts similarity index 100% rename from packages/plugin/src/tool.ts rename to packages/plugin/src/v1/tool.ts diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/v1/tui.ts similarity index 100% rename from packages/plugin/src/tui.ts rename to packages/plugin/src/v1/tui.ts diff --git a/packages/plugin/src/v2/effect/agent.ts b/packages/plugin/src/v2/effect/agent.ts deleted file mode 100644 index e14777af7793..000000000000 --- a/packages/plugin/src/v2/effect/agent.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { AgentApi } from "@opencode-ai/client/effect/api" -import type { AgentInfo } from "@opencode-ai/sdk/v2/types" -import type { Effect } from "effect" -import type { Transform } from "./registration.js" - -export interface AgentDraft { - list(): readonly AgentInfo[] - get(id: string): AgentInfo | undefined - default(id: string | undefined): void - update(id: string, update: (agent: AgentInfo) => void): void - remove(id: string): void -} - -export interface AgentDomain extends AgentApi { - readonly get: (id: string) => Effect.Effect - readonly transform: Transform - readonly reload: () => Effect.Effect -} - -type AgentGetOutput = Effect.Success["list"]>>["data"][number] diff --git a/packages/plugin/src/v2/effect/filesystem.ts b/packages/plugin/src/v2/effect/filesystem.ts deleted file mode 100644 index d242b2a692f1..000000000000 --- a/packages/plugin/src/v2/effect/filesystem.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { FileSystemEntry } from "@opencode-ai/sdk/v2/types" -import type { Effect } from "effect" - -export interface FileSystem { - read(input: { readonly path: string }): Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }> - list(input?: { readonly path?: string }): Effect.Effect - find(input: { - readonly query: string - readonly type?: "file" | "directory" - readonly limit?: number - }): Effect.Effect - glob(input: { - readonly pattern: string - readonly path?: string - readonly limit?: number - }): Effect.Effect -} diff --git a/packages/plugin/src/v2/effect/internal/tool.ts b/packages/plugin/src/v2/effect/internal/tool.ts deleted file mode 100644 index abc92c6a7d22..000000000000 --- a/packages/plugin/src/v2/effect/internal/tool.ts +++ /dev/null @@ -1,315 +0,0 @@ -import { Agent } from "@opencode-ai/schema/agent" -import { LLM } from "@opencode-ai/schema/llm" -import { Session } from "@opencode-ai/schema/session" -import { SessionError } from "@opencode-ai/schema/session-error" -import { SessionMessage } from "@opencode-ai/schema/session-message" -import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec" -import { Effect, JsonSchema, Schema } from "effect" -import type { Hooks, Transform } from "../registration.js" - -// Tools - -/** A JSON-compatible value. Tool metadata and encoded outputs must be JSON. */ -export type JsonValue = typeof Schema.Json.Type - -/** Compact JSON metadata for tool-specific UI and client behavior. */ -export type Metadata = Readonly> - -export interface Context { - readonly sessionID: Session.ID - readonly agent: Agent.ID - readonly messageID: SessionMessage.ID - readonly callID: string - readonly progress: (update: Progress) => Effect.Effect -} - -/** Live replacement metadata for a running tool. */ -export type Progress = Metadata - -export type StandardSchemaType = StandardSchemaV1 & - StandardJSONSchemaV1 -export type SchemaType = Schema.Codec | StandardSchemaType | JsonSchema.JsonSchema -type IsAny = 0 extends 1 & A ? true : false -export type InputValue = - IsAny extends true - ? any - : S extends Schema.Codec - ? A - : S extends StandardSchemaV1 - ? A - : unknown -export type OutputValue = - IsAny extends true - ? any - : S extends Schema.Codec - ? A - : S extends StandardSchemaV1 - ? A - : unknown -export type EncodedValue = - IsAny extends true - ? any - : S extends Schema.Codec - ? A - : S extends StandardSchemaV1 - ? A - : unknown - -type ToolDefinition = { - readonly name: string - readonly description: string - readonly inputSchema: JsonSchema.JsonSchema - readonly outputSchema?: JsonSchema.JsonSchema -} - -export class Failure extends Schema.TaggedErrorClass()("LLM.ToolFailure", { - message: Schema.String, - error: Schema.optional(Schema.Defect()), -}) {} - -export class RegistrationError extends Schema.TaggedErrorClass()("Tool.RegistrationError", { - name: Schema.String, - message: Schema.String, -}) {} - -export type Content = - | { readonly type: "text"; readonly text: string } - | { readonly type: "file"; readonly data: string; readonly mime: string; readonly name?: string } - -/** Model-facing tool content: plain text or non-empty rich content. */ -export type ModelOutput = string | readonly [Content, ...Content[]] - -type BaseTool> = { - readonly description: string - readonly input: Input -} - -export type Response> = { - readonly output: OutputValue - readonly content?: ModelOutput - readonly metadata?: Metadata -} - -export type ContentResponse = { - readonly content: ModelOutput - readonly metadata?: Metadata -} - -export type Tool< - Input extends SchemaType, - Output extends SchemaType | undefined = undefined, -> = BaseTool & - (Output extends SchemaType - ? { - readonly output: Output - readonly execute: (input: InputValue, context: Context) => Effect.Effect, Failure> - } - : { - readonly output?: undefined - readonly execute: (input: InputValue, context: Context) => Effect.Effect - }) - -export type Any = BaseTool & { - readonly output?: SchemaType - readonly execute: (input: any, context: Context) => Effect.Effect | ContentResponse, Failure> -} - -export function make, Output extends SchemaType>( - config: Tool, -): Tool -export function make>(config: Tool): Tool -export function make(config: Any): Any -export function make(config: Any): Any { - return config -} - -// Registration - -export interface RegisterOptions { - readonly namespace?: string - /** Defaults to true. False exposes the tool directly to the provider. */ - readonly codemode?: boolean - /** Permission action used for whole-tool visibility filtering. */ - readonly permission?: string -} - -export interface Registration { - readonly tool: Any - readonly name: string - readonly namespace?: string - readonly permission: string -} - -export const validateName = (name: string) => - /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(name) - ? Effect.void - : Effect.fail(new RegistrationError({ name, message: `Invalid tool name: ${name}` })) - -export const registrationEntries = ( - tools: Readonly>, - options?: RegisterOptions, -): Array => - Object.entries(tools).map(([name, tool]) => { - const normalized = name.replace(/[^a-zA-Z0-9_-]/g, "_") - const key = - options?.namespace === undefined ? normalized : `${options.namespace.replaceAll(".", "_")}_${normalized}` - return { - key, - name: normalized, - namespace: options?.namespace, - tool, - permission: options?.permission ?? key, - } - }) - -export const validateNamespace = (namespace: string) => - namespace.split(".").every((segment) => /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(segment)) - ? Effect.void - : Effect.fail( - new RegistrationError({ name: namespace, message: `Invalid tool namespace: ${JSON.stringify(namespace)}` }), - ) - -export const toLLMDefinition = (name: string, tool: Any): ToolDefinition => ({ - name, - description: tool.description, - inputSchema: inputJsonSchema(tool.input), - ...(tool.output === undefined ? {} : { outputSchema: outputJsonSchema(tool.output) }), -}) - -// Schema interpretation - -export function decodeInput(schema: SchemaType, value: unknown): Effect.Effect { - if (Schema.isSchema(schema)) - return Schema.decodeUnknownEffect(schema)(value).pipe( - Effect.mapError((error) => new Failure({ message: `Invalid tool input: ${error.message}` })), - ) - if (isStandardSchema(schema)) return validateStandard(schema, value, "Invalid tool input") - return Effect.succeed(value) -} - -export function encodeOutput(schema: SchemaType, value: unknown): Effect.Effect { - if (Schema.isSchema(schema)) - return Schema.encodeEffect(schema)(value).pipe( - Effect.mapError( - (error) => new Failure({ message: `Tool returned an invalid value for its output schema: ${error.message}` }), - ), - ) - if (isStandardSchema(schema)) - return validateStandard(schema, value, "Tool returned an invalid value for its output schema") - return Schema.decodeUnknownEffect(Schema.Json)(value).pipe( - Effect.mapError( - (error) => new Failure({ message: `Tool returned a non-JSON value for its output schema: ${error.message}` }), - ), - ) -} - -function isStandardSchema(schema: SchemaType): schema is StandardSchemaType { - return "~standard" in schema -} - -function validateStandard(schema: StandardSchemaType, value: unknown, prefix: string): Effect.Effect { - return Effect.gen(function* () { - const pending = yield* Effect.try({ - try: () => schema["~standard"].validate(value), - catch: (error) => standardFailure(prefix, error), - }) - const result = - pending instanceof Promise - ? yield* Effect.tryPromise({ try: () => pending, catch: (error) => standardFailure(prefix, error) }) - : pending - if (result.issues) - return yield* Effect.fail( - new Failure({ message: `${prefix}: ${result.issues.map((issue) => issue.message).join(", ")}` }), - ) - return result.value - }) -} - -function standardFailure(prefix: string, error: unknown) { - return new Failure({ message: `${prefix}: ${error instanceof Error ? error.message : String(error)}` }) -} - -function inputJsonSchema(schema: SchemaType): JsonSchema.JsonSchema { - if (isStandardSchema(schema)) - return schema["~standard"].jsonSchema.input({ target: "draft-2020-12" }) as JsonSchema.JsonSchema - return Schema.isSchema(schema) ? toJsonSchema(schema) : (schema as JsonSchema.JsonSchema) -} - -function outputJsonSchema(schema: SchemaType): JsonSchema.JsonSchema { - if (isStandardSchema(schema)) - return schema["~standard"].jsonSchema.output({ target: "draft-2020-12" }) as JsonSchema.JsonSchema - return Schema.isSchema(schema) ? toJsonSchema(schema) : (schema as JsonSchema.JsonSchema) -} - -function toJsonSchema(schema: Schema.Top): JsonSchema.JsonSchema { - const document = Schema.toJsonSchemaDocument(schema) - if (Object.keys(document.definitions).length === 0) return document.schema - return { ...document.schema, $defs: document.definitions } -} - -// Plugin events - -export interface ToolExecuteBeforeEvent { - readonly tool: string - readonly sessionID: Session.ID - readonly agent: Agent.ID - readonly messageID: SessionMessage.ID - readonly callID: string - input: unknown -} - -type ToolHookBase = { - readonly tool: string - readonly sessionID: Session.ID - readonly agent: Agent.ID - readonly messageID: SessionMessage.ID - readonly callID: string - readonly input: unknown -} - -export const ExecuteAfterOutcome = Schema.Union([ - Schema.Struct({ - status: Schema.Literal("completed"), - content: Schema.NonEmptyArray(LLM.ToolContent), - metadata: Schema.optional(Schema.Record(Schema.String, Schema.Json)), - outputPaths: Schema.optional(Schema.Array(Schema.String)), - }), - Schema.Struct({ - status: Schema.Literal("error"), - error: SessionError.Error, - content: Schema.optional(Schema.NonEmptyArray(LLM.ToolContent)), - metadata: Schema.optional(Schema.Record(Schema.String, Schema.Json)), - outputPaths: Schema.optional(Schema.Array(Schema.String)), - }), -]).pipe(Schema.toTaggedUnion("status")) - -type Mutable = { -readonly [K in keyof A]: A[K] } -type HookOutcome = Omit, "status"> & Pick - -/** The bounded terminal outcome exposed to tool hooks. */ -export type Outcome = typeof ExecuteAfterOutcome.Type extends infer A - ? A extends { readonly status: string } - ? HookOutcome - : never - : never - -/** - * The canonical execution outcome as seen by `execute.after` hooks. Hooks - * observe bounded model content, optional metadata, and managed output paths; - * they never observe the raw domain output. - */ -export type ToolExecuteAfterEvent = ToolHookBase & Outcome - -export interface ToolDraft { - add(name: string, tool: Any, options?: RegisterOptions): void -} - -export interface ToolHooks { - readonly "execute.before": ToolExecuteBeforeEvent - readonly "execute.after": ToolExecuteAfterEvent -} - -export interface ToolDomain { - readonly transform: Transform - readonly hook: Hooks -} diff --git a/packages/plugin/src/v2/effect/location.ts b/packages/plugin/src/v2/effect/location.ts deleted file mode 100644 index bc546a3b175d..000000000000 --- a/packages/plugin/src/v2/effect/location.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface Location { - readonly directory: string - readonly project: { - readonly directory: string - } -} diff --git a/packages/plugin/src/v2/effect/npm.ts b/packages/plugin/src/v2/effect/npm.ts deleted file mode 100644 index 4cb96c32d172..000000000000 --- a/packages/plugin/src/v2/effect/npm.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Effect } from "effect" - -export interface Npm { - add(pkg: string): Effect.Effect< - { - readonly directory: string - readonly entrypoint?: string - }, - unknown - > -} diff --git a/packages/plugin/src/v2/effect/path.ts b/packages/plugin/src/v2/effect/path.ts deleted file mode 100644 index f9045cc32d0f..000000000000 --- a/packages/plugin/src/v2/effect/path.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface Path { - readonly home: string - readonly data: string - readonly cache: string - readonly config: string - readonly state: string - readonly temp: string -} diff --git a/packages/plugin/src/v2/effect/tool.ts b/packages/plugin/src/v2/effect/tool.ts deleted file mode 100644 index cbc634f8e5b3..000000000000 --- a/packages/plugin/src/v2/effect/tool.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * as Tool from "./internal/tool.js" -export * from "./internal/tool.js" diff --git a/packages/plugin/src/v2/promise/agent.ts b/packages/plugin/src/v2/promise/agent.ts deleted file mode 100644 index af5a1f68505b..000000000000 --- a/packages/plugin/src/v2/promise/agent.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { AgentApi } from "@opencode-ai/client/promise/api" -import type { AgentDraft } from "../effect/agent.js" -import type { Transform } from "./registration.js" - -export type { AgentDraft } - -export interface AgentDomain extends AgentApi { - readonly get: (id: string) => Promise - readonly transform: Transform - readonly reload: () => Promise -} - -type AgentGetOutput = Awaited>["data"][number] diff --git a/packages/plugin/src/v2/promise/catalog.ts b/packages/plugin/src/v2/promise/catalog.ts deleted file mode 100644 index 26cc25caa3b3..000000000000 --- a/packages/plugin/src/v2/promise/catalog.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { CatalogApi } from "@opencode-ai/client/promise/api" -import type { CatalogDraft, CatalogProviderRecord } from "../effect/catalog.js" -import type { Transform } from "./registration.js" - -export type { CatalogDraft, CatalogProviderRecord } - -export interface CatalogDomain extends CatalogApi { - readonly model: CatalogApi["model"] & { - readonly get: (providerID: string, modelID: string) => Promise - } - readonly transform: Transform - readonly reload: () => Promise -} - -type ModelGetOutput = Awaited>["data"][number] diff --git a/packages/plugin/src/v2/promise/command.ts b/packages/plugin/src/v2/promise/command.ts deleted file mode 100644 index 2c675e285613..000000000000 --- a/packages/plugin/src/v2/promise/command.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { CommandApi } from "@opencode-ai/client/promise/api" -import type { CommandDraft } from "../effect/command.js" -import type { Transform } from "./registration.js" - -export type { CommandDraft } - -export interface CommandDomain extends CommandApi { - readonly transform: Transform - readonly reload: () => Promise -} diff --git a/packages/plugin/src/v2/promise/integration.ts b/packages/plugin/src/v2/promise/integration.ts deleted file mode 100644 index 6b121afc8bc1..000000000000 --- a/packages/plugin/src/v2/promise/integration.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { IntegrationApi } from "@opencode-ai/client/promise/api" -import type { IntegrationDraft, IntegrationMethodRegistration } from "../effect/integration.js" -import type { - CredentialOAuth, - CredentialValue, - IntegrationEnvMethod, - IntegrationInputs, - IntegrationKeyMethod, - IntegrationOAuthMethod, -} from "@opencode-ai/sdk/v2/types" -import type { Transform } from "./registration.js" - -export type { IntegrationDraft, IntegrationMethodRegistration } - -export type IntegrationOAuthAuthorization = { - readonly url: string - readonly instructions: string - readonly expiresAt?: number -} & ( - | { - readonly mode: "auto" - readonly callback: Promise - } - | { - readonly mode: "code" - readonly callback: (code: string) => Promise - } -) - -export interface IntegrationDomain extends Omit { - readonly transform: Transform - readonly reload: () => Promise - readonly connection: { - readonly active: (integrationID: string) => Promise - readonly resolve: ( - connection: import("@opencode-ai/sdk/v2/types").ConnectionInfo, - ) => Promise - } -} diff --git a/packages/plugin/src/v2/promise/internal/tool.ts b/packages/plugin/src/v2/promise/internal/tool.ts deleted file mode 100644 index 3d5def019baa..000000000000 --- a/packages/plugin/src/v2/promise/internal/tool.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { Hooks, Transform } from "../registration.js" - -export type Context = Omit & { - readonly progress: (update: import("../../effect/internal/tool.js").Progress) => Promise -} -export type SchemaType = import("../../effect/internal/tool.js").SchemaType -export type Content = import("../../effect/internal/tool.js").Content -export type Metadata = import("../../effect/internal/tool.js").Metadata -export type ModelOutput = import("../../effect/internal/tool.js").ModelOutput - -export type Tool, Output extends SchemaType | undefined = undefined> = Omit< - import("../../effect/internal/tool.js").Tool, - "execute" -> & { - readonly execute: ( - input: import("../../effect/internal/tool.js").InputValue, - context: Context, - ) => Promise< - Output extends SchemaType - ? import("../../effect/internal/tool.js").Response - : import("../../effect/internal/tool.js").ContentResponse - > -} - -export type Any = Omit & { - readonly execute: ( - input: any, - context: Context, - ) => Promise< - import("../../effect/internal/tool.js").Response | import("../../effect/internal/tool.js").ContentResponse - > -} - -export function make, Output extends SchemaType>( - tool: Tool, -): Tool -export function make>(tool: Tool): Tool -export function make(tool: Any): Any -export function make(tool: Any): Any { - return tool -} - -export type ToolExecuteBeforeEvent = import("../../effect/internal/tool.js").ToolExecuteBeforeEvent -export type ToolExecuteAfterEvent = import("../../effect/internal/tool.js").ToolExecuteAfterEvent -export type RegisterOptions = import("../../effect/internal/tool.js").RegisterOptions - -export interface ToolDraft { - add, Output extends SchemaType>( - name: string, - tool: Tool, - options?: RegisterOptions, - ): void - add>(name: string, tool: Tool, options?: RegisterOptions): void -} - -export interface ToolHooks { - readonly "execute.before": ToolExecuteBeforeEvent - readonly "execute.after": ToolExecuteAfterEvent -} - -export interface ToolDomain { - readonly transform: Transform - readonly hook: Hooks -} diff --git a/packages/plugin/src/v2/promise/reference.ts b/packages/plugin/src/v2/promise/reference.ts deleted file mode 100644 index 05542b2baf7c..000000000000 --- a/packages/plugin/src/v2/promise/reference.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ReferenceApi } from "@opencode-ai/client/promise/api" -import type { ReferenceDraft } from "../effect/reference.js" -import type { Transform } from "./registration.js" - -export type { ReferenceDraft } - -export interface ReferenceDomain extends ReferenceApi { - readonly transform: Transform - readonly reload: () => Promise -} diff --git a/packages/plugin/src/v2/promise/tool.ts b/packages/plugin/src/v2/promise/tool.ts deleted file mode 100644 index cbc634f8e5b3..000000000000 --- a/packages/plugin/src/v2/promise/tool.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * as Tool from "./internal/tool.js" -export * from "./internal/tool.js" diff --git a/packages/plugin/test/contract-identity.test.ts b/packages/plugin/test/contract-identity.test.ts index 264fcaa2f8d4..f93d70b48df6 100644 --- a/packages/plugin/test/contract-identity.test.ts +++ b/packages/plugin/test/contract-identity.test.ts @@ -10,9 +10,9 @@ import { Reference } from "@opencode-ai/schema/reference" import { Skill } from "@opencode-ai/schema/skill" import { WebSearch } from "@opencode-ai/schema/websearch" -const Plugin = await import("../src/v2/effect/index") -const PromisePlugin = await import("../src/v2/promise/index") -const TuiPlugin = await import("../src/v2/tui/index") +const Plugin = await import("../src/effect/index") +const PromisePlugin = await import("../src/promise/index") +const TuiPlugin = await import("../src/tui/index") test.each([ ["effect", Plugin], @@ -43,7 +43,7 @@ test.each([ ]) }) -test("tui entrypoint exposes the V2 plugin definition", () => { +test("tui entrypoint exposes the plugin definition", () => { const plugin = TuiPlugin.Plugin.define({ id: "demo", setup() {} }) expect(plugin.id).toBe("demo") }) diff --git a/packages/protocol/openapi.json b/packages/protocol/openapi.json index 1c8b3dc9412d..b7cc24d70918 100644 --- a/packages/protocol/openapi.json +++ b/packages/protocol/openapi.json @@ -346,6 +346,122 @@ "summary": "List agents" } }, + "/api/agent/{agentID}": { + "get": { + "tags": [ + "agent" + ], + "operationId": "v2.agent.get", + "parameters": [ + { + "name": "agentID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Agent.Info" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "AgentNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentNotFoundError" + } + } + } + } + }, + "description": "Retrieve a single currently registered agent.", + "summary": "Get agent" + } + }, "/api/plugin": { "get": { "tags": [ @@ -4279,7 +4395,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/ProviderV2.Info" + "$ref": "#/components/schemas/Provider.Info" } } }, @@ -4396,7 +4512,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/ProviderV2.Info" + "$ref": "#/components/schemas/Provider.Info" } }, "required": [ @@ -7466,7 +7582,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionV2.Request" + "$ref": "#/components/schemas/Permission.Request" } } }, @@ -7664,7 +7780,7 @@ ] }, "effect": { - "$ref": "#/components/schemas/PermissionV2.Effect" + "$ref": "#/components/schemas/Permission.Effect" } }, "required": [ @@ -7762,7 +7878,7 @@ "type": "object" }, "source": { - "$ref": "#/components/schemas/PermissionV2.Source" + "$ref": "#/components/schemas/Permission.Source" }, "agent": { "anyOf": [ @@ -7818,7 +7934,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionV2.Request" + "$ref": "#/components/schemas/Permission.Request" } } }, @@ -7916,7 +8032,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/PermissionV2.Request" + "$ref": "#/components/schemas/Permission.Request" } }, "required": [ @@ -8061,7 +8177,7 @@ "type": "object", "properties": { "reply": { - "$ref": "#/components/schemas/PermissionV2.Reply" + "$ref": "#/components/schemas/Permission.Reply" }, "message": { "anyOf": [ @@ -10456,7 +10572,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Request" + "$ref": "#/components/schemas/Question.Request" } } }, @@ -10527,7 +10643,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Request" + "$ref": "#/components/schemas/Question.Request" } } }, @@ -10667,7 +10783,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QuestionV2.Reply" + "$ref": "#/components/schemas/Question.Reply" } } }, @@ -11981,7 +12097,7 @@ "Agent.Color": { "type": "string" }, - "PermissionV2.Effect": { + "Permission.Effect": { "type": "string", "enum": [ "allow", @@ -11989,7 +12105,7 @@ "ask" ] }, - "PermissionV2.Rule": { + "Permission.Rule": { "type": "object", "properties": { "action": { @@ -11999,7 +12115,7 @@ "type": "string" }, "effect": { - "$ref": "#/components/schemas/PermissionV2.Effect" + "$ref": "#/components/schemas/Permission.Effect" } }, "required": [ @@ -12009,10 +12125,10 @@ ], "additionalProperties": false }, - "PermissionV2.Ruleset": { + "Permission.Ruleset": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionV2.Rule" + "$ref": "#/components/schemas/Permission.Rule" } }, "Agent.Info": { @@ -12059,7 +12175,7 @@ ] }, "permissions": { - "$ref": "#/components/schemas/PermissionV2.Ruleset" + "$ref": "#/components/schemas/Permission.Ruleset" } }, "required": [ @@ -12072,6 +12188,29 @@ ], "additionalProperties": false }, + "AgentNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "AgentNotFoundError" + ] + }, + "agentID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "agentID", + "message" + ], + "additionalProperties": false + }, "Plugin.Info": { "type": "object", "properties": { @@ -18537,7 +18676,7 @@ ], "additionalProperties": false }, - "ProviderV2.Info": { + "Provider.Info": { "type": "object", "properties": { "id": { @@ -21466,7 +21605,7 @@ ], "additionalProperties": false }, - "PermissionV2.Source": { + "Permission.Source": { "anyOf": [ { "type": "object", @@ -21493,7 +21632,7 @@ } ] }, - "PermissionV2.Request": { + "Permission.Request": { "type": "object", "properties": { "id": { @@ -21531,7 +21670,7 @@ "type": "object" }, "source": { - "$ref": "#/components/schemas/PermissionV2.Source" + "$ref": "#/components/schemas/Permission.Source" } }, "required": [ @@ -21589,7 +21728,7 @@ ], "additionalProperties": false }, - "PermissionV2.Reply": { + "Permission.Reply": { "type": "string", "enum": [ "once", @@ -21935,7 +22074,7 @@ ], "additionalProperties": false }, - "PermissionAction": { + "PermissionV1.Action": { "type": "string", "enum": [ "allow", @@ -21943,7 +22082,7 @@ "ask" ] }, - "PermissionRule": { + "PermissionV1.Rule": { "type": "object", "properties": { "permission": { @@ -21953,7 +22092,7 @@ "type": "string" }, "action": { - "$ref": "#/components/schemas/PermissionAction" + "$ref": "#/components/schemas/PermissionV1.Action" } }, "required": [ @@ -21963,10 +22102,10 @@ ], "additionalProperties": false }, - "PermissionRuleset": { + "PermissionV1.Ruleset": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionRule" + "$ref": "#/components/schemas/PermissionV1.Rule" } }, "SessionV1.Info": { @@ -22155,7 +22294,7 @@ "additionalProperties": false }, "permission": { - "$ref": "#/components/schemas/PermissionRuleset" + "$ref": "#/components/schemas/PermissionV1.Ruleset" }, "revert": { "type": "object", @@ -22455,10 +22594,10 @@ ], "additionalProperties": false }, - "JSONSchema": { + "SessionV1.JSONSchema": { "type": "object" }, - "OutputFormat": { + "SessionV1.OutputFormat": { "anyOf": [ { "type": "object", @@ -22485,7 +22624,7 @@ ] }, "schema": { - "$ref": "#/components/schemas/JSONSchema" + "$ref": "#/components/schemas/SessionV1.JSONSchema" }, "retryCount": { "anyOf": [ @@ -22518,7 +22657,7 @@ } ] }, - "UserMessage": { + "SessionV1.UserMessage": { "type": "object", "properties": { "id": { @@ -22563,7 +22702,7 @@ "format": { "anyOf": [ { - "$ref": "#/components/schemas/OutputFormat" + "$ref": "#/components/schemas/SessionV1.OutputFormat" }, { "type": "null" @@ -22985,7 +23124,7 @@ ], "additionalProperties": false }, - "AssistantMessage": { + "SessionV1.AssistantMessage": { "type": "object", "properties": { "id": { @@ -23218,13 +23357,13 @@ ], "additionalProperties": false }, - "Message": { + "SessionV1.Message": { "anyOf": [ { - "$ref": "#/components/schemas/UserMessage" + "$ref": "#/components/schemas/SessionV1.UserMessage" }, { - "$ref": "#/components/schemas/AssistantMessage" + "$ref": "#/components/schemas/SessionV1.AssistantMessage" } ] }, @@ -23294,7 +23433,7 @@ ] }, "info": { - "$ref": "#/components/schemas/Message" + "$ref": "#/components/schemas/SessionV1.Message" } }, "required": [ @@ -23403,7 +23542,7 @@ ], "additionalProperties": false }, - "TextPart": { + "SessionV1.TextPart": { "type": "object", "properties": { "id": { @@ -23518,7 +23657,7 @@ ], "additionalProperties": false }, - "SubtaskPart": { + "SessionV1.SubtaskPart": { "type": "object", "properties": { "id": { @@ -23605,7 +23744,7 @@ ], "additionalProperties": false }, - "ReasoningPart": { + "SessionV1.ReasoningPart": { "type": "object", "properties": { "id": { @@ -23694,7 +23833,7 @@ ], "additionalProperties": false }, - "FilePartSourceText": { + "SessionV1.FilePartSourceText": { "type": "object", "properties": { "value": { @@ -23714,11 +23853,11 @@ ], "additionalProperties": false }, - "FileSource": { + "SessionV1.FileSource": { "type": "object", "properties": { "text": { - "$ref": "#/components/schemas/FilePartSourceText" + "$ref": "#/components/schemas/SessionV1.FilePartSourceText" }, "type": { "type": "string", @@ -23737,7 +23876,7 @@ ], "additionalProperties": false }, - "Range": { + "SessionV1.Range": { "type": "object", "properties": { "start": { @@ -23799,11 +23938,11 @@ ], "additionalProperties": false }, - "SymbolSource": { + "SessionV1.SymbolSource": { "type": "object", "properties": { "text": { - "$ref": "#/components/schemas/FilePartSourceText" + "$ref": "#/components/schemas/SessionV1.FilePartSourceText" }, "type": { "type": "string", @@ -23815,7 +23954,7 @@ "type": "string" }, "range": { - "$ref": "#/components/schemas/Range" + "$ref": "#/components/schemas/SessionV1.Range" }, "name": { "type": "string" @@ -23839,11 +23978,11 @@ ], "additionalProperties": false }, - "ResourceSource": { + "SessionV1.ResourceSource": { "type": "object", "properties": { "text": { - "$ref": "#/components/schemas/FilePartSourceText" + "$ref": "#/components/schemas/SessionV1.FilePartSourceText" }, "type": { "type": "string", @@ -23866,20 +24005,20 @@ ], "additionalProperties": false }, - "FilePartSource": { + "SessionV1.FilePartSource": { "anyOf": [ { - "$ref": "#/components/schemas/FileSource" + "$ref": "#/components/schemas/SessionV1.FileSource" }, { - "$ref": "#/components/schemas/SymbolSource" + "$ref": "#/components/schemas/SessionV1.SymbolSource" }, { - "$ref": "#/components/schemas/ResourceSource" + "$ref": "#/components/schemas/SessionV1.ResourceSource" } ] }, - "FilePart": { + "SessionV1.FilePart": { "type": "object", "properties": { "id": { @@ -23931,7 +24070,7 @@ "source": { "anyOf": [ { - "$ref": "#/components/schemas/FilePartSource" + "$ref": "#/components/schemas/SessionV1.FilePartSource" }, { "type": "null" @@ -23949,7 +24088,7 @@ ], "additionalProperties": false }, - "ToolStatePending": { + "SessionV1.ToolStatePending": { "type": "object", "properties": { "status": { @@ -23972,7 +24111,7 @@ ], "additionalProperties": false }, - "ToolStateRunning": { + "SessionV1.ToolStateRunning": { "type": "object", "properties": { "status": { @@ -24029,7 +24168,7 @@ ], "additionalProperties": false }, - "ToolStateCompleted": { + "SessionV1.ToolStateCompleted": { "type": "object", "properties": { "status": { @@ -24096,7 +24235,7 @@ { "type": "array", "items": { - "$ref": "#/components/schemas/FilePart" + "$ref": "#/components/schemas/SessionV1.FilePart" } }, { @@ -24115,7 +24254,7 @@ ], "additionalProperties": false }, - "ToolStateError": { + "SessionV1.ToolStateError": { "type": "object", "properties": { "status": { @@ -24175,23 +24314,23 @@ ], "additionalProperties": false }, - "ToolState": { + "SessionV1.ToolState": { "anyOf": [ { - "$ref": "#/components/schemas/ToolStatePending" + "$ref": "#/components/schemas/SessionV1.ToolStatePending" }, { - "$ref": "#/components/schemas/ToolStateRunning" + "$ref": "#/components/schemas/SessionV1.ToolStateRunning" }, { - "$ref": "#/components/schemas/ToolStateCompleted" + "$ref": "#/components/schemas/SessionV1.ToolStateCompleted" }, { - "$ref": "#/components/schemas/ToolStateError" + "$ref": "#/components/schemas/SessionV1.ToolStateError" } ] }, - "ToolPart": { + "SessionV1.ToolPart": { "type": "object", "properties": { "id": { @@ -24231,7 +24370,7 @@ "type": "string" }, "state": { - "$ref": "#/components/schemas/ToolState" + "$ref": "#/components/schemas/SessionV1.ToolState" }, "metadata": { "anyOf": [ @@ -24255,7 +24394,7 @@ ], "additionalProperties": false }, - "StepStartPart": { + "SessionV1.StepStartPart": { "type": "object", "properties": { "id": { @@ -24307,7 +24446,7 @@ ], "additionalProperties": false }, - "StepFinishPart": { + "SessionV1.StepFinishPart": { "type": "object", "properties": { "id": { @@ -24415,7 +24554,7 @@ ], "additionalProperties": false }, - "SnapshotPart": { + "SessionV1.SnapshotPart": { "type": "object", "properties": { "id": { @@ -24461,7 +24600,7 @@ ], "additionalProperties": false }, - "PatchPart": { + "SessionV1.PatchPart": { "type": "object", "properties": { "id": { @@ -24514,7 +24653,7 @@ ], "additionalProperties": false }, - "AgentPart": { + "SessionV1.AgentPart": { "type": "object", "properties": { "id": { @@ -24597,7 +24736,7 @@ ], "additionalProperties": false }, - "RetryPart": { + "SessionV1.RetryPart": { "type": "object", "properties": { "id": { @@ -24670,7 +24809,7 @@ ], "additionalProperties": false }, - "CompactionPart": { + "SessionV1.CompactionPart": { "type": "object", "properties": { "id": { @@ -24741,43 +24880,43 @@ ], "additionalProperties": false }, - "Part": { + "SessionV1.Part": { "anyOf": [ { - "$ref": "#/components/schemas/TextPart" + "$ref": "#/components/schemas/SessionV1.TextPart" }, { - "$ref": "#/components/schemas/SubtaskPart" + "$ref": "#/components/schemas/SessionV1.SubtaskPart" }, { - "$ref": "#/components/schemas/ReasoningPart" + "$ref": "#/components/schemas/SessionV1.ReasoningPart" }, { - "$ref": "#/components/schemas/FilePart" + "$ref": "#/components/schemas/SessionV1.FilePart" }, { - "$ref": "#/components/schemas/ToolPart" + "$ref": "#/components/schemas/SessionV1.ToolPart" }, { - "$ref": "#/components/schemas/StepStartPart" + "$ref": "#/components/schemas/SessionV1.StepStartPart" }, { - "$ref": "#/components/schemas/StepFinishPart" + "$ref": "#/components/schemas/SessionV1.StepFinishPart" }, { - "$ref": "#/components/schemas/SnapshotPart" + "$ref": "#/components/schemas/SessionV1.SnapshotPart" }, { - "$ref": "#/components/schemas/PatchPart" + "$ref": "#/components/schemas/SessionV1.PatchPart" }, { - "$ref": "#/components/schemas/AgentPart" + "$ref": "#/components/schemas/SessionV1.AgentPart" }, { - "$ref": "#/components/schemas/RetryPart" + "$ref": "#/components/schemas/SessionV1.RetryPart" }, { - "$ref": "#/components/schemas/CompactionPart" + "$ref": "#/components/schemas/SessionV1.CompactionPart" } ] }, @@ -24847,7 +24986,7 @@ ] }, "part": { - "$ref": "#/components/schemas/Part" + "$ref": "#/components/schemas/SessionV1.Part" }, "time": { "type": "number" @@ -25472,7 +25611,7 @@ ], "additionalProperties": false }, - "permission.v2.asked": { + "permission.asked": { "type": "object", "properties": { "id": { @@ -25492,7 +25631,7 @@ "type": { "type": "string", "enum": [ - "permission.v2.asked" + "permission.asked" ] }, "location": { @@ -25536,7 +25675,7 @@ "type": "object" }, "source": { - "$ref": "#/components/schemas/PermissionV2.Source" + "$ref": "#/components/schemas/Permission.Source" } }, "required": [ @@ -25556,7 +25695,7 @@ ], "additionalProperties": false }, - "permission.v2.replied": { + "permission.replied": { "type": "object", "properties": { "id": { @@ -25576,7 +25715,7 @@ "type": { "type": "string", "enum": [ - "permission.v2.replied" + "permission.replied" ] }, "location": { @@ -25602,7 +25741,7 @@ ] }, "reply": { - "$ref": "#/components/schemas/PermissionV2.Reply" + "$ref": "#/components/schemas/Permission.Reply" } }, "required": [ @@ -26327,7 +26466,7 @@ ], "additionalProperties": false }, - "QuestionV2.Option": { + "Question.Option": { "type": "object", "properties": { "label": { @@ -26345,7 +26484,7 @@ ], "additionalProperties": false }, - "QuestionV2.Info": { + "Question.Info": { "type": "object", "properties": { "question": { @@ -26359,7 +26498,7 @@ "options": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Option" + "$ref": "#/components/schemas/Question.Option" }, "description": "Available choices" }, @@ -26377,7 +26516,7 @@ ], "additionalProperties": false }, - "QuestionV2.Tool": { + "Question.Tool": { "type": "object", "properties": { "messageID": { @@ -26393,7 +26532,7 @@ ], "additionalProperties": false }, - "question.v2.asked": { + "question.asked": { "type": "object", "properties": { "id": { @@ -26413,7 +26552,7 @@ "type": { "type": "string", "enum": [ - "question.v2.asked" + "question.asked" ] }, "location": { @@ -26441,12 +26580,12 @@ "questions": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Info" + "$ref": "#/components/schemas/Question.Info" }, "description": "Questions to ask" }, "tool": { - "$ref": "#/components/schemas/QuestionV2.Tool" + "$ref": "#/components/schemas/Question.Tool" } }, "required": [ @@ -26465,13 +26604,13 @@ ], "additionalProperties": false }, - "QuestionV2.Answer": { + "Question.Answer": { "type": "array", "items": { "type": "string" } }, - "question.v2.replied": { + "question.replied": { "type": "object", "properties": { "id": { @@ -26491,7 +26630,7 @@ "type": { "type": "string", "enum": [ - "question.v2.replied" + "question.replied" ] }, "location": { @@ -26519,7 +26658,7 @@ "answers": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Answer" + "$ref": "#/components/schemas/Question.Answer" } } }, @@ -26539,7 +26678,7 @@ ], "additionalProperties": false }, - "question.v2.rejected": { + "question.rejected": { "type": "object", "properties": { "id": { @@ -26559,7 +26698,7 @@ "type": { "type": "string", "enum": [ - "question.v2.rejected" + "question.rejected" ] }, "location": { @@ -28068,7 +28207,7 @@ ], "additionalProperties": false }, - "permission.asked": { + "session.error": { "type": "object", "properties": { "id": { @@ -28088,7 +28227,7 @@ "type": { "type": "string", "enum": [ - "permission.asked" + "session.error" ] }, "location": { @@ -28097,57 +28236,50 @@ "data": { "type": "object", "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^per" - } - ] - }, "sessionID": { - "type": "string", - "allOf": [ + "anyOf": [ { - "pattern": "^ses" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + { + "type": "null" } ] }, - "permission": { - "type": "string" - }, - "patterns": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "always": { - "type": "array", - "items": { - "type": "string" - } - }, - "tool": { + "error": { "anyOf": [ { - "type": "object", - "properties": { - "messageID": { - "type": "string" + "anyOf": [ + { + "$ref": "#/components/schemas/ProviderAuthError" }, - "callID": { - "type": "string" + { + "$ref": "#/components/schemas/UnknownError1" + }, + { + "$ref": "#/components/schemas/MessageOutputLengthError" + }, + { + "$ref": "#/components/schemas/MessageAbortedError" + }, + { + "$ref": "#/components/schemas/StructuredOutputError" + }, + { + "$ref": "#/components/schemas/ContextOverflowError" + }, + { + "$ref": "#/components/schemas/ContentFilterError" + }, + { + "$ref": "#/components/schemas/APIError" } - }, - "required": [ - "messageID", - "callID" - ], - "additionalProperties": false + ] }, { "type": "null" @@ -28155,14 +28287,6 @@ ] } }, - "required": [ - "id", - "sessionID", - "permission", - "patterns", - "metadata", - "always" - ], "additionalProperties": false } }, @@ -28174,7 +28298,7 @@ ], "additionalProperties": false }, - "permission.replied": { + "V2Event.server.connected": { "type": "object", "properties": { "id": { @@ -28185,498 +28309,36 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { - "type": "object" + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "location": { + "anyOf": [ + { + "$ref": "#/components/schemas/Location.Ref" + }, + { + "type": "null" + } + ] }, "type": { "type": "string", "enum": [ - "permission.replied" + "server.connected" ] }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "requestID": { - "type": "string", - "allOf": [ - { - "pattern": "^per" - } - ] - }, - "reply": { - "type": "string", - "enum": [ - "once", - "always", - "reject" - ] - } - }, - "required": [ - "sessionID", - "requestID", - "reply" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "QuestionOption": { - "type": "object", - "properties": { - "label": { - "type": "string", - "description": "Display text (1-5 words, concise)" - }, - "description": { - "type": "string", - "description": "Explanation of choice" - } - }, - "required": [ - "label", - "description" - ], - "additionalProperties": false - }, - "QuestionInfo": { - "type": "object", - "properties": { - "question": { - "type": "string", - "description": "Complete question" - }, - "header": { - "type": "string", - "description": "Very short label (max 30 chars)" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionOption" - }, - "description": "Available choices" - }, - "multiple": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "description": "Allow selecting multiple choices" - }, - "custom": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "description": "Allow typing a custom answer (default: true)" - } - }, - "required": [ - "question", - "header", - "options" - ], - "additionalProperties": false - }, - "QuestionTool": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg" - } - ] - }, - "callID": { - "type": "string" - } - }, - "required": [ - "messageID", - "callID" - ], - "additionalProperties": false - }, - "question.asked": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "question.asked" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^que" - } - ] - }, - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionInfo" - }, - "description": "Questions to ask" - }, - "tool": { - "anyOf": [ - { - "$ref": "#/components/schemas/QuestionTool" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "id", - "sessionID", - "questions" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "QuestionAnswer": { - "type": "array", - "items": { - "type": "string" - } - }, - "question.replied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "question.replied" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "requestID": { - "type": "string", - "allOf": [ - { - "pattern": "^que" - } - ] - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } - } - }, - "required": [ - "sessionID", - "requestID", - "answers" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "question.rejected": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "question.rejected" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "requestID": { - "type": "string", - "allOf": [ - { - "pattern": "^que" - } - ] - } - }, - "required": [ - "sessionID", - "requestID" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "session.error": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "session.error" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "anyOf": [ - { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - { - "type": "null" - } - ] - }, - "error": { - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/components/schemas/ProviderAuthError" - }, - { - "$ref": "#/components/schemas/UnknownError1" - }, - { - "$ref": "#/components/schemas/MessageOutputLengthError" - }, - { - "$ref": "#/components/schemas/MessageAbortedError" - }, - { - "$ref": "#/components/schemas/StructuredOutputError" - }, - { - "$ref": "#/components/schemas/ContextOverflowError" - }, - { - "$ref": "#/components/schemas/ContentFilterError" - }, - { - "$ref": "#/components/schemas/APIError" - } - ] - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "V2Event.server.connected": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "metadata": { - "anyOf": [ - { - "type": "object" - }, - { - "type": "null" - } - ] - }, - "location": { - "anyOf": [ - { - "$ref": "#/components/schemas/Location.Ref" - }, - { - "type": "null" - } - ] - }, - "type": { - "type": "string", - "enum": [ - "server.connected" - ] - }, - "data": { - "anyOf": [ - { - "type": "object" + "anyOf": [ + { + "type": "object" }, { "type": "array" @@ -28865,10 +28527,10 @@ "$ref": "#/components/schemas/reference.updated" }, { - "$ref": "#/components/schemas/permission.v2.asked" + "$ref": "#/components/schemas/permission.asked" }, { - "$ref": "#/components/schemas/permission.v2.replied" + "$ref": "#/components/schemas/permission.replied" }, { "$ref": "#/components/schemas/plugin.added" @@ -28910,13 +28572,13 @@ "$ref": "#/components/schemas/shell.deleted" }, { - "$ref": "#/components/schemas/question.v2.asked" + "$ref": "#/components/schemas/question.asked" }, { - "$ref": "#/components/schemas/question.v2.replied" + "$ref": "#/components/schemas/question.replied" }, { - "$ref": "#/components/schemas/question.v2.rejected" + "$ref": "#/components/schemas/question.rejected" }, { "$ref": "#/components/schemas/form.created" @@ -28963,21 +28625,6 @@ { "$ref": "#/components/schemas/mcp.resources.changed" }, - { - "$ref": "#/components/schemas/permission.asked" - }, - { - "$ref": "#/components/schemas/permission.replied" - }, - { - "$ref": "#/components/schemas/question.asked" - }, - { - "$ref": "#/components/schemas/question.replied" - }, - { - "$ref": "#/components/schemas/question.rejected" - }, { "$ref": "#/components/schemas/session.error" }, @@ -29153,7 +28800,7 @@ ], "additionalProperties": false }, - "QuestionV2.Request": { + "Question.Request": { "type": "object", "properties": { "id": { @@ -29175,12 +28822,12 @@ "questions": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Info" + "$ref": "#/components/schemas/Question.Info" }, "description": "Questions to ask" }, "tool": { - "$ref": "#/components/schemas/QuestionV2.Tool" + "$ref": "#/components/schemas/Question.Tool" } }, "required": [ @@ -29190,13 +28837,13 @@ ], "additionalProperties": false }, - "QuestionV2.Reply": { + "Question.Reply": { "type": "object", "properties": { "answers": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Answer" + "$ref": "#/components/schemas/Question.Answer" }, "description": "User answers in order of questions (each answer is an array of selected labels)" } diff --git a/packages/protocol/src/errors.ts b/packages/protocol/src/errors.ts index a678fb734619..7b7cde045b6f 100644 --- a/packages/protocol/src/errors.ts +++ b/packages/protocol/src/errors.ts @@ -62,6 +62,15 @@ export class ProviderNotFoundError extends Schema.TaggedErrorClass()( + "AgentNotFoundError", + { + agentID: Schema.String, + message: Schema.String, + }, + { httpApiStatus: 404 }, +) {} + export class SessionNotFoundError extends Schema.TaggedErrorClass()( "SessionNotFoundError", { diff --git a/packages/protocol/src/groups/agent.ts b/packages/protocol/src/groups/agent.ts index cfae58533a37..cdfaaf5a72f9 100644 --- a/packages/protocol/src/groups/agent.ts +++ b/packages/protocol/src/groups/agent.ts @@ -3,6 +3,7 @@ import { Location } from "@opencode-ai/schema/location" import { Schema } from "effect" import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { LocationQuery, locationQueryOpenApi } from "./location.js" +import { AgentNotFoundError } from "../errors.js" export const AgentGroup = HttpApiGroup.make("server.agent") .add( @@ -19,4 +20,20 @@ export const AgentGroup = HttpApiGroup.make("server.agent") }), ), ) + .add( + HttpApiEndpoint.get("agent.get", "/api/agent/:agentID", { + params: { agentID: Agent.ID }, + query: LocationQuery, + success: Location.response(Agent.Info), + error: AgentNotFoundError, + }) + .annotateMerge(locationQueryOpenApi) + .annotateMerge( + OpenApi.annotations({ + identifier: "v2.agent.get", + summary: "Get agent", + description: "Retrieve a single currently registered agent.", + }), + ), + ) .annotateMerge(OpenApi.annotations({ title: "agent" })) diff --git a/packages/schema/package.json b/packages/schema/package.json index 445991c4e2b5..04551ef505f7 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -24,6 +24,7 @@ "typecheck": "tsgo --noEmit" }, "dependencies": { + "@standard-schema/spec": "catalog:", "effect": "catalog:" }, "devDependencies": { diff --git a/packages/schema/src/event-manifest.ts b/packages/schema/src/event-manifest.ts index a9c9aaa52e92..084c41dd7ae0 100644 --- a/packages/schema/src/event-manifest.ts +++ b/packages/schema/src/event-manifest.ts @@ -12,18 +12,16 @@ import { FileSystemV1 } from "./filesystem-v1.js" import { Form } from "./form.js" import { InstallationEvent } from "./installation-event.js" import { Integration } from "./integration.js" -import { LegacyEvent } from "./legacy-event.js" +import { LegacyEventV1 } from "./legacy-event.js" import { LspEvent } from "./lsp-event.js" import { McpEvent } from "./mcp-event.js" import { ModelsDev } from "./models-dev.js" import { Permission } from "./permission.js" -import { PermissionV1 } from "./permission-v1.js" import { Plugin } from "./plugin.js" import { Project } from "./project.js" import { ProjectDirectories } from "./project-directories.js" import { Pty } from "./pty.js" import { Question } from "./question.js" -import { QuestionV1 } from "./question-v1.js" import { Reference } from "./reference.js" import { ServerEvent } from "./server-event.js" import { Shell } from "./shell.js" @@ -81,11 +79,7 @@ export const ServerDefinitions = Event.inventory( ...VcsEvent.Definitions, McpEvent.StatusChanged, McpEvent.ResourcesChanged, - // Shared transitional: V1 contracts the current TUI still consumes during - // the migration (permission.asked/replied, question.asked, session.error). - // Remove when the TUI moves to the current permission/question surfaces. - ...PermissionV1.Event.Definitions, - ...QuestionV1.Event.Definitions, + // Shared transitional event retained until the TUI moves to the current session error surface. SessionV1.Error, ) export const Server = Event.latest(ServerDefinitions) @@ -98,14 +92,12 @@ export const Definitions = Event.inventory( ...InstallationEvent.Definitions, ...featureDefinitions, ...LspEvent.Definitions, - ...PermissionV1.Event.Definitions, ...TuiEvent.Definitions, ...McpEvent.Definitions, - ...LegacyEvent.Definitions, + ...LegacyEventV1.Definitions, ...FileSystemV1.Event.Definitions, ...Project.Event.Definitions, ...SessionStatusEvent.Definitions, - ...QuestionV1.Event.Definitions, ...SessionCompactionEvent.Definitions, ...VcsEvent.Definitions, ...WorkspaceEvent.Definitions, diff --git a/packages/schema/src/llm.ts b/packages/schema/src/llm.ts index 2bc1d93bd8fb..3d4e15143210 100644 --- a/packages/schema/src/llm.ts +++ b/packages/schema/src/llm.ts @@ -10,22 +10,3 @@ export type ProviderMetadata = Schema.Schema.Type export const FinishReason = Schema.Literals(["stop", "length", "tool-calls", "content-filter", "error", "unknown"]) export type FinishReason = typeof FinishReason.Type - -export interface ToolTextContent extends Schema.Schema.Type {} -export const ToolTextContent = Schema.Struct({ - type: Schema.Literal("text"), - text: Schema.String, -}).annotate({ identifier: "Tool.TextContent" }) - -export interface ToolFileContent extends Schema.Schema.Type {} -export const ToolFileContent = Schema.Struct({ - type: Schema.Literal("file"), - uri: Schema.String, - mime: Schema.String, - name: optional(Schema.String), -}).annotate({ identifier: "Tool.FileContent" }) - -export const ToolContent = Schema.Union([ToolTextContent, ToolFileContent]) - .pipe(Schema.toTaggedUnion("type")) - .annotate({ identifier: "LLM.ToolContent" }) -export type ToolContent = Schema.Schema.Type diff --git a/packages/schema/src/permission.ts b/packages/schema/src/permission.ts index ca79081eda2d..e7e7fc7baeb7 100644 --- a/packages/schema/src/permission.ts +++ b/packages/schema/src/permission.ts @@ -8,7 +8,7 @@ import { SessionID } from "./session-id.js" import { statics } from "./schema.js" export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe( - Schema.brand("PermissionV2.ID"), + Schema.brand("Permission.ID"), statics((schema) => ({ create: (id?: string) => schema.make(id ?? "per_" + ascending()) })), ) export type ID = typeof ID.Type @@ -19,7 +19,7 @@ export const Source = Schema.Union([ messageID: Schema.String, callID: Schema.String, }), -]).annotate({ identifier: "PermissionV2.Source" }) +]).annotate({ identifier: "Permission.Source" }) export type Source = typeof Source.Type const RequestFields = { @@ -34,15 +34,15 @@ const RequestFields = { export const Request = Schema.Struct({ id: ID, ...RequestFields, -}).annotate({ identifier: "PermissionV2.Request" }) +}).annotate({ identifier: "Permission.Request" }) export interface Request extends Schema.Schema.Type {} -export const Reply = Schema.Literals(["once", "always", "reject"]).annotate({ identifier: "PermissionV2.Reply" }) +export const Reply = Schema.Literals(["once", "always", "reject"]).annotate({ identifier: "Permission.Reply" }) export type Reply = typeof Reply.Type -const Asked = ephemeral({ type: "permission.v2.asked", schema: Request.fields }) +const Asked = ephemeral({ type: "permission.asked", schema: Request.fields }) const Replied = ephemeral({ - type: "permission.v2.replied", + type: "permission.replied", schema: { sessionID: SessionID, requestID: ID, @@ -51,7 +51,7 @@ const Replied = ephemeral({ }) export const Event = { Asked, Replied, Definitions: inventory(Asked, Replied) } -export const Effect = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionV2.Effect" }) +export const Effect = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "Permission.Effect" }) export type Effect = typeof Effect.Type export interface Rule extends Schema.Schema.Type {} @@ -59,7 +59,7 @@ export const Rule = Schema.Struct({ action: Schema.String, resource: Schema.String, effect: Effect, -}).annotate({ identifier: "PermissionV2.Rule" }) +}).annotate({ identifier: "Permission.Rule" }) -export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionV2.Ruleset" }) +export const Ruleset = Schema.Array(Rule).annotate({ identifier: "Permission.Ruleset" }) export type Ruleset = typeof Ruleset.Type diff --git a/packages/schema/src/provider.ts b/packages/schema/src/provider.ts index 3e98b16990a9..69a8d0212ff3 100644 --- a/packages/schema/src/provider.ts +++ b/packages/schema/src/provider.ts @@ -5,7 +5,7 @@ import { Integration } from "./integration.js" import { optional, statics } from "./schema.js" export const ID = Schema.String.pipe( - Schema.brand("ProviderV2.ID"), + Schema.brand("Provider.ID"), statics((schema) => ({ opencode: schema.make("opencode"), anthropic: schema.make("anthropic"), @@ -26,19 +26,19 @@ export const Package = Schema.String export type Package = typeof Package.Type export const Overlays = { - settings: Schema.Record(Schema.String, Schema.Json).pipe(optional), + settings: Schema.Record(Schema.String, Schema.Any).pipe(optional), headers: Schema.Record(Schema.String, Schema.String).pipe(optional), - body: Schema.Record(Schema.String, Schema.Json).pipe(optional), + body: Schema.Record(Schema.String, Schema.Any).pipe(optional), } -export const Settings = Schema.Record(Schema.String, Schema.Json).annotate({ identifier: "Provider.Settings" }) +export const Settings = Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "Provider.Settings" }) export type Settings = typeof Settings.Type export interface Request extends Schema.Schema.Type {} export const Request = Schema.Struct({ settings: Settings.pipe(Schema.withConstructorDefault(Effect.succeed({}))), headers: Schema.Record(Schema.String, Schema.String), - body: Schema.Record(Schema.String, Schema.Json), + body: Schema.Record(Schema.String, Schema.Any), }).annotate({ identifier: "Provider.Request" }) export interface Info extends Schema.Schema.Type {} @@ -50,7 +50,7 @@ export const Info = Schema.Struct({ package: Package, ...Overlays, }) - .annotate({ identifier: "ProviderV2.Info" }) + .annotate({ identifier: "Provider.Info" }) .pipe( statics(() => ({ empty: (id: ID): Info => ({ id, name: id, package: "" }), diff --git a/packages/schema/src/question.ts b/packages/schema/src/question.ts index 617ad2a89503..d427cd1eaecd 100644 --- a/packages/schema/src/question.ts +++ b/packages/schema/src/question.ts @@ -8,7 +8,7 @@ import { SessionID } from "./session-id.js" import { statics } from "./schema.js" export const ID = Schema.String.check(Schema.isStartsWith("que")).pipe( - Schema.brand("QuestionV2.ID"), + Schema.brand("Question.ID"), statics((schema) => { const create = () => schema.make("que_" + ascending()) return { @@ -22,7 +22,7 @@ export type ID = typeof ID.Type 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" }) +}).annotate({ identifier: "Question.Option" }) export interface Option extends Schema.Schema.Type {} const base = { @@ -37,16 +37,16 @@ export const Info = Schema.Struct({ custom: Schema.Boolean.pipe(optional).annotate({ description: "Allow typing a custom answer (default: true)", }), -}).annotate({ identifier: "QuestionV2.Info" }) +}).annotate({ identifier: "Question.Info" }) export interface Info extends Schema.Schema.Type {} -export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionV2.Prompt" }) +export const Prompt = Schema.Struct(base).annotate({ identifier: "Question.Prompt" }) export interface Prompt extends Schema.Schema.Type {} export const Tool = Schema.Struct({ messageID: Schema.String, callID: Schema.String, -}).annotate({ identifier: "QuestionV2.Tool" }) +}).annotate({ identifier: "Question.Tool" }) export interface Tool extends Schema.Schema.Type {} export const Request = Schema.Struct({ @@ -54,22 +54,22 @@ export const Request = Schema.Struct({ sessionID: SessionID, questions: Schema.Array(Info).annotate({ description: "Questions to ask" }), tool: Tool.pipe(optional), -}).annotate({ identifier: "QuestionV2.Request" }) +}).annotate({ identifier: "Question.Request" }) export interface Request extends Schema.Schema.Type {} -export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionV2.Answer" }) +export const Answer = Schema.Array(Schema.String).annotate({ identifier: "Question.Answer" }) export type Answer = typeof Answer.Type 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" }) +}).annotate({ identifier: "Question.Reply" }) export interface Reply extends Schema.Schema.Type {} -const Asked = ephemeral({ type: "question.v2.asked", schema: Request.fields }) +const Asked = ephemeral({ type: "question.asked", schema: Request.fields }) const Replied = ephemeral({ - type: "question.v2.replied", + type: "question.replied", schema: { sessionID: SessionID, requestID: ID, @@ -77,7 +77,7 @@ const Replied = ephemeral({ }, }) const Rejected = ephemeral({ - type: "question.v2.rejected", + type: "question.rejected", schema: { sessionID: SessionID, requestID: ID, diff --git a/packages/schema/src/session-event.ts b/packages/schema/src/session-event.ts index 3a31643bbbf7..6a9258a549b8 100644 --- a/packages/schema/src/session-event.ts +++ b/packages/schema/src/session-event.ts @@ -3,7 +3,8 @@ export * as SessionEvent from "./session-event.js" import { Schema } from "effect" import { optional } from "./schema.js" import { Event } from "./event.js" -import { FinishReason, ToolContent } from "./llm.js" +import { FinishReason } from "./llm.js" +import { Content } from "./tool.js" import { Model } from "./model.js" import { NonNegativeInt, PositiveInt, RelativePath } from "./schema.js" import { FileAttachment } from "./prompt.js" @@ -429,7 +430,7 @@ export namespace Tool { }, schema: { ...ToolBase, - content: Schema.NonEmptyArray(ToolContent), + content: Schema.NonEmptyArray(Content), metadata: Schema.Record(Schema.String, Schema.Json).pipe(optional), executed: Schema.Boolean, resultState: SessionMessage.ProviderState.pipe(optional), @@ -451,7 +452,7 @@ export namespace Tool { schema: { ...ToolBase, error: SessionError.Error, - content: Schema.NonEmptyArray(ToolContent).pipe(optional), + content: Schema.NonEmptyArray(Content).pipe(optional), metadata: Schema.Record(Schema.String, Schema.Json).pipe(optional), executed: Schema.Boolean, resultState: SessionMessage.ProviderState.pipe(optional), diff --git a/packages/schema/src/session-message.ts b/packages/schema/src/session-message.ts index 4e0858e40554..c6b8fa4acd1b 100644 --- a/packages/schema/src/session-message.ts +++ b/packages/schema/src/session-message.ts @@ -2,7 +2,7 @@ export * as SessionMessage from "./session-message.js" import { Schema } from "effect" import { optional } from "./schema.js" -import { ToolContent } from "./llm.js" +import { Content } from "./tool.js" import { Model } from "./model.js" import { Prompt } from "./prompt.js" import { DateTimeUtcFromMillis, PositiveInt, RelativePath, statics } from "./schema.js" @@ -117,7 +117,7 @@ export interface ToolStateCompleted extends Schema.Schema.Type> + +export const CallID = Schema.String.pipe(Schema.brand("Tool.CallID")) +export type CallID = typeof CallID.Type + +export interface Context { + readonly sessionID: Session.ID + readonly agent: Agent.ID + readonly messageID: SessionMessage.ID + readonly callID: CallID + readonly progress: (update: Metadata) => Effect.Effect +} + +export interface Options { + readonly namespace?: string + readonly codemode?: boolean + readonly permission?: string +} + +export type ValueSchema = + | Schema.Codec + | (StandardSchemaV1 & StandardJSONSchemaV1) + | JsonSchema.JsonSchema + +type InputValue = 0 extends 1 & S + ? any + : S extends Schema.Codec + ? A + : S extends StandardSchemaV1 + ? A + : unknown +type OutputValue = S extends undefined + ? never + : S extends Schema.Codec + ? A + : S extends StandardSchemaV1 + ? A + : any + +export class Error extends Schema.TaggedErrorClass()("Tool.Error", { + message: Schema.String, + error: Schema.optional(Schema.Defect()), + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), +}) {} + +export interface TextContent extends Schema.Schema.Type {} +export const TextContent = Schema.Struct({ + type: Schema.Literal("text"), + text: Schema.String, +}).annotate({ identifier: "Tool.TextContent" }) + +export interface FileContent extends Schema.Schema.Type {} +export const FileContent = Schema.Struct({ + type: Schema.Literal("file"), + uri: Schema.String, + mime: Schema.String, + name: Schema.optional(Schema.String), +}).annotate({ identifier: "Tool.FileContent" }) + +export const Content = Schema.Union([TextContent, FileContent]) + .pipe(Schema.toTaggedUnion("type")) + .annotate({ identifier: "Tool.Content" }) +export type Content = Schema.Schema.Type + +export interface Result | undefined = ValueSchema | undefined> { + readonly output?: OutputValue + readonly content?: string | ReadonlyArray + readonly metadata?: Metadata +} + +export type Info< + Input extends ValueSchema = ValueSchema, + Output extends ValueSchema | undefined = ValueSchema | undefined, +> = { + readonly name: string + readonly input: Input + readonly description: string + readonly execute: (input: InputValue, context: Context) => Effect.Effect, Error> + readonly output?: Output + readonly options?: Options +} diff --git a/packages/schema/src/v1/legacy-event.ts b/packages/schema/src/v1/legacy-event.ts index ac22fff5e394..8c36a6967523 100644 --- a/packages/schema/src/v1/legacy-event.ts +++ b/packages/schema/src/v1/legacy-event.ts @@ -1,4 +1,4 @@ -export * as LegacyEvent from "./legacy-event.js" +export * as LegacyEventV1 from "./legacy-event.js" import { Schema } from "effect" import { ephemeral, inventory } from "../event.js" diff --git a/packages/schema/src/v1/permission.ts b/packages/schema/src/v1/permission.ts index 1faa46a5083a..c7d2e71b3da0 100644 --- a/packages/schema/src/v1/permission.ts +++ b/packages/schema/src/v1/permission.ts @@ -8,20 +8,20 @@ import { statics } from "../schema.js" import { SessionID } from "../session-id.js" export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe( - Schema.brand("PermissionID"), + Schema.brand("PermissionV1.ID"), statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "per_" + ascending()) })), ) export type ID = typeof ID.Type -export const Action = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionAction" }) +export const Action = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionV1.Action" }) export type Action = typeof Action.Type export const Rule = Schema.Struct({ permission: Schema.String, pattern: Schema.String, action: Action }).annotate({ - identifier: "PermissionRule", + identifier: "PermissionV1.Rule", }) export type Rule = typeof Rule.Type -export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionRuleset" }) +export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionV1.Ruleset" }) export type Ruleset = typeof Ruleset.Type export const Request = Schema.Struct({ @@ -32,29 +32,29 @@ export const Request = Schema.Struct({ metadata: Schema.Record(Schema.String, Schema.Unknown), always: Schema.Array(Schema.String), tool: Schema.optional(Schema.Struct({ messageID: Schema.String, callID: Schema.String })), -}).annotate({ identifier: "PermissionRequest" }) +}).annotate({ identifier: "PermissionV1.Request" }) 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.optional(Schema.String) }).annotate({ - identifier: "PermissionReplyBody", + identifier: "PermissionV1.ReplyBody", }) export type ReplyBody = typeof ReplyBody.Type export const Approval = Schema.Struct({ projectID: Project.ID, patterns: Schema.Array(Schema.String) }).annotate({ - identifier: "PermissionApproval", + identifier: "PermissionV1.Approval", }) export type Approval = typeof Approval.Type export const AskInput = Schema.Struct({ ...Request.fields, id: Schema.optional(ID), ruleset: Ruleset }).annotate({ - identifier: "PermissionAskInput", + identifier: "PermissionV1.AskInput", }) export type AskInput = typeof AskInput.Type export const ReplyInput = Schema.Struct({ requestID: ID, ...ReplyBody.fields }).annotate({ - identifier: "PermissionReplyInput", + identifier: "PermissionV1.ReplyInput", }) export type ReplyInput = typeof ReplyInput.Type diff --git a/packages/schema/src/v1/question.ts b/packages/schema/src/v1/question.ts index da47f37ffd72..dfd6dc8ee217 100644 --- a/packages/schema/src/v1/question.ts +++ b/packages/schema/src/v1/question.ts @@ -8,14 +8,14 @@ import { SessionID } from "../session-id.js" import { SessionV1 } from "./session.js" export const ID = Schema.String.check(Schema.isStartsWith("que")).pipe( - Schema.brand("QuestionID"), + Schema.brand("QuestionV1.ID"), statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "que_" + ascending()) })), ) 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: "QuestionOption" }) +}).annotate({ identifier: "QuestionV1.Option" }) const base = { question: Schema.String.annotate({ description: "Complete question" }), @@ -27,32 +27,32 @@ const base = { export const Info = Schema.Struct({ ...base, custom: Schema.optional(Schema.Boolean).annotate({ description: "Allow typing a custom answer (default: true)" }), -}).annotate({ identifier: "QuestionInfo" }) -export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionPrompt" }) +}).annotate({ identifier: "QuestionV1.Info" }) +export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionV1.Prompt" }) export const Tool = Schema.Struct({ messageID: SessionV1.MessageID, callID: Schema.String }).annotate({ - identifier: "QuestionTool", + identifier: "QuestionV1.Tool", }) export const Request = Schema.Struct({ id: ID, sessionID: SessionID, questions: Schema.Array(Info).annotate({ description: "Questions to ask" }), tool: Schema.optional(Tool), -}).annotate({ identifier: "QuestionRequest" }) -export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionAnswer" }) +}).annotate({ identifier: "QuestionV1.Request" }) +export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionV1.Answer" }) 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: "QuestionReply" }) +}).annotate({ identifier: "QuestionV1.Reply" }) export const Replied = Schema.Struct({ sessionID: SessionID, requestID: ID, answers: Schema.Array(Answer), }).annotate({ - identifier: "QuestionReplied", + identifier: "QuestionV1.Replied", }) export const Rejected = Schema.Struct({ sessionID: SessionID, requestID: ID }).annotate({ - identifier: "QuestionRejected", + identifier: "QuestionV1.Rejected", }) const Asked = ephemeral({ type: "question.asked", schema: Request.fields }) diff --git a/packages/schema/src/v1/session.ts b/packages/schema/src/v1/session.ts index 3932cae88a2b..9da5efaf66c3 100644 --- a/packages/schema/src/v1/session.ts +++ b/packages/schema/src/v1/session.ts @@ -15,13 +15,13 @@ import { FileDiff } from "../file-diff.js" const Timestamp = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)) export const MessageID = Schema.String.check(Schema.isStartsWith("msg")).pipe( - Schema.brand("MessageID"), + Schema.brand("SessionV1.MessageID"), statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "msg_" + ascending()) })), ) export type MessageID = typeof MessageID.Type export const PartID = Schema.String.check(Schema.isStartsWith("prt")).pipe( - Schema.brand("PartID"), + Schema.brand("SessionV1.PartID"), statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "prt_" + ascending()) })), ) export type PartID = typeof PartID.Type @@ -68,13 +68,13 @@ export class OutputFormatText extends Schema.Class("OutputForm export class OutputFormatJsonSchema extends Schema.Class("OutputFormatJsonSchema")({ type: Schema.Literal("json_schema"), - schema: Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "JSONSchema" }), + schema: Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "SessionV1.JSONSchema" }), retryCount: NonNegativeInt.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(2))), }) {} export const Format = Schema.Union([OutputFormatText, OutputFormatJsonSchema]).annotate({ discriminator: "type", - identifier: "OutputFormat", + identifier: "SessionV1.OutputFormat", }) export type OutputFormat = Schema.Schema.Type @@ -88,7 +88,7 @@ export const SnapshotPart = Schema.Struct({ ...partBase, type: Schema.Literal("snapshot"), snapshot: Schema.String, -}).annotate({ identifier: "SnapshotPart" }) +}).annotate({ identifier: "SessionV1.SnapshotPart" }) export type SnapshotPart = Types.DeepMutable> export const PatchPart = Schema.Struct({ @@ -96,7 +96,7 @@ export const PatchPart = Schema.Struct({ type: Schema.Literal("patch"), hash: Schema.String, files: Schema.Array(Schema.String), -}).annotate({ identifier: "PatchPart" }) +}).annotate({ identifier: "SessionV1.PatchPart" }) export type PatchPart = Types.DeepMutable> export const TextPart = Schema.Struct({ @@ -112,7 +112,7 @@ export const TextPart = Schema.Struct({ }), ), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), -}).annotate({ identifier: "TextPart" }) +}).annotate({ identifier: "SessionV1.TextPart" }) export type TextPart = Types.DeepMutable> export const ReasoningPart = Schema.Struct({ @@ -124,7 +124,7 @@ export const ReasoningPart = Schema.Struct({ start: NonNegativeInt, end: Schema.optional(NonNegativeInt), }), -}).annotate({ identifier: "ReasoningPart" }) +}).annotate({ identifier: "SessionV1.ReasoningPart" }) export type ReasoningPart = Types.DeepMutable> const filePartSourceBase = { @@ -132,20 +132,20 @@ const filePartSourceBase = { value: Schema.String, start: Schema.Finite, end: Schema.Finite, - }).annotate({ identifier: "FilePartSourceText" }), + }).annotate({ identifier: "SessionV1.FilePartSourceText" }), } export const Range = Schema.Struct({ start: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }), end: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }), -}).annotate({ identifier: "Range" }) +}).annotate({ identifier: "SessionV1.Range" }) export type Range = typeof Range.Type export const FileSource = Schema.Struct({ ...filePartSourceBase, type: Schema.Literal("file"), path: Schema.String, -}).annotate({ identifier: "FileSource" }) +}).annotate({ identifier: "SessionV1.FileSource" }) export const SymbolSource = Schema.Struct({ ...filePartSourceBase, @@ -154,18 +154,18 @@ export const SymbolSource = Schema.Struct({ range: Range, name: Schema.String, kind: NonNegativeInt, -}).annotate({ identifier: "SymbolSource" }) +}).annotate({ identifier: "SessionV1.SymbolSource" }) export const ResourceSource = Schema.Struct({ ...filePartSourceBase, type: Schema.Literal("resource"), clientName: Schema.String, uri: Schema.String, -}).annotate({ identifier: "ResourceSource" }) +}).annotate({ identifier: "SessionV1.ResourceSource" }) export const FilePartSource = Schema.Union([FileSource, SymbolSource, ResourceSource]).annotate({ discriminator: "type", - identifier: "FilePartSource", + identifier: "SessionV1.FilePartSource", }) export const FilePart = Schema.Struct({ @@ -175,7 +175,7 @@ export const FilePart = Schema.Struct({ filename: Schema.optional(Schema.String), url: Schema.String, source: Schema.optional(FilePartSource), -}).annotate({ identifier: "FilePart" }) +}).annotate({ identifier: "SessionV1.FilePart" }) export type FilePart = Types.DeepMutable> export const AgentPart = Schema.Struct({ @@ -189,7 +189,7 @@ export const AgentPart = Schema.Struct({ end: NonNegativeInt, }), ), -}).annotate({ identifier: "AgentPart" }) +}).annotate({ identifier: "SessionV1.AgentPart" }) export type AgentPart = Types.DeepMutable> export const CompactionPart = Schema.Struct({ @@ -198,7 +198,7 @@ export const CompactionPart = Schema.Struct({ auto: Schema.Boolean, overflow: Schema.optional(Schema.Boolean), tail_start_id: Schema.optional(MessageID), -}).annotate({ identifier: "CompactionPart" }) +}).annotate({ identifier: "SessionV1.CompactionPart" }) export type CompactionPart = Types.DeepMutable> export const SubtaskPart = Schema.Struct({ @@ -214,7 +214,7 @@ export const SubtaskPart = Schema.Struct({ }), ), command: Schema.optional(Schema.String), -}).annotate({ identifier: "SubtaskPart" }) +}).annotate({ identifier: "SessionV1.SubtaskPart" }) export type SubtaskPart = Types.DeepMutable> export const RetryPart = Schema.Struct({ @@ -225,7 +225,7 @@ export const RetryPart = Schema.Struct({ time: Schema.Struct({ created: NonNegativeInt, }), -}).annotate({ identifier: "RetryPart" }) +}).annotate({ identifier: "SessionV1.RetryPart" }) export type RetryPart = Omit>, "error"> & { error: APIError } @@ -234,7 +234,7 @@ export const StepStartPart = Schema.Struct({ ...partBase, type: Schema.Literal("step-start"), snapshot: Schema.optional(Schema.String), -}).annotate({ identifier: "StepStartPart" }) +}).annotate({ identifier: "SessionV1.StepStartPart" }) export type StepStartPart = Types.DeepMutable> export const StepFinishPart = Schema.Struct({ @@ -253,14 +253,14 @@ export const StepFinishPart = Schema.Struct({ write: Schema.Finite, }), }), -}).annotate({ identifier: "StepFinishPart" }) +}).annotate({ identifier: "SessionV1.StepFinishPart" }) export type StepFinishPart = Types.DeepMutable> export const ToolStatePending = Schema.Struct({ status: Schema.Literal("pending"), input: Schema.Record(Schema.String, Schema.Any), raw: Schema.String, -}).annotate({ identifier: "ToolStatePending" }) +}).annotate({ identifier: "SessionV1.ToolStatePending" }) export type ToolStatePending = Types.DeepMutable> export const ToolStateRunning = Schema.Struct({ @@ -271,7 +271,7 @@ export const ToolStateRunning = Schema.Struct({ time: Schema.Struct({ start: NonNegativeInt, }), -}).annotate({ identifier: "ToolStateRunning" }) +}).annotate({ identifier: "SessionV1.ToolStateRunning" }) export type ToolStateRunning = Types.DeepMutable> export const ToolStateCompleted = Schema.Struct({ @@ -286,7 +286,7 @@ export const ToolStateCompleted = Schema.Struct({ compacted: Schema.optional(NonNegativeInt), }), attachments: Schema.optional(Schema.Array(FilePart)), -}).annotate({ identifier: "ToolStateCompleted" }) +}).annotate({ identifier: "SessionV1.ToolStateCompleted" }) export type ToolStateCompleted = Types.DeepMutable> export const ToolStateError = Schema.Struct({ @@ -298,7 +298,7 @@ export const ToolStateError = Schema.Struct({ start: NonNegativeInt, end: NonNegativeInt, }), -}).annotate({ identifier: "ToolStateError" }) +}).annotate({ identifier: "SessionV1.ToolStateError" }) export type ToolStateError = Types.DeepMutable> export const ToolState = Schema.Union([ @@ -308,7 +308,7 @@ export const ToolState = Schema.Union([ ToolStateError, ]).annotate({ discriminator: "status", - identifier: "ToolState", + identifier: "SessionV1.ToolState", }) export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError @@ -319,7 +319,7 @@ export const ToolPart = Schema.Struct({ tool: Schema.String, state: ToolState, metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), -}).annotate({ identifier: "ToolPart" }) +}).annotate({ identifier: "SessionV1.ToolPart" }) export type ToolPart = Omit>, "state"> & { state: ToolState } @@ -351,7 +351,7 @@ export const User = Schema.Struct({ }), system: Schema.optional(Schema.String), tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)), -}).annotate({ identifier: "UserMessage" }) +}).annotate({ identifier: "SessionV1.UserMessage" }) export type User = Types.DeepMutable> export const Part = Schema.Union([ @@ -367,7 +367,7 @@ export const Part = Schema.Union([ AgentPart, RetryPart, CompactionPart, -]).annotate({ discriminator: "type", identifier: "Part" }) +]).annotate({ discriminator: "type", identifier: "SessionV1.Part" }) export type Part = | TextPart | SubtaskPart @@ -407,7 +407,7 @@ export const TextPartInput = Schema.Struct({ }), ), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), -}).annotate({ identifier: "TextPartInput" }) +}).annotate({ identifier: "SessionV1.TextPartInput" }) export type TextPartInput = Types.DeepMutable> export const FilePartInput = Schema.Struct({ @@ -417,7 +417,7 @@ export const FilePartInput = Schema.Struct({ filename: Schema.optional(Schema.String), url: Schema.String, source: Schema.optional(FilePartSource), -}).annotate({ identifier: "FilePartInput" }) +}).annotate({ identifier: "SessionV1.FilePartInput" }) export type FilePartInput = Types.DeepMutable> export const AgentPartInput = Schema.Struct({ @@ -431,7 +431,7 @@ export const AgentPartInput = Schema.Struct({ end: NonNegativeInt, }), ), -}).annotate({ identifier: "AgentPartInput" }) +}).annotate({ identifier: "SessionV1.AgentPartInput" }) export type AgentPartInput = Types.DeepMutable> export const SubtaskPartInput = Schema.Struct({ @@ -447,7 +447,7 @@ export const SubtaskPartInput = Schema.Struct({ }), ), command: Schema.optional(Schema.String), -}).annotate({ identifier: "SubtaskPartInput" }) +}).annotate({ identifier: "SessionV1.SubtaskPartInput" }) export type SubtaskPartInput = Types.DeepMutable> export const Assistant = Schema.Struct({ @@ -482,12 +482,12 @@ export const Assistant = Schema.Struct({ structured: Schema.optional(Schema.Any), variant: Schema.optional(Schema.String), finish: Schema.optional(Schema.String), -}).annotate({ identifier: "AssistantMessage" }) +}).annotate({ identifier: "SessionV1.AssistantMessage" }) export type Assistant = Omit>, "error"> & { error?: AssistantError } -export const Info = Schema.Union([User, Assistant]).annotate({ discriminator: "role", identifier: "Message" }) +export const Info = Schema.Union([User, Assistant]).annotate({ discriminator: "role", identifier: "SessionV1.Message" }) export type Info = User | Assistant export const WithParts = Schema.Struct({ diff --git a/packages/schema/src/workspace-id.ts b/packages/schema/src/workspace-id.ts index 913fc8e2a244..ad2ed5a71291 100644 --- a/packages/schema/src/workspace-id.ts +++ b/packages/schema/src/workspace-id.ts @@ -3,7 +3,7 @@ import { ascending } from "./identifier.js" import { statics } from "./schema.js" export const WorkspaceID = Schema.String.check(Schema.isStartsWith("wrk")).pipe( - Schema.brand("WorkspaceV2.ID"), + Schema.brand("Workspace.ID"), statics((schema) => { const create = () => schema.make("wrk_" + ascending()) return { diff --git a/packages/schema/test/contract-hygiene.test.ts b/packages/schema/test/contract-hygiene.test.ts index 46784ee27d56..f9b379e8e08f 100644 --- a/packages/schema/test/contract-hygiene.test.ts +++ b/packages/schema/test/contract-hygiene.test.ts @@ -84,14 +84,14 @@ describe("contract hygiene", () => { test("model defaults and provider overlays preserve public invariants", () => { const id = Model.ID.make("model") expect(Model.Info.default(Provider.ID.make("provider"), id)).toMatchObject({ modelID: id, variants: [] }) - expect(() => + expect( Schema.decodeUnknownSync(Provider.Info)({ id: "provider", name: "Provider", package: "native", - settings: { invalid: 1n }, - }), - ).toThrow() + settings: { arbitrary: 1n }, + }).settings, + ).toEqual({ arbitrary: 1n }) }) test("current ID constructors expose create", () => { @@ -137,15 +137,19 @@ describe("contract hygiene", () => { expect(new Set(identifiers).size).toBe(identifiers.length) }) - test("current source avoids Any and mutable contract wrappers", async () => { + test("current source limits Any to provider options and avoids mutable contract wrappers", async () => { const files = [...new Bun.Glob("*.ts").scanSync(new URL("../src", import.meta.url).pathname)].filter( (file) => !file.endsWith("-v1.ts"), ) - const source = await Promise.all( - files.map((file) => Bun.file(new URL(`../src/${file}`, import.meta.url)).text()), - ).then((values) => values.join("\n")) + const sources = await Promise.all( + files.map(async (file) => ({ file, source: await Bun.file(new URL(`../src/${file}`, import.meta.url)).text() })), + ) + const source = sources.map((item) => item.source).join("\n") - expect(source).not.toContain("Schema.Any") + expect(sources.filter((item) => item.file !== "provider.ts").map((item) => item.source).join("\n")).not.toContain( + "Schema.Any", + ) + expect(sources.find((item) => item.file === "provider.ts")?.source.match(/Schema\.Any/g)).toHaveLength(4) expect(source).not.toContain("Schema.mutable") }) diff --git a/packages/schema/test/legacy-event.test.ts b/packages/schema/test/legacy-event.test.ts index c9c8f56e49a7..b8723de3ccbc 100644 --- a/packages/schema/test/legacy-event.test.ts +++ b/packages/schema/test/legacy-event.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { LegacyEvent } from "../src/legacy-event.js" +import { LegacyEventV1 } from "../src/legacy-event.js" import { PermissionV1 } from "../src/permission-v1.js" import { QuestionV1 } from "../src/question-v1.js" import { Project } from "../src/project.js" @@ -36,7 +36,7 @@ describe("legacy public event schemas", () => { QuestionV1.Event.Replied.type, QuestionV1.Event.Rejected.type, Project.Event.Updated.type, - LegacyEvent.CommandExecuted.type, + LegacyEventV1.CommandExecuted.type, ]).toEqual([ "message.part.delta", "session.diff", diff --git a/packages/schema/test/v1-isolation.test.ts b/packages/schema/test/v1-isolation.test.ts index f605e74ce404..f38b5b76fcb7 100644 --- a/packages/schema/test/v1-isolation.test.ts +++ b/packages/schema/test/v1-isolation.test.ts @@ -1,18 +1,18 @@ import { expect, test } from "bun:test" -import { LegacyEvent } from "../src/legacy-event.js" +import { LegacyEventV1 } from "../src/legacy-event.js" import { PermissionV1 } from "../src/permission-v1.js" import { QuestionV1 } from "../src/question-v1.js" import { SessionV1 } from "../src/session-v1.js" -import { LegacyEvent as IsolatedLegacyEvent } from "../src/v1/legacy-event.js" -import { PermissionV1 as IsolatedPermissionV1 } from "../src/v1/permission.js" -import { QuestionV1 as IsolatedQuestionV1 } from "../src/v1/question.js" -import { SessionV1 as IsolatedSessionV1 } from "../src/v1/session.js" +const isolatedLegacyEvent = await import("../src/v1/legacy-event.js") +const isolatedPermission = await import("../src/v1/permission.js") +const isolatedQuestion = await import("../src/v1/question.js") +const isolatedSession = await import("../src/v1/session.js") test("compatibility entrypoints preserve isolated V1 schema identity", () => { - expect(LegacyEvent).toBe(IsolatedLegacyEvent) - expect(PermissionV1).toBe(IsolatedPermissionV1) - expect(QuestionV1).toBe(IsolatedQuestionV1) - expect(SessionV1).toBe(IsolatedSessionV1) + expect(LegacyEventV1).toBe(isolatedLegacyEvent.LegacyEventV1) + expect(PermissionV1).toBe(isolatedPermission.PermissionV1) + expect(QuestionV1).toBe(isolatedQuestion.QuestionV1) + expect(SessionV1).toBe(isolatedSession.SessionV1) }) test("current source does not import the V1 subtree directly", async () => { diff --git a/packages/sdk-next/src/tool.ts b/packages/sdk-next/src/tool.ts index 978ea6d0cbf1..af878df5fd51 100644 --- a/packages/sdk-next/src/tool.ts +++ b/packages/sdk-next/src/tool.ts @@ -1,2 +1,3 @@ -export { Failure, RegistrationError, make } from "@opencode-ai/plugin/v2/effect/tool" -export type { Any, Content, Context, Tool } from "@opencode-ai/plugin/v2/effect/tool" +export { RegistrationError } from "@opencode-ai/core/tool" +export { Error } from "@opencode-ai/schema/tool" +export type { Context, Info } from "@opencode-ai/schema/tool" diff --git a/packages/sdk-next/test/contract-identity.test.ts b/packages/sdk-next/test/contract-identity.test.ts index 0f662af27b0c..afba8f58add0 100644 --- a/packages/sdk-next/test/contract-identity.test.ts +++ b/packages/sdk-next/test/contract-identity.test.ts @@ -1,9 +1,5 @@ import { expect, test } from "bun:test" -import { AgentV2 } from "@opencode-ai/core/agent" import { Location as CoreLocation } from "@opencode-ai/core/location" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProjectV2 } from "@opencode-ai/core/project" -import { SessionV2 } from "@opencode-ai/core/session" import { SessionPending as CoreSessionPending } from "@opencode-ai/core/session/pending" import { SessionMessage as CoreSessionMessage } from "@opencode-ai/core/session/message" import { Agent } from "@opencode-ai/schema/agent" @@ -21,6 +17,10 @@ import { ClientApi, groupNames, promiseOmitEndpoints } from "@opencode-ai/protoc import { compile, emitPromise } from "@opencode-ai/httpapi-codegen" const SDK = await import("../src/index") +const CoreAgent = await import("@opencode-ai/core/agent") +const CoreModel = await import("@opencode-ai/core/model") +const CoreProject = await import("@opencode-ai/core/project") +const CoreSession = await import("@opencode-ai/core/session") test("re-exports canonical contracts directly from Schema", () => { expect(SDK.Agent).toBe(Agent) @@ -59,13 +59,13 @@ test("re-exports canonical contracts directly from Schema", () => { }) test("Core and Server reuse the authoritative Schema and Protocol values", () => { - expect(AgentV2.ID).toBe(Agent.ID) + expect(CoreAgent.ID).toBe(Agent.ID) expect(CoreLocation.Ref).toBe(Location.Ref) - expect(ModelV2.Ref).toBe(Model.Ref) - expect(SessionV2.Info).toBe(Session.Info) - expect(ProjectV2.Current).toBe(Project.Current) - expect(ProjectV2.Directory).toBe(Project.Directory) - expect(ProjectV2.Directories).toBe(Project.Directories) + expect(CoreModel.Ref).toBe(Model.Ref) + expect(CoreSession.Info).toBe(Session.Info) + expect(CoreProject.Current).toBe(Project.Current) + expect(CoreProject.Directory).toBe(Project.Directory) + expect(CoreProject.Directories).toBe(Project.Directories) expect(CoreSessionPending.Message).toBe(SessionPending.Message) expect(CoreSessionPending.User).toBe(SessionPending.User) expect(CoreSessionPending.Synthetic).toBe(SessionPending.Synthetic) diff --git a/packages/sdk-next/test/embedded.test.ts b/packages/sdk-next/test/embedded.test.ts index bff122bcad70..2f229c4efebf 100644 --- a/packages/sdk-next/test/embedded.test.ts +++ b/packages/sdk-next/test/embedded.test.ts @@ -72,8 +72,8 @@ it.live( yield* ctx.tool .transform((draft) => draft.add( - "bootstrap_sdk_tool", - fixture.sdk.Tool.make({ + ({ + name: "bootstrap_sdk_tool", description: "Marks the initial Location plugin generation", input: Schema.Struct({}), output: Schema.Void, @@ -99,8 +99,8 @@ it.live( yield* ctx.tool .transform((draft) => draft.add( - "late_sdk_tool", - fixture.sdk.Tool.make({ + ({ + name: "late_sdk_tool", description: "Tool registered after Location boot", input: Schema.Struct({}), output: Schema.Void, @@ -220,8 +220,8 @@ it.live( ctx.tool .transform((draft) => draft.add( - "embedded_tool", - fixture.sdk.Tool.make({ + ({ + name: "embedded_tool", description: "Embedded test tool", input: Schema.Struct({}), output: Schema.Struct({ ok: Schema.Boolean }), diff --git a/packages/server/src/event-feed.ts b/packages/server/src/event-feed.ts index 7b668df5b42a..d7891eadd126 100644 --- a/packages/server/src/event-feed.ts +++ b/packages/server/src/event-feed.ts @@ -1,6 +1,7 @@ export * as EventFeed from "./event-feed" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" import { isOpenCodeEvent, OpenCodeEvent } from "@opencode-ai/protocol/groups/event" import { Cause, Context, Effect, Layer, Queue, Schema, Scope, Stream } from "effect" @@ -12,7 +13,7 @@ export class SubscriberOverflowError extends Schema.TaggedErrorClass()("EventFeed.EncodingError", { - eventID: EventV2.ID, + eventID: Event.ID, eventType: Schema.String, cause: Schema.Defect(), }) {} @@ -32,7 +33,7 @@ export function frame(event: OpenCodeEvent) { } export const make = Effect.fn("EventFeed.make")(function* ( - observe: (subscriber: EventV2.Subscriber) => Effect.Effect, + observe: (subscriber: Bus.Subscriber) => Effect.Effect, options?: { readonly capacity?: number; readonly encode?: (event: OpenCodeEvent) => string }, ) { const capacity = options?.capacity ?? SubscriberCapacity @@ -46,7 +47,7 @@ export const make = Effect.fn("EventFeed.make")(function* ( for (const subscriber of current) Queue.failCauseUnsafe(subscriber, Cause.fail(error)) }) - const publish = Effect.fnUntraced(function* (event: EventV2.Payload) { + const publish = Effect.fnUntraced(function* (event: Event.Payload) { if (!isOpenCodeEvent(event)) return if (subscribers.size === 0) return const encoded = yield* Effect.try({ @@ -84,7 +85,7 @@ export const make = Effect.fn("EventFeed.make")(function* ( export const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service - return yield* make(events.listen) + const bus = yield* Bus.Service + return yield* make(bus.listen) }), ) diff --git a/packages/server/src/handlers/agent.ts b/packages/server/src/handlers/agent.ts index 8d7dcb8b3c85..d395b01bb39d 100644 --- a/packages/server/src/handlers/agent.ts +++ b/packages/server/src/handlers/agent.ts @@ -1,13 +1,27 @@ -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Effect } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" import { Api } from "../api" import { response } from "../location" +import { AgentNotFoundError } from "@opencode-ai/protocol/errors" export const AgentHandler = HttpApiBuilder.group(Api, "server.agent", (handlers) => - handlers.handle("agent.list", () => - Effect.gen(function* () { - return yield* response(AgentV2.Service.use((agent) => agent.list())) - }), - ), + handlers + .handle("agent.list", () => + Effect.gen(function* () { + return yield* response(Agent.Service.use((agent) => agent.list())) + }), + ) + .handle( + "agent.get", + Effect.fn(function* (ctx) { + const agent = yield* Agent.Service.use((service) => service.get(ctx.params.agentID)) + if (!agent) + return yield* new AgentNotFoundError({ + agentID: ctx.params.agentID, + message: `Agent not found: ${ctx.params.agentID}`, + }) + return yield* response(Effect.succeed(agent)) + }), + ), ) diff --git a/packages/server/src/handlers/command.ts b/packages/server/src/handlers/command.ts index bf41e79f835b..8e3f01b3c70f 100644 --- a/packages/server/src/handlers/command.ts +++ b/packages/server/src/handlers/command.ts @@ -1,8 +1,8 @@ -import { CommandV2 } from "@opencode-ai/core/command" +import { Command } from "@opencode-ai/core/command" import { HttpApiBuilder } from "effect/unstable/httpapi" import { Api } from "../api" import { response } from "../location" export const CommandHandler = HttpApiBuilder.group(Api, "server.command", (handlers) => - handlers.handle("command.list", () => response(CommandV2.Service.use((command) => command.list()))), + handlers.handle("command.list", () => response(Command.Service.use((command) => command.list()))), ) diff --git a/packages/server/src/handlers/event.ts b/packages/server/src/handlers/event.ts index feeba74288d6..a0d1b81cd3ba 100644 --- a/packages/server/src/handlers/event.ts +++ b/packages/server/src/handlers/event.ts @@ -1,4 +1,5 @@ -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" import { Effect, Stream } from "effect" import { HttpServerResponse } from "effect/unstable/http" import { HttpApiBuilder } from "effect/unstable/httpapi" @@ -11,7 +12,7 @@ export const EventHandler = HttpApiBuilder.group(Api, "server.event", (handlers) return handlers.handleRaw("event.subscribe", () => Effect.gen(function* () { const connected = { - id: EventV2.ID.create(), + id: Event.ID.create(), type: "server.connected", data: {}, } as const diff --git a/packages/server/src/handlers/message.ts b/packages/server/src/handlers/message.ts index 40e2ba1077f2..8bff2191b06c 100644 --- a/packages/server/src/handlers/message.ts +++ b/packages/server/src/handlers/message.ts @@ -1,5 +1,5 @@ import { SessionMessage } from "@opencode-ai/core/session/message" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { Effect, Schema } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" import { Api } from "../api" @@ -26,7 +26,7 @@ const cursor = { export const MessageHandler = HttpApiBuilder.group(Api, "server.message", (handlers) => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service return handlers.handle( "session.messages", diff --git a/packages/server/src/handlers/permission.ts b/packages/server/src/handlers/permission.ts index cbc49180f64e..53aea5b310e8 100644 --- a/packages/server/src/handlers/permission.ts +++ b/packages/server/src/handlers/permission.ts @@ -1,5 +1,5 @@ import { Location } from "@opencode-ai/core/location" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { PermissionSaved } from "@opencode-ai/core/permission/saved" import { Effect } from "effect" import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" @@ -7,7 +7,7 @@ import { Api } from "../api" import { PermissionNotFoundError, SessionNotFoundError } from "@opencode-ai/protocol/errors" import { response } from "../location" -function missingRequest(id: PermissionV2.ID) { +function missingRequest(id: Permission.ID) { return new PermissionNotFoundError({ requestID: id, message: `Permission request not found: ${id}` }) } @@ -17,14 +17,14 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission", .handle( "permission.request.list", Effect.fn(function* () { - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service return yield* response(permission.list()) }), ) .handle( "session.permission.create", Effect.fn(function* (ctx) { - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service return { data: yield* permission .ask({ @@ -53,14 +53,14 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission", .handle( "session.permission.list", Effect.fn(function* (ctx) { - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service return { data: yield* permission.forSession(ctx.params.sessionID) } }), ) .handle( "session.permission.get", Effect.fn(function* (ctx) { - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service const request = yield* permission.get(ctx.params.requestID) if (!request || request.sessionID !== ctx.params.sessionID) return yield* missingRequest(ctx.params.requestID) return { data: request } @@ -69,12 +69,12 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission", .handle( "session.permission.reply", Effect.fn(function* (ctx) { - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service const request = yield* permission.get(ctx.params.requestID) if (!request || request.sessionID !== ctx.params.sessionID) return yield* missingRequest(ctx.params.requestID) yield* permission .reply({ requestID: ctx.params.requestID, reply: ctx.payload.reply, message: ctx.payload.message }) - .pipe(Effect.catchTag("PermissionV2.NotFoundError", () => missingRequest(ctx.params.requestID))) + .pipe(Effect.catchTag("Permission.NotFoundError", () => missingRequest(ctx.params.requestID))) return HttpApiSchema.NoContent.make() }), ) diff --git a/packages/server/src/handlers/plugin.ts b/packages/server/src/handlers/plugin.ts index dd8da6a3d935..0a7f200f9efa 100644 --- a/packages/server/src/handlers/plugin.ts +++ b/packages/server/src/handlers/plugin.ts @@ -1,4 +1,4 @@ -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Plugin } from "@opencode-ai/core/plugin" import { Effect } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" import { Api } from "../api" @@ -7,7 +7,7 @@ import { response } from "../location" export const PluginHandler = HttpApiBuilder.group(Api, "server.plugin", (handlers) => handlers.handle("plugin.list", () => Effect.gen(function* () { - return yield* response(PluginV2.Service.use((plugin) => plugin.list())) + return yield* response(Plugin.Service.use((plugin) => plugin.list())) }), ), ) diff --git a/packages/server/src/handlers/question.ts b/packages/server/src/handlers/question.ts index d8011dd26976..7229de0ae58b 100644 --- a/packages/server/src/handlers/question.ts +++ b/packages/server/src/handlers/question.ts @@ -1,22 +1,22 @@ -import { QuestionV2 } from "@opencode-ai/core/question" +import { Question } from "@opencode-ai/core/question" import { Effect } from "effect" import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" import { Api } from "../api" import { QuestionNotFoundError } from "@opencode-ai/protocol/errors" import { response } from "../location" -function missingRequest(id: QuestionV2.ID) { +function missingRequest(id: Question.ID) { return new QuestionNotFoundError({ requestID: id, message: `Question request not found: ${id}` }) } export const QuestionHandler = HttpApiBuilder.group(Api, "server.question", (handlers) => Effect.gen(function* () { const withOwnedQuestion = Effect.fnUntraced(function* ( - sessionID: QuestionV2.Request["sessionID"], - requestID: QuestionV2.ID, - use: (question: QuestionV2.Interface) => Effect.Effect, + sessionID: Question.Request["sessionID"], + requestID: Question.ID, + use: (question: Question.Interface) => Effect.Effect, ) { - const question = yield* QuestionV2.Service + const question = yield* Question.Service const request = (yield* question.list()).find((request) => request.id === requestID) if (!request || request.sessionID !== sessionID) return yield* missingRequest(requestID) return yield* use(question) @@ -26,14 +26,14 @@ export const QuestionHandler = HttpApiBuilder.group(Api, "server.question", (han .handle( "question.request.list", Effect.fn(function* () { - const question = yield* QuestionV2.Service + const question = yield* Question.Service return yield* response(question.list()) }), ) .handle( "session.question.list", Effect.fn(function* (ctx) { - const question = yield* QuestionV2.Service + const question = yield* Question.Service const requests = yield* question.list() return { data: requests.filter((request) => request.sessionID === ctx.params.sessionID) } }), @@ -44,7 +44,7 @@ export const QuestionHandler = HttpApiBuilder.group(Api, "server.question", (han yield* withOwnedQuestion(ctx.params.sessionID, ctx.params.requestID, (question) => question .reply({ requestID: ctx.params.requestID, answers: ctx.payload.answers }) - .pipe(Effect.catchTag("QuestionV2.NotFoundError", () => missingRequest(ctx.params.requestID))), + .pipe(Effect.catchTag("Question.NotFoundError", () => missingRequest(ctx.params.requestID))), ) return HttpApiSchema.NoContent.make() }), @@ -55,7 +55,7 @@ export const QuestionHandler = HttpApiBuilder.group(Api, "server.question", (han yield* withOwnedQuestion(ctx.params.sessionID, ctx.params.requestID, (question) => question .reject(ctx.params.requestID) - .pipe(Effect.catchTag("QuestionV2.NotFoundError", () => missingRequest(ctx.params.requestID))), + .pipe(Effect.catchTag("Question.NotFoundError", () => missingRequest(ctx.params.requestID))), ) return HttpApiSchema.NoContent.make() }), diff --git a/packages/server/src/handlers/session.ts b/packages/server/src/handlers/session.ts index 02d1c8d80ab7..796116da01b6 100644 --- a/packages/server/src/handlers/session.ts +++ b/packages/server/src/handlers/session.ts @@ -1,4 +1,4 @@ -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { InstructionEntry } from "@opencode-ai/core/session/instruction-entry" import { DateTime, Effect, Stream } from "effect" import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" @@ -23,7 +23,7 @@ const DefaultSessionsLimit = 50 export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handlers) => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service return handlers .handle( diff --git a/packages/server/src/handlers/skill.ts b/packages/server/src/handlers/skill.ts index 8ffeaca8ea21..2b22851253dd 100644 --- a/packages/server/src/handlers/skill.ts +++ b/packages/server/src/handlers/skill.ts @@ -1,8 +1,8 @@ -import { SkillV2 } from "@opencode-ai/core/skill" +import { Skill } from "@opencode-ai/core/skill" import { HttpApiBuilder } from "effect/unstable/httpapi" import { Api } from "../api" import { response } from "../location" export const SkillHandler = HttpApiBuilder.group(Api, "server.skill", (handlers) => - handlers.handle("skill.list", () => response(SkillV2.Service.use((skill) => skill.list()))), + handlers.handle("skill.list", () => response(Skill.Service.use((skill) => skill.list()))), ) diff --git a/packages/server/src/location.ts b/packages/server/src/location.ts index 6cc90f3262e2..7b23b5e48992 100644 --- a/packages/server/src/location.ts +++ b/packages/server/src/location.ts @@ -1,7 +1,7 @@ import { Location } from "@opencode-ai/core/location" import { LocationServiceMap } from "@opencode-ai/core/location-services" import { AbsolutePath } from "@opencode-ai/core/schema" -import { WorkspaceV2 } from "@opencode-ai/core/workspace" +import { Workspace } from "@opencode-ai/core/workspace" import { Effect, Layer } from "effect" import { HttpServerRequest } from "effect/unstable/http" import { HttpApiMiddleware } from "effect/unstable/httpapi" @@ -34,7 +34,7 @@ export function requestRef(request: HttpServerRequest.HttpServerRequest): Locati (request.headers["x-opencode-directory"] ? decode(request.headers["x-opencode-directory"]) : process.cwd()) return Location.Ref.make({ directory: AbsolutePath.make(directory), - workspaceID: workspaceID ? WorkspaceV2.ID.make(workspaceID) : undefined, + workspaceID: workspaceID ? Workspace.ID.make(workspaceID) : undefined, }) } diff --git a/packages/server/src/middleware/form-location.ts b/packages/server/src/middleware/form-location.ts index f6b93b118a5e..96333dc1c641 100644 --- a/packages/server/src/middleware/form-location.ts +++ b/packages/server/src/middleware/form-location.ts @@ -2,9 +2,9 @@ import { Database } from "@opencode-ai/core/database/database" import { Location } from "@opencode-ai/core/location" import { LocationServiceMap } from "@opencode-ai/core/location-services" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionTable } from "@opencode-ai/core/session/sql" -import { WorkspaceV2 } from "@opencode-ai/core/workspace" +import { Workspace } from "@opencode-ai/core/workspace" import { InvalidRequestError, SessionNotFoundError } from "@opencode-ai/protocol/errors" import { eq } from "drizzle-orm" import { Effect, Layer, Schema } from "effect" @@ -19,7 +19,7 @@ export class FormLocationMiddleware extends HttpApiMiddleware.Service< error: [InvalidRequestError, SessionNotFoundError], }) {} -const decodeSessionID = Schema.decodeUnknownEffect(SessionV2.ID) +const decodeSessionID = Schema.decodeUnknownEffect(Session.ID) export const formLocationLayer = Layer.effect( FormLocationMiddleware, @@ -65,7 +65,7 @@ export const formLocationLayer = Layer.effect( locations.get( Location.Ref.make({ directory: AbsolutePath.make(row.directory), - workspaceID: row.workspaceID ? WorkspaceV2.ID.make(row.workspaceID) : undefined, + workspaceID: row.workspaceID ? Workspace.ID.make(row.workspaceID) : undefined, }), ), ), diff --git a/packages/server/src/middleware/session-location.ts b/packages/server/src/middleware/session-location.ts index 86fa80f75c1e..d34076e5ce47 100644 --- a/packages/server/src/middleware/session-location.ts +++ b/packages/server/src/middleware/session-location.ts @@ -2,9 +2,9 @@ import { Database } from "@opencode-ai/core/database/database" import { LocationServiceMap } from "@opencode-ai/core/location-services" import { Location } from "@opencode-ai/core/location" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionTable } from "@opencode-ai/core/session/sql" -import { WorkspaceV2 } from "@opencode-ai/core/workspace" +import { Workspace } from "@opencode-ai/core/workspace" import { eq } from "drizzle-orm" import { Effect, Layer, Schema } from "effect" import { HttpRouter } from "effect/unstable/http" @@ -19,7 +19,7 @@ export class SessionLocationMiddleware extends HttpApiMiddleware.Service< error: [InvalidRequestError, SessionNotFoundError], }) {} -const decodeSessionID = Schema.decodeUnknownEffect(SessionV2.ID) +const decodeSessionID = Schema.decodeUnknownEffect(Session.ID) export const sessionLocationLayer = Layer.effect( SessionLocationMiddleware, @@ -56,7 +56,7 @@ export const sessionLocationLayer = Layer.effect( locations.get( Location.Ref.make({ directory: AbsolutePath.make(row.directory), - workspaceID: row.workspaceID ? WorkspaceV2.ID.make(row.workspaceID) : undefined, + workspaceID: row.workspaceID ? Workspace.ID.make(row.workspaceID) : undefined, }), ), ), diff --git a/packages/server/src/routes.ts b/packages/server/src/routes.ts index edffdbd4f801..f2d0d68df738 100644 --- a/packages/server/src/routes.ts +++ b/packages/server/src/routes.ts @@ -3,18 +3,18 @@ import { App } from "@opencode-ai/core/app" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { httpClient } from "@opencode-ai/util/effect/app-node-platform" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { EventLogger } from "@opencode-ai/core/event-logger" import { FileSystemSearch } from "@opencode-ai/core/filesystem/search" import { Observability } from "@opencode-ai/util/observability" import { Credential } from "@opencode-ai/core/credential" import { Config } from "@opencode-ai/core/config" -import { CommandV2 } from "@opencode-ai/core/command" +import { Command } from "@opencode-ai/core/command" import { PermissionSaved } from "@opencode-ai/core/permission/saved" import { PtyTicket } from "@opencode-ai/core/pty/ticket" import { Pty } from "@opencode-ai/core/pty" import { Project } from "@opencode-ai/core/project" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { Shell } from "@opencode-ai/core/shell" import { Job } from "@opencode-ai/core/job" import { MCP } from "@opencode-ai/core/mcp/index" @@ -25,7 +25,6 @@ import { ModelsDev } from "@opencode-ai/core/models-dev" import { SessionRestart } from "@opencode-ai/core/session/execution/restart" import { PluginRuntime } from "@opencode-ai/core/plugin/runtime" import { SdkPlugins } from "@opencode-ai/core/plugin/sdk" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { WellKnown } from "@opencode-ai/core/wellknown" import { Watcher } from "@opencode-ai/core/filesystem/watcher" import { HttpRouter } from "effect/unstable/http" @@ -45,13 +44,12 @@ import type { ServerOptions } from "./options" const applicationServices = LayerNode.group([ Database.node, - EventV2.node, + Bus.node, EventLogger.node, httpClient, - ToolOutputStore.cleanupNode, Job.node, Project.node, - SessionV2.node, + Session.node, PluginRuntime.providerNode, SdkPlugins.node, PermissionSaved.node, @@ -99,7 +97,7 @@ function makeRoutes( }), ], [InstructionDiscovery.node, InstructionDiscovery.configured({ project: options.config?.project })], - [CommandV2.node, CommandV2.configured({ gitbash: options.windows?.gitbash })], + [Command.node, Command.configured({ gitbash: options.windows?.gitbash })], [Pty.node, Pty.configured({ gitbash: options.windows?.gitbash })], [Shell.node, Shell.configured({ gitbash: options.windows?.gitbash })], [ diff --git a/packages/server/test/event-feed.test.ts b/packages/server/test/event-feed.test.ts index 29129c735c2f..897fcf35832b 100644 --- a/packages/server/test/event-feed.test.ts +++ b/packages/server/test/event-feed.test.ts @@ -1,38 +1,39 @@ import { describe, expect, test } from "bun:test" -import { AgentV2 } from "@opencode-ai/core/agent" -import { EventV2 } from "@opencode-ai/core/event" +import { Agent } from "@opencode-ai/core/agent" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" import { OpenCodeEvent } from "@opencode-ai/protocol/groups/event" import { DateTime, Deferred, Effect, Exit, Fiber, Option, Schema, Stream } from "effect" import { it } from "../../core/test/lib/effect" import { EventFeed } from "../src/event-feed" -const Internal = EventV2.ephemeral({ type: "test.internal", schema: { value: Schema.String } }) +const Internal = Bus.ephemeral({ type: "test.internal", schema: { value: Schema.String } }) -const event = (id: string): EventV2.Payload => ({ - id: EventV2.ID.make(`evt_${id}`), +const event = (id: string): Event.Payload => ({ + id: Event.ID.make(`evt_${id}`), created: DateTime.makeUnsafe(Date.now()), - type: AgentV2.Event.Updated.type, + type: Agent.Event.Updated.type, data: {}, }) -const internal = (value: string): EventV2.Payload => ({ - id: EventV2.ID.create(), +const internal = (value: string): Event.Payload => ({ + id: Event.ID.create(), created: DateTime.makeUnsafe(Date.now()), type: Internal.type, data: { value }, }) function makeSource() { - let subscriber: EventV2.Subscriber | undefined + let subscriber: Bus.Subscriber | undefined return { - observe: (next: EventV2.Subscriber) => + observe: (next: Bus.Subscriber) => Effect.sync(() => { subscriber = next return Effect.sync(() => { if (subscriber === next) subscriber = undefined }) }), - publish: (event: EventV2.Payload) => Effect.suspend(() => (subscriber ? subscriber(event) : Effect.void)), + publish: (event: Event.Payload) => Effect.suspend(() => (subscriber ? subscriber(event) : Effect.void)), } } @@ -62,8 +63,8 @@ describe("EventFeed", () => { yield* source.publish(event("example")) expect([Array.from(yield* Fiber.join(left)), Array.from(yield* Fiber.join(right))]).toEqual([ - [AgentV2.Event.Updated.type], - [AgentV2.Event.Updated.type], + [Agent.Event.Updated.type], + [Agent.Event.Updated.type], ]) expect(encodes).toBe(1) }), @@ -123,7 +124,7 @@ describe("EventFeed", () => { yield* source.publish(internal("two")) yield* source.publish(event("public")) - expect(Array.from(yield* stream.pipe(Stream.take(1), Stream.runCollect))).toEqual([AgentV2.Event.Updated.type]) + expect(Array.from(yield* stream.pipe(Stream.take(1), Stream.runCollect))).toEqual([Agent.Event.Updated.type]) }), ) @@ -132,7 +133,7 @@ describe("EventFeed", () => { const source = makeSource() const feed = yield* EventFeed.make(source.observe, { encode: (event) => { - if (event.id === EventV2.ID.make("evt_bad")) throw new Error("invalid event") + if (event.id === Event.ID.make("evt_bad")) throw new Error("invalid event") return event.id }, }) diff --git a/packages/simulation/src/backend/simulated-provider.ts b/packages/simulation/src/backend/simulated-provider.ts index 3bb9014df8d6..84b36d5878b9 100644 --- a/packages/simulation/src/backend/simulated-provider.ts +++ b/packages/simulation/src/backend/simulated-provider.ts @@ -1,6 +1,6 @@ import { SdkPlugins } from "@opencode-ai/core/plugin/sdk" -import { Plugin } from "@opencode-ai/plugin/v2/effect" -import { Tool } from "@opencode-ai/plugin/v2/effect/tool" +import { Tool } from "@opencode-ai/core/tool" +import { Plugin } from "@opencode-ai/plugin/effect" import { createHash } from "node:crypto" import { Cause, @@ -452,10 +452,10 @@ const makeToolDriver = Effect.fn("SimulatedProvider.makeToolDriver")(function* ( name: string, input: unknown, context: Tool.Context, - ): Effect.Effect, Tool.Failure> => + ): Effect.Effect => Effect.gen(function* () { const encoded = yield* Schema.decodeUnknownEffect(Schema.Json)(input).pipe( - Effect.mapError((error) => new Tool.Failure({ message: `Simulated tool input is not JSON: ${error.message}` })), + Effect.mapError((error) => new Tool.Error({ message: `Simulated tool input is not JSON: ${error.message}` })), ) const invocation = yield* Effect.uninterruptibleMask((restore) => attachmentLock @@ -465,7 +465,7 @@ const makeToolDriver = Effect.fn("SimulatedProvider.makeToolDriver")(function* ( const current = yield* Ref.get(state) if (current.generation !== registrationGeneration) yield* Effect.fail( - new Tool.Failure({ message: `Simulated tool registration is no longer active: ${name}` }), + new Tool.Error({ message: `Simulated tool registration is no longer active: ${name}` }), ) const id = `tool_${current.counter + 1}` const completion = yield* Deferred.make() @@ -526,9 +526,20 @@ const makeToolDriver = Effect.fn("SimulatedProvider.makeToolDriver")(function* ( output: invocation.output.structured, ...(invocation.output.content.length === 0 ? {} - : { content: invocation.output.content as [Tool.Content, ...Tool.Content[]] }), + : { + content: invocation.output.content.map((part) => + part.type === "text" + ? part + : { + type: "file" as const, + uri: `data:${part.mime};base64,${part.data}`, + mime: part.mime, + ...(part.name === undefined ? {} : { name: part.name }), + }, + ), + }), } - return yield* Effect.fail(new Tool.Failure({ message: invocation.message })) + return yield* new Tool.Error({ message: invocation.message }) }) yield* plugins.register( @@ -552,8 +563,12 @@ const makeToolDriver = Effect.fn("SimulatedProvider.makeToolDriver")(function* ( .transform((draft) => { for (const registration of nextRegistrations) draft.add( - registration.name, - Tool.make({ + { + name: registration.name, + options: + registration.permission === undefined + ? registration.options + : { ...registration.options, permission: registration.permission }, description: registration.description, input: registration.inputSchema, output: registration.outputSchema ?? {}, @@ -564,10 +579,7 @@ const makeToolDriver = Effect.fn("SimulatedProvider.makeToolDriver")(function* ( input, context, ), - }), - registration.permission === undefined - ? registration.options - : { ...registration.options, permission: registration.permission }, + }, ) }) .pipe(Scope.provide(nextScope)), diff --git a/packages/simulation/test/simulated-provider.test.ts b/packages/simulation/test/simulated-provider.test.ts index 82238bae0286..39a6509c188c 100644 --- a/packages/simulation/test/simulated-provider.test.ts +++ b/packages/simulation/test/simulated-provider.test.ts @@ -2,22 +2,22 @@ import { expect, test } from "bun:test" import { mkdir, mkdtemp, rm } from "node:fs/promises" import { tmpdir } from "node:os" import { join } from "node:path" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Config } from "@opencode-ai/core/config" import { Database } from "@opencode-ai/core/database/database" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Location } from "@opencode-ai/core/location" import { LocationServiceMap } from "@opencode-ai/core/location-services" import { SdkPlugins } from "@opencode-ai/core/plugin/sdk" import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionMessage } from "@opencode-ai/core/session/message" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { Plugin } from "@opencode-ai/plugin/v2/effect" +import { Tool } from "@opencode-ai/core/tool" +import { Plugin } from "@opencode-ai/plugin/effect" import { Deferred, Effect, Fiber, Layer, Queue, Stream } from "effect" import type { Scope } from "effect/Scope" import { SimulatedProvider } from "../src/backend/simulated-provider" @@ -264,7 +264,7 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { }), ) expect(yield* Queue.take(messages)).toMatchObject({ id: 1, result: { attached: true } }) - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const toolSet = yield* registry.snapshot() expect(toolSet.definitions).toContainEqual( expect.objectContaining({ name: "lookup", description: "Look up a value" }), @@ -272,17 +272,17 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { expect( (yield* registry.snapshot([{ action: "simulate_lookup", resource: "*", effect: "deny" }])).definitions, ).not.toContainEqual(expect.objectContaining({ name: "lookup" })) - const secondaryToolSet = yield* ToolRegistry.Service.use((secondaryRegistry) => + const secondaryToolSet = yield* Tool.Service.use((secondaryRegistry) => secondaryRegistry.snapshot(), ).pipe(Effect.provide(secondary)) expect(secondaryToolSet.definitions).toContainEqual( expect.objectContaining({ name: "lookup", description: "Look up a value" }), ) - const progress: ToolRegistry.Progress[] = [] + const progress: Tool.Metadata[] = [] const executeCall = (callID: string, query: string) => toolSet.execute({ - sessionID: SessionV2.ID.make("ses_simulated_tools"), - agent: AgentV2.ID.make("build"), + sessionID: Session.ID.make("ses_simulated_tools"), + agent: Agent.ID.make("build"), messageID: SessionMessage.ID.make("msg_simulated_tools"), progress: (update) => Effect.sync(() => progress.push(update)), call: { @@ -390,13 +390,12 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { ) expect(yield* Queue.take(messages)).toMatchObject({ id: 23, result: { ok: true } }) expect(yield* Fiber.join(successful)).toMatchObject({ - status: "completed", output: { answer: 42 }, content: [{ type: "text", text: "42" }], }) expect(progress).toEqual([{ phase: "searching" }]) - const failed = yield* executeCall("call_failure", "missing").pipe(Effect.forkScoped) + const failed = yield* executeCall("call_failure", "missing").pipe(Effect.exit, Effect.forkScoped) const failedInvocation = yield* takeToolInvocation(messages) const failedID = requireString(requireRecord(failedInvocation.params).id) socket.send( @@ -408,10 +407,9 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { }), ) expect(yield* Queue.take(messages)).toMatchObject({ id: 4, result: { ok: true } }) - expect(yield* Fiber.join(failed)).toMatchObject({ - status: "error", - error: { message: "lookup failed" }, - }) + const failedExit = yield* Fiber.join(failed) + expect(failedExit).toMatchObject({ _tag: "Failure" }) + expect(failedExit.toString()).toContain("lookup failed") const concurrent = [ yield* executeCall("call_first", "first").pipe(Effect.forkScoped), @@ -443,12 +441,10 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { expect(yield* Queue.take(messages)).toMatchObject({ id, result: { ok: true } }) } expect(yield* Fiber.join(concurrent[0])).toMatchObject({ - status: "completed", output: "first result", content: [{ type: "text", text: "first result" }], }) expect(yield* Fiber.join(concurrent[1])).toMatchObject({ - status: "completed", output: "second result", content: [{ type: "text", text: "second result" }], }) @@ -564,7 +560,6 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { ) expect(yield* Queue.take(replacementMessages)).toMatchObject({ id: 10, result: { ok: true } }) expect(yield* Fiber.join(replayed)).toMatchObject({ - status: "completed", output: "replayed result", content: [{ type: "text", text: "replayed result" }], }) @@ -585,7 +580,6 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { ) expect(yield* Queue.take(replacementMessages)).toMatchObject({ id: 27, result: { ok: true } }) expect(yield* Fiber.join(preserved)).toMatchObject({ - status: "completed", output: "preserved", content: [{ type: "text", text: "preserved" }], }) @@ -607,7 +601,7 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { const replacedNames = replaced.definitions.map((definition) => definition.name) expect(replacedNames).toEqual(expect.arrayContaining(["github_search", "web_search"])) expect(replacedNames).not.toContain("lookup") - const secondaryReplaced = yield* ToolRegistry.Service.use((secondaryRegistry) => + const secondaryReplaced = yield* Tool.Service.use((secondaryRegistry) => secondaryRegistry.snapshot(), ).pipe(Effect.provide(secondary)) const secondaryNames = secondaryReplaced.definitions.map((definition) => definition.name) @@ -615,8 +609,8 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { expect(secondaryNames).not.toContain("lookup") const routed = yield* replaced .execute({ - sessionID: SessionV2.ID.make("ses_simulated_tools"), - agent: AgentV2.ID.make("build"), + sessionID: Session.ID.make("ses_simulated_tools"), + agent: Agent.ID.make("build"), messageID: SessionMessage.ID.make("msg_simulated_tools"), call: { type: "tool-call", @@ -642,14 +636,13 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { ) expect(yield* Queue.take(replacementMessages)).toMatchObject({ id: 12, result: { ok: true } }) expect(yield* Fiber.join(routed)).toMatchObject({ - status: "completed", output: "routed", content: [{ type: "text", text: "routed" }], }) - expect( - yield* toolSet.execute({ - sessionID: SessionV2.ID.make("ses_simulated_tools"), - agent: AgentV2.ID.make("build"), + const stale = yield* toolSet + .execute({ + sessionID: Session.ID.make("ses_simulated_tools"), + agent: Agent.ID.make("build"), messageID: SessionMessage.ID.make("msg_simulated_tools"), call: { type: "tool-call", @@ -657,11 +650,10 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { name: "lookup", input: { query: "stale" }, }, - }), - ).toMatchObject({ - status: "error", - error: { message: expect.stringContaining("no longer active") }, - }) + }) + .pipe(Effect.exit) + expect(stale).toMatchObject({ _tag: "Failure" }) + expect(stale.toString()).toContain("no longer active") expect(activations).toBe(2) }).pipe(Effect.provide(primary)) }).pipe(Effect.provide(toolLifecycleLayer(endpoint)), Effect.scoped), @@ -708,7 +700,7 @@ const toolLifecycleLayer = (endpoint: string) => { deps: [SdkPlugins.node], }) return AppNodeBuilder.build( - LayerNode.group([Database.node, EventV2.node, SdkPlugins.node, LocationServiceMap.node, provider]), + LayerNode.group([Database.node, Bus.node, SdkPlugins.node, LocationServiceMap.node, provider]), [[Config.node, Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) }))]], ) } diff --git a/packages/tui/src/attention.ts b/packages/tui/src/attention.ts index 7eac2e064692..57272243ee97 100644 --- a/packages/tui/src/attention.ts +++ b/packages/tui/src/attention.ts @@ -9,7 +9,7 @@ import type { TuiAttentionSoundName, TuiAttentionSoundPack, TuiAttentionSoundPackInfo, -} from "@opencode-ai/plugin/tui" +} from "@opencode-ai/plugin/v1/tui" import { AttentionSoundName, type Config } from "./config" import { Schema } from "effect" import stripAnsi from "strip-ansi" diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index 724d67830e45..f0e313f9214f 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -14,8 +14,8 @@ import type { McpServer, ModelInfo, PermissionSavedInfo, - PermissionV2Request, - ProviderV2Info, + PermissionRequest, + ProviderInfo, ReferenceInfo, SessionMessageInfo, SessionMessageAssistant, @@ -29,7 +29,7 @@ import type { OpenCodeEvent, WebSearchProvider, } from "@opencode-ai/client" -import type { Plugin } from "@opencode-ai/plugin/v2/tui" +import type { Plugin } from "@opencode-ai/plugin/tui" import { createStore, produce, reconcile } from "solid-js/store" import { createSimpleContext } from "./helper" import { useClient } from "./client" @@ -55,7 +55,7 @@ type LocationData = { resource?: McpResource[] } model?: ModelInfo[] - provider?: ProviderV2Info[] + provider?: ProviderInfo[] reference?: ReferenceInfo[] websearch?: WebSearchProvider[] // Currently running shell commands for this location, keyed by shell id. Entries are removed @@ -75,7 +75,7 @@ type Store = { message: Record pending: Record input: Record - permission: Record + permission: Record // Pending forms keyed by owner: a session ID or the temporary "global" elicitation sentinel. form: Record } @@ -808,14 +808,14 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ message.append(draft, index, failed) }) break - case "permission.v2.asked": + case "permission.asked": if (store.session.permission[event.data.sessionID]?.some((request) => request.id === event.data.id)) break setStore("session", "permission", event.data.sessionID, [ ...(store.session.permission[event.data.sessionID] ?? []), event.data, ]) break - case "permission.v2.replied": + case "permission.replied": setStore( "session", "permission", diff --git a/packages/tui/src/context/keymap.tsx b/packages/tui/src/context/keymap.tsx index e8da0ca909aa..e29c6fa2f3ac 100644 --- a/packages/tui/src/context/keymap.tsx +++ b/packages/tui/src/context/keymap.tsx @@ -1,4 +1,4 @@ -import type { KeymapCommand, KeymapLayer } from "@opencode-ai/plugin/v2/tui/context" +import type { KeymapCommand, KeymapLayer } from "@opencode-ai/plugin/tui/context" import { InputRenderable, TextareaRenderable, type KeyEvent, type Renderable } from "@opentui/core" import { stringifyKeyStroke, type Binding, type CommandContext } from "@opentui/keymap" import { @@ -155,7 +155,7 @@ function Provider(props: ParentProps<{ config?: KeymapConfig }>) { ) } -export type { KeymapCommand, KeymapLayer } from "@opencode-ai/plugin/v2/tui/context" +export type { KeymapCommand, KeymapLayer } from "@opencode-ai/plugin/tui/context" export interface Keymap { /** Dispatches a reachable command by ID. */ diff --git a/packages/tui/src/feature-plugins/builtins.ts b/packages/tui/src/feature-plugins/builtins.ts index 1e051ae4d2a0..e3c1783f58ec 100644 --- a/packages/tui/src/feature-plugins/builtins.ts +++ b/packages/tui/src/feature-plugins/builtins.ts @@ -1,4 +1,4 @@ -import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui" +import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/v1/tui" import type { PluginRuntime } from "../plugin/runtime" import Notifications from "./system/notifications" import PluginManager from "./system/plugins" diff --git a/packages/tui/src/feature-plugins/home/footer.tsx b/packages/tui/src/feature-plugins/home/footer.tsx index e8d39ee82f4c..60623ef61b96 100644 --- a/packages/tui/src/feature-plugins/home/footer.tsx +++ b/packages/tui/src/feature-plugins/home/footer.tsx @@ -1,4 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin/v2/tui" +import { Plugin } from "@opencode-ai/plugin/tui" import { createMemo, Match, Show, Switch } from "solid-js" import { useTerminalDimensions } from "@opentui/solid" import { useTuiApp, useTuiPaths } from "../../context/runtime" diff --git a/packages/tui/src/feature-plugins/sidebar/context.tsx b/packages/tui/src/feature-plugins/sidebar/context.tsx index aaec364aa396..52655add5184 100644 --- a/packages/tui/src/feature-plugins/sidebar/context.tsx +++ b/packages/tui/src/feature-plugins/sidebar/context.tsx @@ -1,4 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin/v2/tui" +import { Plugin } from "@opencode-ai/plugin/tui" import { createMemo, Show } from "solid-js" import { useTheme } from "../../context/theme" import { contextUsage } from "../../util/session" diff --git a/packages/tui/src/feature-plugins/sidebar/footer.tsx b/packages/tui/src/feature-plugins/sidebar/footer.tsx index 49f5c25a4842..adde20e115f8 100644 --- a/packages/tui/src/feature-plugins/sidebar/footer.tsx +++ b/packages/tui/src/feature-plugins/sidebar/footer.tsx @@ -1,4 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin/v2/tui" +import { Plugin } from "@opencode-ai/plugin/tui" import { createMemo, Show } from "solid-js" import { useTuiPaths } from "../../context/runtime" import { useTheme } from "../../context/theme" diff --git a/packages/tui/src/feature-plugins/sidebar/lsp.tsx b/packages/tui/src/feature-plugins/sidebar/lsp.tsx index 63369e344818..5eedcc12b216 100644 --- a/packages/tui/src/feature-plugins/sidebar/lsp.tsx +++ b/packages/tui/src/feature-plugins/sidebar/lsp.tsx @@ -1,4 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin/v2/tui" +import { Plugin } from "@opencode-ai/plugin/tui" import { useTheme } from "../../context/theme" function View() { diff --git a/packages/tui/src/feature-plugins/sidebar/mcp.tsx b/packages/tui/src/feature-plugins/sidebar/mcp.tsx index 17f21c2b6c3b..842da4c31095 100644 --- a/packages/tui/src/feature-plugins/sidebar/mcp.tsx +++ b/packages/tui/src/feature-plugins/sidebar/mcp.tsx @@ -1,4 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin/v2/tui" +import { Plugin } from "@opencode-ai/plugin/tui" import { createMemo, For, Match, Show, Switch, createSignal } from "solid-js" import { useTheme } from "../../context/theme" diff --git a/packages/tui/src/feature-plugins/system/diff-viewer.tsx b/packages/tui/src/feature-plugins/system/diff-viewer.tsx index 380a5035bdab..d7dbbd957e6b 100644 --- a/packages/tui/src/feature-plugins/system/diff-viewer.tsx +++ b/packages/tui/src/feature-plugins/system/diff-viewer.tsx @@ -1,7 +1,7 @@ /** @jsxImportSource @opentui/solid */ import type { FileDiffInfo } from "@opencode-ai/client" -import { Plugin } from "@opencode-ai/plugin/v2/tui" -import type { KeymapCommand, Route } from "@opencode-ai/plugin/v2/tui/context" +import { Plugin } from "@opencode-ai/plugin/tui" +import type { KeymapCommand, Route } from "@opencode-ai/plugin/tui/context" import { TextAttributes, type BorderSides, diff --git a/packages/tui/src/feature-plugins/system/notifications.ts b/packages/tui/src/feature-plugins/system/notifications.ts index 871f8e6eecdc..6592b843b9b7 100644 --- a/packages/tui/src/feature-plugins/system/notifications.ts +++ b/packages/tui/src/feature-plugins/system/notifications.ts @@ -1,5 +1,5 @@ import type { OpenCodeEvent } from "@opencode-ai/client" -import type { TuiAttentionSoundName, TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui" +import type { TuiAttentionSoundName, TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/v1/tui" import type { BuiltinTuiPlugin } from "../builtins" const id = "internal:notifications" diff --git a/packages/tui/src/feature-plugins/system/plugins.tsx b/packages/tui/src/feature-plugins/system/plugins.tsx index 02c916e6ff0a..c04ac640a239 100644 --- a/packages/tui/src/feature-plugins/system/plugins.tsx +++ b/packages/tui/src/feature-plugins/system/plugins.tsx @@ -1,4 +1,4 @@ -import type { TuiPlugin, TuiPluginApi, TuiPluginStatus } from "@opencode-ai/plugin/tui" +import type { TuiPlugin, TuiPluginApi, TuiPluginStatus } from "@opencode-ai/plugin/v1/tui" import type { BuiltinTuiPlugin } from "../builtins" import { useTerminalDimensions } from "@opentui/solid" import { fileURLToPath } from "url" diff --git a/packages/tui/src/feature-plugins/system/scrap.tsx b/packages/tui/src/feature-plugins/system/scrap.tsx index 1660a892f89a..1a21a4ca514b 100644 --- a/packages/tui/src/feature-plugins/system/scrap.tsx +++ b/packages/tui/src/feature-plugins/system/scrap.tsx @@ -1,4 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin/v2/tui" +import { Plugin } from "@opencode-ai/plugin/tui" import { useTerminalDimensions } from "@opentui/solid" import { Keymap } from "../../context/keymap" import { useTheme } from "../../context/theme" diff --git a/packages/tui/src/feature-plugins/system/which-key.tsx b/packages/tui/src/feature-plugins/system/which-key.tsx index 732b3a96c3aa..2617d27af42a 100644 --- a/packages/tui/src/feature-plugins/system/which-key.tsx +++ b/packages/tui/src/feature-plugins/system/which-key.tsx @@ -4,7 +4,7 @@ import { useTerminalDimensions } from "@opentui/solid" import { createEffect, createMemo, createSignal, For, Show } from "solid-js" import { Keymap } from "../../context/keymap" import type { ActiveKey } from "@opentui/keymap" -import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui" +import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/v1/tui" import type { BuiltinTuiPlugin } from "../builtins" const command = { diff --git a/packages/tui/src/mini/stream-v2.subagent.ts b/packages/tui/src/mini/stream-v2.subagent.ts index 95e9719f8699..6f1fdf56c75e 100644 --- a/packages/tui/src/mini/stream-v2.subagent.ts +++ b/packages/tui/src/mini/stream-v2.subagent.ts @@ -18,7 +18,7 @@ import type { EventSubscribeOutput, OpenCodeClient, - PermissionV2Request, + PermissionRequest, SessionMessageAssistantTool, SessionMessageInfo, } from "@opencode-ai/client/promise" @@ -168,14 +168,14 @@ function sourceKey(messageID: string, callID: string) { return `${messageID}\u0000${callID}` } -function permissionTool(request: PermissionV2Request, tools: Map) { +function permissionTool(request: PermissionRequest, tools: Map) { if (request.source?.type !== "tool") return request const tool = tools.get(sourceKey(request.source.messageID, request.source.callID)) return tool ? { ...request, tool } : request } function blockerCategory(event: V2Event): "permission" | "form" | undefined { - if (event.type === "permission.v2.asked" || event.type === "permission.v2.replied") return "permission" + if (event.type === "permission.asked" || event.type === "permission.replied") return "permission" if (event.type === "form.created" || event.type === "form.replied" || event.type === "form.cancelled") return "form" } @@ -436,7 +436,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac const resolvePermissionTools = async ( sdk: OpenCodeClient, child: ChildState, - permissions: PermissionV2Request[], + permissions: PermissionRequest[], epoch: number, signal = input.signal, ) => { @@ -859,13 +859,13 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac notifyDetail(child) return } - if (event.type === "permission.v2.asked") { + if (event.type === "permission.asked") { if (!child.permissions.some((item) => item.id === event.data.id)) child.permissions.push(permissionTool(event.data, child.toolSources)) input.emit() return } - if (event.type === "permission.v2.replied") { + if (event.type === "permission.replied") { child.permissions = child.permissions.filter((item) => item.id !== event.data.requestID) input.emit() return diff --git a/packages/tui/src/mini/stream-v2.transport.ts b/packages/tui/src/mini/stream-v2.transport.ts index e0e767ca56d0..5ba7870d5c1e 100644 --- a/packages/tui/src/mini/stream-v2.transport.ts +++ b/packages/tui/src/mini/stream-v2.transport.ts @@ -3,7 +3,7 @@ import type { FormInfo, LocationRef, OpenCodeClient, - PermissionV2Request, + PermissionRequest, SessionMessageAssistantTool, SessionMessageInfo, SessionPendingInfo, @@ -295,7 +295,7 @@ function permissionSourceKey(messageID: string, callID: string) { return streamPartKey(messageID, callID) } -function permissionTool(request: PermissionV2Request, tools: Map) { +function permissionTool(request: PermissionRequest, tools: Map) { if (request.source?.type !== "tool") return request const tool = tools.get(permissionSourceKey(request.source.messageID, request.source.callID)) return tool ? { ...request, tool } : request @@ -715,7 +715,7 @@ export async function createSessionTransport(input: StreamInput): Promise { const pending = new Set( @@ -1098,13 +1098,13 @@ export async function createSessionTransport(input: StreamInput): Promise item.id === event.data.id)) state.permissions.push(permissionTool(event.data, state.toolSources)) syncBlockers() return } - if (event.type === "permission.v2.replied") { + if (event.type === "permission.replied") { state.permissions = state.permissions.filter((item) => item.id !== event.data.requestID) pruneToolSources() syncBlockers() diff --git a/packages/tui/src/mini/theme.ts b/packages/tui/src/mini/theme.ts index 3c5afed16e6a..5a1fd22f4aa6 100644 --- a/packages/tui/src/mini/theme.ts +++ b/packages/tui/src/mini/theme.ts @@ -6,7 +6,7 @@ // the run footer + scrollback color model. Falls back to a hardcoded dark-mode // palette if detection fails. import { RGBA, SyntaxStyle, type CliRenderer, type ColorInput, type TerminalColors } from "@opentui/core" -import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui" +import type { TuiThemeCurrent } from "@opencode-ai/plugin/v1/tui" import { ansiToRgba } from "../theme/color" import { resolveThemeColors } from "../theme/resolve" import { terminalMode } from "../theme/system" diff --git a/packages/tui/src/mini/types.ts b/packages/tui/src/mini/types.ts index 2ee854010bfb..5f0e2f79e1b4 100644 --- a/packages/tui/src/mini/types.ts +++ b/packages/tui/src/mini/types.ts @@ -17,7 +17,7 @@ import type { OpenCodeClient, LocationGetOutput, LocationRef, - PermissionV2Request, + PermissionRequest, ReferenceListOutput, SessionMessageAssistantTool, } from "@opencode-ai/client/promise" @@ -254,7 +254,7 @@ export type MiniToolPart = { state: MiniToolState } -export type MiniPermissionRequest = PermissionV2Request & { +export type MiniPermissionRequest = PermissionRequest & { tool?: SessionMessageAssistantTool } diff --git a/packages/tui/src/plugin/api.ts b/packages/tui/src/plugin/api.ts index 9ab6192f63d4..1a9335dd6f6c 100644 --- a/packages/tui/src/plugin/api.ts +++ b/packages/tui/src/plugin/api.ts @@ -1,4 +1,4 @@ -import type { TuiRouteDefinition } from "@opencode-ai/plugin/tui" +import type { TuiRouteDefinition } from "@opencode-ai/plugin/v1/tui" import { createSignal } from "solid-js" type RouteEntry = { diff --git a/packages/tui/src/plugin/command-shim.ts b/packages/tui/src/plugin/command-shim.ts index 36e7548fcfac..6141cbebd547 100644 --- a/packages/tui/src/plugin/command-shim.ts +++ b/packages/tui/src/plugin/command-shim.ts @@ -1,5 +1,5 @@ // Legacy `api.command` bridge for v1 plugins; remove in v2. -import type { TuiCommand, TuiPluginApi } from "@opencode-ai/plugin/tui" +import type { TuiCommand, TuiPluginApi } from "@opencode-ai/plugin/v1/tui" import { TuiKeybind } from "../config/keybind" import type { DialogContext } from "../ui/dialog" diff --git a/packages/tui/src/plugin/context.tsx b/packages/tui/src/plugin/context.tsx index 29777342c8d3..a42434b5d6a3 100644 --- a/packages/tui/src/plugin/context.tsx +++ b/packages/tui/src/plugin/context.tsx @@ -1,4 +1,4 @@ -import type { Plugin } from "@opencode-ai/plugin/v2/tui" +import type { Plugin } from "@opencode-ai/plugin/tui" import { batch, createContext, @@ -13,7 +13,7 @@ import { import path from "path" import { stat } from "fs/promises" import { fileURLToPath, pathToFileURL } from "url" -import type { Context, Page, Slot } from "@opencode-ai/plugin/v2/tui/context" +import type { Context, Page, Slot } from "@opencode-ai/plugin/tui/context" import { createStore, produce, reconcile as reconcileStore } from "solid-js/store" import { useConfig } from "../config" import { useClient } from "../context/client" diff --git a/packages/tui/src/plugin/runtime.tsx b/packages/tui/src/plugin/runtime.tsx index 4e8fa51f67d5..0d248545cdc7 100644 --- a/packages/tui/src/plugin/runtime.tsx +++ b/packages/tui/src/plugin/runtime.tsx @@ -3,7 +3,7 @@ import type { TuiPluginInstallOptions, TuiPluginInstallResult, TuiPluginStatus, -} from "@opencode-ai/plugin/tui" +} from "@opencode-ai/plugin/v1/tui" import { createContext, createSignal, useContext, type JSX, type ParentProps } from "solid-js" import { createPluginRoutes } from "./api" import { createSlots, type HostSlots } from "./slots" diff --git a/packages/tui/src/plugin/slots.tsx b/packages/tui/src/plugin/slots.tsx index 8c1da1c7ebdc..a40037532b41 100644 --- a/packages/tui/src/plugin/slots.tsx +++ b/packages/tui/src/plugin/slots.tsx @@ -1,4 +1,4 @@ -import type { TuiPluginApi, TuiSlotContext, TuiSlotMap, TuiSlotProps } from "@opencode-ai/plugin/tui" +import type { TuiPluginApi, TuiSlotContext, TuiSlotMap, TuiSlotProps } from "@opencode-ai/plugin/v1/tui" import { createSlot, createSolidSlotRegistry, type JSX, type SolidPlugin } from "@opentui/solid" import { createSignal } from "solid-js" import { isRecord } from "../util/record" diff --git a/packages/tui/src/routes/session/permission.tsx b/packages/tui/src/routes/session/permission.tsx index a6a746d881bc..e70c2917f026 100644 --- a/packages/tui/src/routes/session/permission.tsx +++ b/packages/tui/src/routes/session/permission.tsx @@ -3,7 +3,7 @@ import { createMemo, For, Match, Show, Switch } from "solid-js" import { Portal, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid" import type { TextareaRenderable } from "@opentui/core" import { useTheme } from "../../context/theme" -import type { PermissionV2Request } from "@opencode-ai/client" +import type { PermissionRequest } from "@opencode-ai/client" import { useClient } from "../../context/client" import { SplitBorder } from "../../ui/border" import { useData } from "../../context/data" @@ -107,7 +107,7 @@ function EditBody(props: { file?: string; diff?: string; patch?: string }) { ) } -export function PermissionPrompt(props: { request: PermissionV2Request; directory?: string }) { +export function PermissionPrompt(props: { request: PermissionRequest; directory?: string }) { const client = useClient() const data = useData() const [store, setStore] = createStore({ diff --git a/packages/tui/test/cli/cmd/tui/notifications.test.ts b/packages/tui/test/cli/cmd/tui/notifications.test.ts index 1fa253b0fdc8..f717496defeb 100644 --- a/packages/tui/test/cli/cmd/tui/notifications.test.ts +++ b/packages/tui/test/cli/cmd/tui/notifications.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import Notifications from "../../../../src/feature-plugins/system/notifications" import type { OpenCodeEvent, PermissionAsked, QuestionAsked } from "@opencode-ai/client" -import type { TuiAttentionNotifyInput, TuiPluginApi } from "@opencode-ai/plugin/tui" +import type { TuiAttentionNotifyInput, TuiPluginApi } from "@opencode-ai/plugin/v1/tui" import { createTuiPluginApi } from "../../../fixture/tui-plugin" type Session = NonNullable> @@ -95,10 +95,10 @@ function permission(id: string, sessionID = "session"): PermissionAsked["data"] return { id, sessionID, - permission: "edit", - patterns: [], + action: "edit", + resources: [], metadata: {}, - always: [], + save: [], } } diff --git a/packages/tui/test/cli/tui/data.test.tsx b/packages/tui/test/cli/tui/data.test.tsx index 60a557854292..b94dae7e8094 100644 --- a/packages/tui/test/cli/tui/data.test.tsx +++ b/packages/tui/test/cli/tui/data.test.tsx @@ -3,7 +3,8 @@ import { expect, test } from "bun:test" import { testRender } from "@opentui/solid" import type { OpenCodeEvent } from "@opencode-ai/client" import { SessionMessage } from "@opencode-ai/core/session/message" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" import { createEffect, onMount, type ParentProps } from "solid-js" import { ConfigProvider } from "../../../src/config" import { ClientProvider, useClient } from "../../../src/context/client" @@ -879,7 +880,7 @@ test("removes committed revert messages from local state", async () => { try { for (const [seq, inputID] of ["msg_001", "msg_002", "msg_003"].entries()) { emitEvent(events, { - id: EventV2.ID.create(), + id: Event.ID.create(), created: seq, type: "session.input.admitted", durable: durable(sessionID, seq), @@ -889,7 +890,7 @@ test("removes committed revert messages from local state", async () => { await wait(() => data.session.message.list(sessionID).length === 3) emitEvent(events, { - id: EventV2.ID.create(), + id: Event.ID.create(), created: 3, type: "session.revert.committed", durable: durable(sessionID, 3), @@ -1812,7 +1813,7 @@ test("adds and dismisses permission requests from live events", async () => { emitEvent(events, { id: "evt_permission_asked_1", created: 0, - type: "permission.v2.asked", + type: "permission.asked", data: { id: "per_1", sessionID: "ses_1", @@ -1823,7 +1824,7 @@ test("adds and dismisses permission requests from live events", async () => { emitEvent(events, { id: "evt_permission_asked_2", created: 0, - type: "permission.v2.asked", + type: "permission.asked", data: { id: "per_2", sessionID: "ses_1", @@ -1836,7 +1837,7 @@ test("adds and dismisses permission requests from live events", async () => { emitEvent(events, { id: "evt_permission_replied_1", created: 0, - type: "permission.v2.replied", + type: "permission.replied", data: { sessionID: "ses_1", requestID: "per_1", reply: "once" }, }) await wait(() => data.session.permission.list("ses_1")?.length === 1) @@ -1845,7 +1846,7 @@ test("adds and dismisses permission requests from live events", async () => { emitEvent(events, { id: "evt_permission_replied_2", created: 0, - type: "permission.v2.replied", + type: "permission.replied", data: { sessionID: "ses_1", requestID: "per_2", reply: "reject" }, }) await wait(() => data.session.permission.list("ses_1")?.length === 0) @@ -2570,7 +2571,7 @@ test("skips initial instruction state and projects later updates with their mess await wait(() => sync.session.message.list("session-1")?.some((message) => message.time.created === 1)) expect(sync.session.message.list("session-1")).toHaveLength(1) expect(sync.session.message.list("session-1")?.[0]).toMatchObject({ - id: SessionMessage.ID.fromEvent(EventV2.ID.make("evt_instructions_2")), + id: SessionMessage.ID.fromEvent(Event.ID.make("evt_instructions_2")), type: "system", text: "Instructions updated: core/date", time: { created: 1 }, diff --git a/packages/tui/test/cli/tui/diff-viewer.test.tsx b/packages/tui/test/cli/tui/diff-viewer.test.tsx index f09e49aa0a9c..c54df1f229ad 100644 --- a/packages/tui/test/cli/tui/diff-viewer.test.tsx +++ b/packages/tui/test/cli/tui/diff-viewer.test.tsx @@ -10,7 +10,7 @@ import type { Page, Route, Slot, -} from "@opencode-ai/plugin/v2/tui/context" +} from "@opencode-ai/plugin/tui/context" import { ThemeProvider } from "../../../src/context/theme" import { ConfigProvider } from "../../../src/config" import { TuiKeybind } from "../../../src/config/keybind" diff --git a/packages/tui/test/fixture/tui-plugin.ts b/packages/tui/test/fixture/tui-plugin.ts index 23dbf526ae4e..b4e462808315 100644 --- a/packages/tui/test/fixture/tui-plugin.ts +++ b/packages/tui/test/fixture/tui-plugin.ts @@ -1,4 +1,4 @@ -import type { TuiPluginApi } from "@opencode-ai/plugin/tui" +import type { TuiPluginApi } from "@opencode-ai/plugin/v1/tui" import { RGBA } from "@opentui/core" import { createTuiResolvedConfig } from "./tui-runtime" diff --git a/packages/tui/test/mini/stream-v2.transport.test.ts b/packages/tui/test/mini/stream-v2.transport.test.ts index b4ce61ef8d18..6b18cff9f09a 100644 --- a/packages/tui/test/mini/stream-v2.transport.test.ts +++ b/packages/tui/test/mini/stream-v2.transport.test.ts @@ -8,7 +8,7 @@ import { type FormInfo, type MessageListOutput, type OpenCodeClient, - type PermissionV2Request, + type PermissionRequest, } from "@opencode-ai/client/promise" import { createSessionTransport } from "../../src/mini/stream-v2.transport" import type { StreamCommit } from "../../src/mini/types" @@ -126,7 +126,7 @@ function sdk(input: { forms?: Record globals?: FormInfo[] globalLocation?: { directory: string; workspaceID?: string } - permissions?: Record + permissions?: Record pending?: Record>> wait?: () => Promise }) { @@ -330,7 +330,7 @@ describe("V2 mini transport", () => { ], time: { created: 1 }, } - const permission: PermissionV2Request = { + const permission: PermissionRequest = { id: "per_child_startup", sessionID: "ses_child", action: "shell", @@ -1038,7 +1038,7 @@ describe("V2 mini transport", () => { events.push({ id: "evt_permission", created: 0, - type: "permission.v2.asked", + type: "permission.asked", data: { id: "per_1", sessionID: "ses_1", action: "read", resources: ["/tmp/file"] }, }) @@ -2998,7 +2998,7 @@ describe("V2 mini transport", () => { events.push({ id: "evt_child_permission", created: 7, - type: "permission.v2.asked", + type: "permission.asked", data: { id: "per_child", sessionID: "ses_child_progress", diff --git a/packages/util/src/fs-util.ts b/packages/util/src/fs-util.ts index 46d408a82714..affeedebe3f3 100644 --- a/packages/util/src/fs-util.ts +++ b/packages/util/src/fs-util.ts @@ -46,7 +46,7 @@ export namespace FSUtil { readonly globMatch: (pattern: string, filepath: string) => boolean } - export class Service extends Context.Service()("@opencode/FileSystem") {} + export class Service extends Context.Service()("@opencode/FSUtil") {} export const use = serviceUse(Service) diff --git a/packages/www/content/docs/build/plugins.mdx b/packages/www/content/docs/build/plugins.mdx index 6522c55906dc..7d36320556dd 100644 --- a/packages/www/content/docs/build/plugins.mdx +++ b/packages/www/content/docs/build/plugins.mdx @@ -121,7 +121,7 @@ package version or a local dependency when no watched file changed. Export the result of `Plugin.define` as the module default: ```ts title=".opencode/plugins/reviewer.ts" -import { Plugin } from "@opencode-ai/plugin/v2" +import { Plugin } from "@opencode-ai/plugin" export default Plugin.define({ id: "acme.reviewer", @@ -203,7 +203,7 @@ other sources. Here's an example that keeps models synced from a remote source: ```js title=".opencode/plugins/remote-models.js" -import { Plugin } from "@opencode-ai/plugin/v2" +import { Plugin } from "@opencode-ai/plugin" export default Plugin.define({ id: "acme.remote-models", @@ -248,13 +248,13 @@ mutable fields: | `ctx.aisdk.hook("language", callback)` | `language`, after inspecting `model`, `sdk`, and `options` | | `ctx.session.hook("request", callback)` | `system`, `messages`, and the `tools` record immediately before model dispatch | | `ctx.tool.hook("execute.before", callback)` | `input`, before the selected tool executes | -| `ctx.tool.hook("execute.after", callback)` | Terminal `content`, `metadata`, and `outputPaths`; `error` on failure | +| `ctx.tool.hook("execute.after", callback)` | Terminal `result` on success or `error` on failure | For example, remove a tool from selected model requests and normalize another tool's input: ```ts title=".opencode/plugins/guards.ts" -import { Plugin } from "@opencode-ai/plugin/v2" +import { Plugin } from "@opencode-ai/plugin" export default Plugin.define({ id: "acme.guards", @@ -278,13 +278,11 @@ handle expected errors inside the callback. ### Add a tool -Create an executable tool with `Tool.make`, then register it with a name -and registration options. Define its input with JSON Schema and use an async -executor: +Register a structural tool definition with a name and registration options. +Define its input with JSON Schema and use an async executor: ```js title=".opencode/plugins/greeting.js" -import { Plugin } from "@opencode-ai/plugin/v2" -import { Tool } from "@opencode-ai/plugin/v2/tool" +import { Plugin } from "@opencode-ai/plugin" export default Plugin.define({ id: "acme.greeting", @@ -292,7 +290,7 @@ export default Plugin.define({ await ctx.tool.transform((tools) => { tools.add( "greeting", - Tool.make({ + { description: "Create a greeting", input: { type: "object", @@ -315,7 +313,7 @@ export default Plugin.define({ content: text, } }, - }), + }, ) }) }, @@ -342,7 +340,7 @@ without `output` returns model-visible `content` instead. ### Add a command ```js title=".opencode/plugins/review-command.js" -import { Plugin } from "@opencode-ai/plugin/v2" +import { Plugin } from "@opencode-ai/plugin" export default Plugin.define({ id: "acme.review-command", @@ -360,7 +358,7 @@ export default Plugin.define({ ### Set the default model ```js title=".opencode/plugins/default-model.js" -import { Plugin } from "@opencode-ai/plugin/v2" +import { Plugin } from "@opencode-ai/plugin" export default Plugin.define({ id: "acme.default-model", @@ -410,7 +408,7 @@ being resolved. ## Effect OpenCode provides a first-class Effect API for plugins through the -`@opencode-ai/plugin/v2/effect` entrypoint. Install `effect` alongside the +`@opencode-ai/plugin/effect` entrypoint. Install `effect` alongside the plugin package and export an `effect` function instead of `setup`: ```sh @@ -418,7 +416,7 @@ bun add @opencode-ai/plugin@next effect ``` ```ts title=".opencode/plugins/reviewer-effect.ts" -import { Plugin } from "@opencode-ai/plugin/v2/effect" +import { Plugin } from "@opencode-ai/plugin/effect" import { Effect } from "effect" export default Plugin.define({ @@ -440,7 +438,6 @@ fibers, and registrations are released when the plugin reloads or unloads. OpenCode does not expose its private Core services to the plugin; use the capabilities on `ctx`. -Typed tools can use `Schema` from `effect` and `Tool.make` from -`@opencode-ai/plugin/v2/effect/tool`. Effect and Promise plugins use the same -`tools.add(name, tool, options?)` registration shape. Effect executors +Typed tools can use `Schema` from `effect`. Effect and Promise plugins use the +same `tools.add(name, tool, options?)` registration shape. Effect executors return an Effect and may fail with the typed tool failure channel. diff --git a/packages/www/openapi.json b/packages/www/openapi.json index 1c8b3dc9412d..0508db242812 100644 --- a/packages/www/openapi.json +++ b/packages/www/openapi.json @@ -4279,7 +4279,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/ProviderV2.Info" + "$ref": "#/components/schemas/Provider.Info" } } }, @@ -4396,7 +4396,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/ProviderV2.Info" + "$ref": "#/components/schemas/Provider.Info" } }, "required": [ @@ -7466,7 +7466,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionV2.Request" + "$ref": "#/components/schemas/Permission.Request" } } }, @@ -7664,7 +7664,7 @@ ] }, "effect": { - "$ref": "#/components/schemas/PermissionV2.Effect" + "$ref": "#/components/schemas/Permission.Effect" } }, "required": [ @@ -7762,7 +7762,7 @@ "type": "object" }, "source": { - "$ref": "#/components/schemas/PermissionV2.Source" + "$ref": "#/components/schemas/Permission.Source" }, "agent": { "anyOf": [ @@ -7818,7 +7818,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionV2.Request" + "$ref": "#/components/schemas/Permission.Request" } } }, @@ -7916,7 +7916,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/PermissionV2.Request" + "$ref": "#/components/schemas/Permission.Request" } }, "required": [ @@ -8061,7 +8061,7 @@ "type": "object", "properties": { "reply": { - "$ref": "#/components/schemas/PermissionV2.Reply" + "$ref": "#/components/schemas/Permission.Reply" }, "message": { "anyOf": [ @@ -10456,7 +10456,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Request" + "$ref": "#/components/schemas/Question.Request" } } }, @@ -10527,7 +10527,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Request" + "$ref": "#/components/schemas/Question.Request" } } }, @@ -10667,7 +10667,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QuestionV2.Reply" + "$ref": "#/components/schemas/Question.Reply" } } }, @@ -11981,7 +11981,7 @@ "Agent.Color": { "type": "string" }, - "PermissionV2.Effect": { + "Permission.Effect": { "type": "string", "enum": [ "allow", @@ -11989,7 +11989,7 @@ "ask" ] }, - "PermissionV2.Rule": { + "Permission.Rule": { "type": "object", "properties": { "action": { @@ -11999,7 +11999,7 @@ "type": "string" }, "effect": { - "$ref": "#/components/schemas/PermissionV2.Effect" + "$ref": "#/components/schemas/Permission.Effect" } }, "required": [ @@ -12009,10 +12009,10 @@ ], "additionalProperties": false }, - "PermissionV2.Ruleset": { + "Permission.Ruleset": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionV2.Rule" + "$ref": "#/components/schemas/Permission.Rule" } }, "Agent.Info": { @@ -12059,7 +12059,7 @@ ] }, "permissions": { - "$ref": "#/components/schemas/PermissionV2.Ruleset" + "$ref": "#/components/schemas/Permission.Ruleset" } }, "required": [ @@ -18537,7 +18537,7 @@ ], "additionalProperties": false }, - "ProviderV2.Info": { + "Provider.Info": { "type": "object", "properties": { "id": { @@ -21466,7 +21466,7 @@ ], "additionalProperties": false }, - "PermissionV2.Source": { + "Permission.Source": { "anyOf": [ { "type": "object", @@ -21493,7 +21493,7 @@ } ] }, - "PermissionV2.Request": { + "Permission.Request": { "type": "object", "properties": { "id": { @@ -21531,7 +21531,7 @@ "type": "object" }, "source": { - "$ref": "#/components/schemas/PermissionV2.Source" + "$ref": "#/components/schemas/Permission.Source" } }, "required": [ @@ -21589,7 +21589,7 @@ ], "additionalProperties": false }, - "PermissionV2.Reply": { + "Permission.Reply": { "type": "string", "enum": [ "once", @@ -21935,7 +21935,7 @@ ], "additionalProperties": false }, - "PermissionAction": { + "PermissionV1.Action": { "type": "string", "enum": [ "allow", @@ -21943,7 +21943,7 @@ "ask" ] }, - "PermissionRule": { + "PermissionV1.Rule": { "type": "object", "properties": { "permission": { @@ -21953,7 +21953,7 @@ "type": "string" }, "action": { - "$ref": "#/components/schemas/PermissionAction" + "$ref": "#/components/schemas/PermissionV1.Action" } }, "required": [ @@ -21963,10 +21963,10 @@ ], "additionalProperties": false }, - "PermissionRuleset": { + "PermissionV1.Ruleset": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionRule" + "$ref": "#/components/schemas/PermissionV1.Rule" } }, "SessionV1.Info": { @@ -22155,7 +22155,7 @@ "additionalProperties": false }, "permission": { - "$ref": "#/components/schemas/PermissionRuleset" + "$ref": "#/components/schemas/PermissionV1.Ruleset" }, "revert": { "type": "object", @@ -22455,10 +22455,10 @@ ], "additionalProperties": false }, - "JSONSchema": { + "SessionV1.JSONSchema": { "type": "object" }, - "OutputFormat": { + "SessionV1.OutputFormat": { "anyOf": [ { "type": "object", @@ -22485,7 +22485,7 @@ ] }, "schema": { - "$ref": "#/components/schemas/JSONSchema" + "$ref": "#/components/schemas/SessionV1.JSONSchema" }, "retryCount": { "anyOf": [ @@ -22518,7 +22518,7 @@ } ] }, - "UserMessage": { + "SessionV1.UserMessage": { "type": "object", "properties": { "id": { @@ -22563,7 +22563,7 @@ "format": { "anyOf": [ { - "$ref": "#/components/schemas/OutputFormat" + "$ref": "#/components/schemas/SessionV1.OutputFormat" }, { "type": "null" @@ -22985,7 +22985,7 @@ ], "additionalProperties": false }, - "AssistantMessage": { + "SessionV1.AssistantMessage": { "type": "object", "properties": { "id": { @@ -23218,13 +23218,13 @@ ], "additionalProperties": false }, - "Message": { + "SessionV1.Message": { "anyOf": [ { - "$ref": "#/components/schemas/UserMessage" + "$ref": "#/components/schemas/SessionV1.UserMessage" }, { - "$ref": "#/components/schemas/AssistantMessage" + "$ref": "#/components/schemas/SessionV1.AssistantMessage" } ] }, @@ -23294,7 +23294,7 @@ ] }, "info": { - "$ref": "#/components/schemas/Message" + "$ref": "#/components/schemas/SessionV1.Message" } }, "required": [ @@ -23403,7 +23403,7 @@ ], "additionalProperties": false }, - "TextPart": { + "SessionV1.TextPart": { "type": "object", "properties": { "id": { @@ -23518,7 +23518,7 @@ ], "additionalProperties": false }, - "SubtaskPart": { + "SessionV1.SubtaskPart": { "type": "object", "properties": { "id": { @@ -23605,7 +23605,7 @@ ], "additionalProperties": false }, - "ReasoningPart": { + "SessionV1.ReasoningPart": { "type": "object", "properties": { "id": { @@ -23694,7 +23694,7 @@ ], "additionalProperties": false }, - "FilePartSourceText": { + "SessionV1.FilePartSourceText": { "type": "object", "properties": { "value": { @@ -23714,11 +23714,11 @@ ], "additionalProperties": false }, - "FileSource": { + "SessionV1.FileSource": { "type": "object", "properties": { "text": { - "$ref": "#/components/schemas/FilePartSourceText" + "$ref": "#/components/schemas/SessionV1.FilePartSourceText" }, "type": { "type": "string", @@ -23737,7 +23737,7 @@ ], "additionalProperties": false }, - "Range": { + "SessionV1.Range": { "type": "object", "properties": { "start": { @@ -23799,11 +23799,11 @@ ], "additionalProperties": false }, - "SymbolSource": { + "SessionV1.SymbolSource": { "type": "object", "properties": { "text": { - "$ref": "#/components/schemas/FilePartSourceText" + "$ref": "#/components/schemas/SessionV1.FilePartSourceText" }, "type": { "type": "string", @@ -23815,7 +23815,7 @@ "type": "string" }, "range": { - "$ref": "#/components/schemas/Range" + "$ref": "#/components/schemas/SessionV1.Range" }, "name": { "type": "string" @@ -23839,11 +23839,11 @@ ], "additionalProperties": false }, - "ResourceSource": { + "SessionV1.ResourceSource": { "type": "object", "properties": { "text": { - "$ref": "#/components/schemas/FilePartSourceText" + "$ref": "#/components/schemas/SessionV1.FilePartSourceText" }, "type": { "type": "string", @@ -23866,20 +23866,20 @@ ], "additionalProperties": false }, - "FilePartSource": { + "SessionV1.FilePartSource": { "anyOf": [ { - "$ref": "#/components/schemas/FileSource" + "$ref": "#/components/schemas/SessionV1.FileSource" }, { - "$ref": "#/components/schemas/SymbolSource" + "$ref": "#/components/schemas/SessionV1.SymbolSource" }, { - "$ref": "#/components/schemas/ResourceSource" + "$ref": "#/components/schemas/SessionV1.ResourceSource" } ] }, - "FilePart": { + "SessionV1.FilePart": { "type": "object", "properties": { "id": { @@ -23931,7 +23931,7 @@ "source": { "anyOf": [ { - "$ref": "#/components/schemas/FilePartSource" + "$ref": "#/components/schemas/SessionV1.FilePartSource" }, { "type": "null" @@ -23949,7 +23949,7 @@ ], "additionalProperties": false }, - "ToolStatePending": { + "SessionV1.ToolStatePending": { "type": "object", "properties": { "status": { @@ -23972,7 +23972,7 @@ ], "additionalProperties": false }, - "ToolStateRunning": { + "SessionV1.ToolStateRunning": { "type": "object", "properties": { "status": { @@ -24029,7 +24029,7 @@ ], "additionalProperties": false }, - "ToolStateCompleted": { + "SessionV1.ToolStateCompleted": { "type": "object", "properties": { "status": { @@ -24096,7 +24096,7 @@ { "type": "array", "items": { - "$ref": "#/components/schemas/FilePart" + "$ref": "#/components/schemas/SessionV1.FilePart" } }, { @@ -24115,7 +24115,7 @@ ], "additionalProperties": false }, - "ToolStateError": { + "SessionV1.ToolStateError": { "type": "object", "properties": { "status": { @@ -24175,23 +24175,23 @@ ], "additionalProperties": false }, - "ToolState": { + "SessionV1.ToolState": { "anyOf": [ { - "$ref": "#/components/schemas/ToolStatePending" + "$ref": "#/components/schemas/SessionV1.ToolStatePending" }, { - "$ref": "#/components/schemas/ToolStateRunning" + "$ref": "#/components/schemas/SessionV1.ToolStateRunning" }, { - "$ref": "#/components/schemas/ToolStateCompleted" + "$ref": "#/components/schemas/SessionV1.ToolStateCompleted" }, { - "$ref": "#/components/schemas/ToolStateError" + "$ref": "#/components/schemas/SessionV1.ToolStateError" } ] }, - "ToolPart": { + "SessionV1.ToolPart": { "type": "object", "properties": { "id": { @@ -24231,7 +24231,7 @@ "type": "string" }, "state": { - "$ref": "#/components/schemas/ToolState" + "$ref": "#/components/schemas/SessionV1.ToolState" }, "metadata": { "anyOf": [ @@ -24255,7 +24255,7 @@ ], "additionalProperties": false }, - "StepStartPart": { + "SessionV1.StepStartPart": { "type": "object", "properties": { "id": { @@ -24307,7 +24307,7 @@ ], "additionalProperties": false }, - "StepFinishPart": { + "SessionV1.StepFinishPart": { "type": "object", "properties": { "id": { @@ -24415,7 +24415,7 @@ ], "additionalProperties": false }, - "SnapshotPart": { + "SessionV1.SnapshotPart": { "type": "object", "properties": { "id": { @@ -24461,7 +24461,7 @@ ], "additionalProperties": false }, - "PatchPart": { + "SessionV1.PatchPart": { "type": "object", "properties": { "id": { @@ -24514,7 +24514,7 @@ ], "additionalProperties": false }, - "AgentPart": { + "SessionV1.AgentPart": { "type": "object", "properties": { "id": { @@ -24597,7 +24597,7 @@ ], "additionalProperties": false }, - "RetryPart": { + "SessionV1.RetryPart": { "type": "object", "properties": { "id": { @@ -24670,7 +24670,7 @@ ], "additionalProperties": false }, - "CompactionPart": { + "SessionV1.CompactionPart": { "type": "object", "properties": { "id": { @@ -24741,43 +24741,43 @@ ], "additionalProperties": false }, - "Part": { + "SessionV1.Part": { "anyOf": [ { - "$ref": "#/components/schemas/TextPart" + "$ref": "#/components/schemas/SessionV1.TextPart" }, { - "$ref": "#/components/schemas/SubtaskPart" + "$ref": "#/components/schemas/SessionV1.SubtaskPart" }, { - "$ref": "#/components/schemas/ReasoningPart" + "$ref": "#/components/schemas/SessionV1.ReasoningPart" }, { - "$ref": "#/components/schemas/FilePart" + "$ref": "#/components/schemas/SessionV1.FilePart" }, { - "$ref": "#/components/schemas/ToolPart" + "$ref": "#/components/schemas/SessionV1.ToolPart" }, { - "$ref": "#/components/schemas/StepStartPart" + "$ref": "#/components/schemas/SessionV1.StepStartPart" }, { - "$ref": "#/components/schemas/StepFinishPart" + "$ref": "#/components/schemas/SessionV1.StepFinishPart" }, { - "$ref": "#/components/schemas/SnapshotPart" + "$ref": "#/components/schemas/SessionV1.SnapshotPart" }, { - "$ref": "#/components/schemas/PatchPart" + "$ref": "#/components/schemas/SessionV1.PatchPart" }, { - "$ref": "#/components/schemas/AgentPart" + "$ref": "#/components/schemas/SessionV1.AgentPart" }, { - "$ref": "#/components/schemas/RetryPart" + "$ref": "#/components/schemas/SessionV1.RetryPart" }, { - "$ref": "#/components/schemas/CompactionPart" + "$ref": "#/components/schemas/SessionV1.CompactionPart" } ] }, @@ -24847,7 +24847,7 @@ ] }, "part": { - "$ref": "#/components/schemas/Part" + "$ref": "#/components/schemas/SessionV1.Part" }, "time": { "type": "number" @@ -25472,7 +25472,7 @@ ], "additionalProperties": false }, - "permission.v2.asked": { + "permission.asked": { "type": "object", "properties": { "id": { @@ -25492,7 +25492,7 @@ "type": { "type": "string", "enum": [ - "permission.v2.asked" + "permission.asked" ] }, "location": { @@ -25536,7 +25536,7 @@ "type": "object" }, "source": { - "$ref": "#/components/schemas/PermissionV2.Source" + "$ref": "#/components/schemas/Permission.Source" } }, "required": [ @@ -25556,7 +25556,7 @@ ], "additionalProperties": false }, - "permission.v2.replied": { + "permission.replied": { "type": "object", "properties": { "id": { @@ -25576,7 +25576,7 @@ "type": { "type": "string", "enum": [ - "permission.v2.replied" + "permission.replied" ] }, "location": { @@ -25602,7 +25602,7 @@ ] }, "reply": { - "$ref": "#/components/schemas/PermissionV2.Reply" + "$ref": "#/components/schemas/Permission.Reply" } }, "required": [ @@ -26327,7 +26327,7 @@ ], "additionalProperties": false }, - "QuestionV2.Option": { + "Question.Option": { "type": "object", "properties": { "label": { @@ -26345,7 +26345,7 @@ ], "additionalProperties": false }, - "QuestionV2.Info": { + "Question.Info": { "type": "object", "properties": { "question": { @@ -26359,7 +26359,7 @@ "options": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Option" + "$ref": "#/components/schemas/Question.Option" }, "description": "Available choices" }, @@ -26377,7 +26377,7 @@ ], "additionalProperties": false }, - "QuestionV2.Tool": { + "Question.Tool": { "type": "object", "properties": { "messageID": { @@ -26393,7 +26393,7 @@ ], "additionalProperties": false }, - "question.v2.asked": { + "question.asked": { "type": "object", "properties": { "id": { @@ -26413,7 +26413,7 @@ "type": { "type": "string", "enum": [ - "question.v2.asked" + "question.asked" ] }, "location": { @@ -26441,12 +26441,12 @@ "questions": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Info" + "$ref": "#/components/schemas/Question.Info" }, "description": "Questions to ask" }, "tool": { - "$ref": "#/components/schemas/QuestionV2.Tool" + "$ref": "#/components/schemas/Question.Tool" } }, "required": [ @@ -26465,13 +26465,13 @@ ], "additionalProperties": false }, - "QuestionV2.Answer": { + "Question.Answer": { "type": "array", "items": { "type": "string" } }, - "question.v2.replied": { + "question.replied": { "type": "object", "properties": { "id": { @@ -26491,7 +26491,7 @@ "type": { "type": "string", "enum": [ - "question.v2.replied" + "question.replied" ] }, "location": { @@ -26519,7 +26519,7 @@ "answers": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Answer" + "$ref": "#/components/schemas/Question.Answer" } } }, @@ -26539,7 +26539,7 @@ ], "additionalProperties": false }, - "question.v2.rejected": { + "question.rejected": { "type": "object", "properties": { "id": { @@ -26559,7 +26559,7 @@ "type": { "type": "string", "enum": [ - "question.v2.rejected" + "question.rejected" ] }, "location": { @@ -28068,483 +28068,6 @@ ], "additionalProperties": false }, - "permission.asked": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "permission.asked" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^per" - } - ] - }, - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "permission": { - "type": "string" - }, - "patterns": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "always": { - "type": "array", - "items": { - "type": "string" - } - }, - "tool": { - "anyOf": [ - { - "type": "object", - "properties": { - "messageID": { - "type": "string" - }, - "callID": { - "type": "string" - } - }, - "required": [ - "messageID", - "callID" - ], - "additionalProperties": false - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "id", - "sessionID", - "permission", - "patterns", - "metadata", - "always" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "permission.replied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "permission.replied" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "requestID": { - "type": "string", - "allOf": [ - { - "pattern": "^per" - } - ] - }, - "reply": { - "type": "string", - "enum": [ - "once", - "always", - "reject" - ] - } - }, - "required": [ - "sessionID", - "requestID", - "reply" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "QuestionOption": { - "type": "object", - "properties": { - "label": { - "type": "string", - "description": "Display text (1-5 words, concise)" - }, - "description": { - "type": "string", - "description": "Explanation of choice" - } - }, - "required": [ - "label", - "description" - ], - "additionalProperties": false - }, - "QuestionInfo": { - "type": "object", - "properties": { - "question": { - "type": "string", - "description": "Complete question" - }, - "header": { - "type": "string", - "description": "Very short label (max 30 chars)" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionOption" - }, - "description": "Available choices" - }, - "multiple": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "description": "Allow selecting multiple choices" - }, - "custom": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "description": "Allow typing a custom answer (default: true)" - } - }, - "required": [ - "question", - "header", - "options" - ], - "additionalProperties": false - }, - "QuestionTool": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg" - } - ] - }, - "callID": { - "type": "string" - } - }, - "required": [ - "messageID", - "callID" - ], - "additionalProperties": false - }, - "question.asked": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "question.asked" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^que" - } - ] - }, - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionInfo" - }, - "description": "Questions to ask" - }, - "tool": { - "anyOf": [ - { - "$ref": "#/components/schemas/QuestionTool" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "id", - "sessionID", - "questions" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "QuestionAnswer": { - "type": "array", - "items": { - "type": "string" - } - }, - "question.replied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "question.replied" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "requestID": { - "type": "string", - "allOf": [ - { - "pattern": "^que" - } - ] - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } - } - }, - "required": [ - "sessionID", - "requestID", - "answers" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "question.rejected": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "question.rejected" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "requestID": { - "type": "string", - "allOf": [ - { - "pattern": "^que" - } - ] - } - }, - "required": [ - "sessionID", - "requestID" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, "session.error": { "type": "object", "properties": { @@ -28865,10 +28388,10 @@ "$ref": "#/components/schemas/reference.updated" }, { - "$ref": "#/components/schemas/permission.v2.asked" + "$ref": "#/components/schemas/permission.asked" }, { - "$ref": "#/components/schemas/permission.v2.replied" + "$ref": "#/components/schemas/permission.replied" }, { "$ref": "#/components/schemas/plugin.added" @@ -28910,13 +28433,13 @@ "$ref": "#/components/schemas/shell.deleted" }, { - "$ref": "#/components/schemas/question.v2.asked" + "$ref": "#/components/schemas/question.asked" }, { - "$ref": "#/components/schemas/question.v2.replied" + "$ref": "#/components/schemas/question.replied" }, { - "$ref": "#/components/schemas/question.v2.rejected" + "$ref": "#/components/schemas/question.rejected" }, { "$ref": "#/components/schemas/form.created" @@ -28963,21 +28486,6 @@ { "$ref": "#/components/schemas/mcp.resources.changed" }, - { - "$ref": "#/components/schemas/permission.asked" - }, - { - "$ref": "#/components/schemas/permission.replied" - }, - { - "$ref": "#/components/schemas/question.asked" - }, - { - "$ref": "#/components/schemas/question.replied" - }, - { - "$ref": "#/components/schemas/question.rejected" - }, { "$ref": "#/components/schemas/session.error" }, @@ -29153,7 +28661,7 @@ ], "additionalProperties": false }, - "QuestionV2.Request": { + "Question.Request": { "type": "object", "properties": { "id": { @@ -29175,12 +28683,12 @@ "questions": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Info" + "$ref": "#/components/schemas/Question.Info" }, "description": "Questions to ask" }, "tool": { - "$ref": "#/components/schemas/QuestionV2.Tool" + "$ref": "#/components/schemas/Question.Tool" } }, "required": [ @@ -29190,13 +28698,13 @@ ], "additionalProperties": false }, - "QuestionV2.Reply": { + "Question.Reply": { "type": "object", "properties": { "answers": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Answer" + "$ref": "#/components/schemas/Question.Answer" }, "description": "User answers in order of questions (each answer is an array of selected labels)" } diff --git a/packages/www/public/openapi.json b/packages/www/public/openapi.json index 1c8b3dc9412d..0508db242812 100644 --- a/packages/www/public/openapi.json +++ b/packages/www/public/openapi.json @@ -4279,7 +4279,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/ProviderV2.Info" + "$ref": "#/components/schemas/Provider.Info" } } }, @@ -4396,7 +4396,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/ProviderV2.Info" + "$ref": "#/components/schemas/Provider.Info" } }, "required": [ @@ -7466,7 +7466,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionV2.Request" + "$ref": "#/components/schemas/Permission.Request" } } }, @@ -7664,7 +7664,7 @@ ] }, "effect": { - "$ref": "#/components/schemas/PermissionV2.Effect" + "$ref": "#/components/schemas/Permission.Effect" } }, "required": [ @@ -7762,7 +7762,7 @@ "type": "object" }, "source": { - "$ref": "#/components/schemas/PermissionV2.Source" + "$ref": "#/components/schemas/Permission.Source" }, "agent": { "anyOf": [ @@ -7818,7 +7818,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionV2.Request" + "$ref": "#/components/schemas/Permission.Request" } } }, @@ -7916,7 +7916,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/PermissionV2.Request" + "$ref": "#/components/schemas/Permission.Request" } }, "required": [ @@ -8061,7 +8061,7 @@ "type": "object", "properties": { "reply": { - "$ref": "#/components/schemas/PermissionV2.Reply" + "$ref": "#/components/schemas/Permission.Reply" }, "message": { "anyOf": [ @@ -10456,7 +10456,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Request" + "$ref": "#/components/schemas/Question.Request" } } }, @@ -10527,7 +10527,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Request" + "$ref": "#/components/schemas/Question.Request" } } }, @@ -10667,7 +10667,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QuestionV2.Reply" + "$ref": "#/components/schemas/Question.Reply" } } }, @@ -11981,7 +11981,7 @@ "Agent.Color": { "type": "string" }, - "PermissionV2.Effect": { + "Permission.Effect": { "type": "string", "enum": [ "allow", @@ -11989,7 +11989,7 @@ "ask" ] }, - "PermissionV2.Rule": { + "Permission.Rule": { "type": "object", "properties": { "action": { @@ -11999,7 +11999,7 @@ "type": "string" }, "effect": { - "$ref": "#/components/schemas/PermissionV2.Effect" + "$ref": "#/components/schemas/Permission.Effect" } }, "required": [ @@ -12009,10 +12009,10 @@ ], "additionalProperties": false }, - "PermissionV2.Ruleset": { + "Permission.Ruleset": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionV2.Rule" + "$ref": "#/components/schemas/Permission.Rule" } }, "Agent.Info": { @@ -12059,7 +12059,7 @@ ] }, "permissions": { - "$ref": "#/components/schemas/PermissionV2.Ruleset" + "$ref": "#/components/schemas/Permission.Ruleset" } }, "required": [ @@ -18537,7 +18537,7 @@ ], "additionalProperties": false }, - "ProviderV2.Info": { + "Provider.Info": { "type": "object", "properties": { "id": { @@ -21466,7 +21466,7 @@ ], "additionalProperties": false }, - "PermissionV2.Source": { + "Permission.Source": { "anyOf": [ { "type": "object", @@ -21493,7 +21493,7 @@ } ] }, - "PermissionV2.Request": { + "Permission.Request": { "type": "object", "properties": { "id": { @@ -21531,7 +21531,7 @@ "type": "object" }, "source": { - "$ref": "#/components/schemas/PermissionV2.Source" + "$ref": "#/components/schemas/Permission.Source" } }, "required": [ @@ -21589,7 +21589,7 @@ ], "additionalProperties": false }, - "PermissionV2.Reply": { + "Permission.Reply": { "type": "string", "enum": [ "once", @@ -21935,7 +21935,7 @@ ], "additionalProperties": false }, - "PermissionAction": { + "PermissionV1.Action": { "type": "string", "enum": [ "allow", @@ -21943,7 +21943,7 @@ "ask" ] }, - "PermissionRule": { + "PermissionV1.Rule": { "type": "object", "properties": { "permission": { @@ -21953,7 +21953,7 @@ "type": "string" }, "action": { - "$ref": "#/components/schemas/PermissionAction" + "$ref": "#/components/schemas/PermissionV1.Action" } }, "required": [ @@ -21963,10 +21963,10 @@ ], "additionalProperties": false }, - "PermissionRuleset": { + "PermissionV1.Ruleset": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionRule" + "$ref": "#/components/schemas/PermissionV1.Rule" } }, "SessionV1.Info": { @@ -22155,7 +22155,7 @@ "additionalProperties": false }, "permission": { - "$ref": "#/components/schemas/PermissionRuleset" + "$ref": "#/components/schemas/PermissionV1.Ruleset" }, "revert": { "type": "object", @@ -22455,10 +22455,10 @@ ], "additionalProperties": false }, - "JSONSchema": { + "SessionV1.JSONSchema": { "type": "object" }, - "OutputFormat": { + "SessionV1.OutputFormat": { "anyOf": [ { "type": "object", @@ -22485,7 +22485,7 @@ ] }, "schema": { - "$ref": "#/components/schemas/JSONSchema" + "$ref": "#/components/schemas/SessionV1.JSONSchema" }, "retryCount": { "anyOf": [ @@ -22518,7 +22518,7 @@ } ] }, - "UserMessage": { + "SessionV1.UserMessage": { "type": "object", "properties": { "id": { @@ -22563,7 +22563,7 @@ "format": { "anyOf": [ { - "$ref": "#/components/schemas/OutputFormat" + "$ref": "#/components/schemas/SessionV1.OutputFormat" }, { "type": "null" @@ -22985,7 +22985,7 @@ ], "additionalProperties": false }, - "AssistantMessage": { + "SessionV1.AssistantMessage": { "type": "object", "properties": { "id": { @@ -23218,13 +23218,13 @@ ], "additionalProperties": false }, - "Message": { + "SessionV1.Message": { "anyOf": [ { - "$ref": "#/components/schemas/UserMessage" + "$ref": "#/components/schemas/SessionV1.UserMessage" }, { - "$ref": "#/components/schemas/AssistantMessage" + "$ref": "#/components/schemas/SessionV1.AssistantMessage" } ] }, @@ -23294,7 +23294,7 @@ ] }, "info": { - "$ref": "#/components/schemas/Message" + "$ref": "#/components/schemas/SessionV1.Message" } }, "required": [ @@ -23403,7 +23403,7 @@ ], "additionalProperties": false }, - "TextPart": { + "SessionV1.TextPart": { "type": "object", "properties": { "id": { @@ -23518,7 +23518,7 @@ ], "additionalProperties": false }, - "SubtaskPart": { + "SessionV1.SubtaskPart": { "type": "object", "properties": { "id": { @@ -23605,7 +23605,7 @@ ], "additionalProperties": false }, - "ReasoningPart": { + "SessionV1.ReasoningPart": { "type": "object", "properties": { "id": { @@ -23694,7 +23694,7 @@ ], "additionalProperties": false }, - "FilePartSourceText": { + "SessionV1.FilePartSourceText": { "type": "object", "properties": { "value": { @@ -23714,11 +23714,11 @@ ], "additionalProperties": false }, - "FileSource": { + "SessionV1.FileSource": { "type": "object", "properties": { "text": { - "$ref": "#/components/schemas/FilePartSourceText" + "$ref": "#/components/schemas/SessionV1.FilePartSourceText" }, "type": { "type": "string", @@ -23737,7 +23737,7 @@ ], "additionalProperties": false }, - "Range": { + "SessionV1.Range": { "type": "object", "properties": { "start": { @@ -23799,11 +23799,11 @@ ], "additionalProperties": false }, - "SymbolSource": { + "SessionV1.SymbolSource": { "type": "object", "properties": { "text": { - "$ref": "#/components/schemas/FilePartSourceText" + "$ref": "#/components/schemas/SessionV1.FilePartSourceText" }, "type": { "type": "string", @@ -23815,7 +23815,7 @@ "type": "string" }, "range": { - "$ref": "#/components/schemas/Range" + "$ref": "#/components/schemas/SessionV1.Range" }, "name": { "type": "string" @@ -23839,11 +23839,11 @@ ], "additionalProperties": false }, - "ResourceSource": { + "SessionV1.ResourceSource": { "type": "object", "properties": { "text": { - "$ref": "#/components/schemas/FilePartSourceText" + "$ref": "#/components/schemas/SessionV1.FilePartSourceText" }, "type": { "type": "string", @@ -23866,20 +23866,20 @@ ], "additionalProperties": false }, - "FilePartSource": { + "SessionV1.FilePartSource": { "anyOf": [ { - "$ref": "#/components/schemas/FileSource" + "$ref": "#/components/schemas/SessionV1.FileSource" }, { - "$ref": "#/components/schemas/SymbolSource" + "$ref": "#/components/schemas/SessionV1.SymbolSource" }, { - "$ref": "#/components/schemas/ResourceSource" + "$ref": "#/components/schemas/SessionV1.ResourceSource" } ] }, - "FilePart": { + "SessionV1.FilePart": { "type": "object", "properties": { "id": { @@ -23931,7 +23931,7 @@ "source": { "anyOf": [ { - "$ref": "#/components/schemas/FilePartSource" + "$ref": "#/components/schemas/SessionV1.FilePartSource" }, { "type": "null" @@ -23949,7 +23949,7 @@ ], "additionalProperties": false }, - "ToolStatePending": { + "SessionV1.ToolStatePending": { "type": "object", "properties": { "status": { @@ -23972,7 +23972,7 @@ ], "additionalProperties": false }, - "ToolStateRunning": { + "SessionV1.ToolStateRunning": { "type": "object", "properties": { "status": { @@ -24029,7 +24029,7 @@ ], "additionalProperties": false }, - "ToolStateCompleted": { + "SessionV1.ToolStateCompleted": { "type": "object", "properties": { "status": { @@ -24096,7 +24096,7 @@ { "type": "array", "items": { - "$ref": "#/components/schemas/FilePart" + "$ref": "#/components/schemas/SessionV1.FilePart" } }, { @@ -24115,7 +24115,7 @@ ], "additionalProperties": false }, - "ToolStateError": { + "SessionV1.ToolStateError": { "type": "object", "properties": { "status": { @@ -24175,23 +24175,23 @@ ], "additionalProperties": false }, - "ToolState": { + "SessionV1.ToolState": { "anyOf": [ { - "$ref": "#/components/schemas/ToolStatePending" + "$ref": "#/components/schemas/SessionV1.ToolStatePending" }, { - "$ref": "#/components/schemas/ToolStateRunning" + "$ref": "#/components/schemas/SessionV1.ToolStateRunning" }, { - "$ref": "#/components/schemas/ToolStateCompleted" + "$ref": "#/components/schemas/SessionV1.ToolStateCompleted" }, { - "$ref": "#/components/schemas/ToolStateError" + "$ref": "#/components/schemas/SessionV1.ToolStateError" } ] }, - "ToolPart": { + "SessionV1.ToolPart": { "type": "object", "properties": { "id": { @@ -24231,7 +24231,7 @@ "type": "string" }, "state": { - "$ref": "#/components/schemas/ToolState" + "$ref": "#/components/schemas/SessionV1.ToolState" }, "metadata": { "anyOf": [ @@ -24255,7 +24255,7 @@ ], "additionalProperties": false }, - "StepStartPart": { + "SessionV1.StepStartPart": { "type": "object", "properties": { "id": { @@ -24307,7 +24307,7 @@ ], "additionalProperties": false }, - "StepFinishPart": { + "SessionV1.StepFinishPart": { "type": "object", "properties": { "id": { @@ -24415,7 +24415,7 @@ ], "additionalProperties": false }, - "SnapshotPart": { + "SessionV1.SnapshotPart": { "type": "object", "properties": { "id": { @@ -24461,7 +24461,7 @@ ], "additionalProperties": false }, - "PatchPart": { + "SessionV1.PatchPart": { "type": "object", "properties": { "id": { @@ -24514,7 +24514,7 @@ ], "additionalProperties": false }, - "AgentPart": { + "SessionV1.AgentPart": { "type": "object", "properties": { "id": { @@ -24597,7 +24597,7 @@ ], "additionalProperties": false }, - "RetryPart": { + "SessionV1.RetryPart": { "type": "object", "properties": { "id": { @@ -24670,7 +24670,7 @@ ], "additionalProperties": false }, - "CompactionPart": { + "SessionV1.CompactionPart": { "type": "object", "properties": { "id": { @@ -24741,43 +24741,43 @@ ], "additionalProperties": false }, - "Part": { + "SessionV1.Part": { "anyOf": [ { - "$ref": "#/components/schemas/TextPart" + "$ref": "#/components/schemas/SessionV1.TextPart" }, { - "$ref": "#/components/schemas/SubtaskPart" + "$ref": "#/components/schemas/SessionV1.SubtaskPart" }, { - "$ref": "#/components/schemas/ReasoningPart" + "$ref": "#/components/schemas/SessionV1.ReasoningPart" }, { - "$ref": "#/components/schemas/FilePart" + "$ref": "#/components/schemas/SessionV1.FilePart" }, { - "$ref": "#/components/schemas/ToolPart" + "$ref": "#/components/schemas/SessionV1.ToolPart" }, { - "$ref": "#/components/schemas/StepStartPart" + "$ref": "#/components/schemas/SessionV1.StepStartPart" }, { - "$ref": "#/components/schemas/StepFinishPart" + "$ref": "#/components/schemas/SessionV1.StepFinishPart" }, { - "$ref": "#/components/schemas/SnapshotPart" + "$ref": "#/components/schemas/SessionV1.SnapshotPart" }, { - "$ref": "#/components/schemas/PatchPart" + "$ref": "#/components/schemas/SessionV1.PatchPart" }, { - "$ref": "#/components/schemas/AgentPart" + "$ref": "#/components/schemas/SessionV1.AgentPart" }, { - "$ref": "#/components/schemas/RetryPart" + "$ref": "#/components/schemas/SessionV1.RetryPart" }, { - "$ref": "#/components/schemas/CompactionPart" + "$ref": "#/components/schemas/SessionV1.CompactionPart" } ] }, @@ -24847,7 +24847,7 @@ ] }, "part": { - "$ref": "#/components/schemas/Part" + "$ref": "#/components/schemas/SessionV1.Part" }, "time": { "type": "number" @@ -25472,7 +25472,7 @@ ], "additionalProperties": false }, - "permission.v2.asked": { + "permission.asked": { "type": "object", "properties": { "id": { @@ -25492,7 +25492,7 @@ "type": { "type": "string", "enum": [ - "permission.v2.asked" + "permission.asked" ] }, "location": { @@ -25536,7 +25536,7 @@ "type": "object" }, "source": { - "$ref": "#/components/schemas/PermissionV2.Source" + "$ref": "#/components/schemas/Permission.Source" } }, "required": [ @@ -25556,7 +25556,7 @@ ], "additionalProperties": false }, - "permission.v2.replied": { + "permission.replied": { "type": "object", "properties": { "id": { @@ -25576,7 +25576,7 @@ "type": { "type": "string", "enum": [ - "permission.v2.replied" + "permission.replied" ] }, "location": { @@ -25602,7 +25602,7 @@ ] }, "reply": { - "$ref": "#/components/schemas/PermissionV2.Reply" + "$ref": "#/components/schemas/Permission.Reply" } }, "required": [ @@ -26327,7 +26327,7 @@ ], "additionalProperties": false }, - "QuestionV2.Option": { + "Question.Option": { "type": "object", "properties": { "label": { @@ -26345,7 +26345,7 @@ ], "additionalProperties": false }, - "QuestionV2.Info": { + "Question.Info": { "type": "object", "properties": { "question": { @@ -26359,7 +26359,7 @@ "options": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Option" + "$ref": "#/components/schemas/Question.Option" }, "description": "Available choices" }, @@ -26377,7 +26377,7 @@ ], "additionalProperties": false }, - "QuestionV2.Tool": { + "Question.Tool": { "type": "object", "properties": { "messageID": { @@ -26393,7 +26393,7 @@ ], "additionalProperties": false }, - "question.v2.asked": { + "question.asked": { "type": "object", "properties": { "id": { @@ -26413,7 +26413,7 @@ "type": { "type": "string", "enum": [ - "question.v2.asked" + "question.asked" ] }, "location": { @@ -26441,12 +26441,12 @@ "questions": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Info" + "$ref": "#/components/schemas/Question.Info" }, "description": "Questions to ask" }, "tool": { - "$ref": "#/components/schemas/QuestionV2.Tool" + "$ref": "#/components/schemas/Question.Tool" } }, "required": [ @@ -26465,13 +26465,13 @@ ], "additionalProperties": false }, - "QuestionV2.Answer": { + "Question.Answer": { "type": "array", "items": { "type": "string" } }, - "question.v2.replied": { + "question.replied": { "type": "object", "properties": { "id": { @@ -26491,7 +26491,7 @@ "type": { "type": "string", "enum": [ - "question.v2.replied" + "question.replied" ] }, "location": { @@ -26519,7 +26519,7 @@ "answers": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Answer" + "$ref": "#/components/schemas/Question.Answer" } } }, @@ -26539,7 +26539,7 @@ ], "additionalProperties": false }, - "question.v2.rejected": { + "question.rejected": { "type": "object", "properties": { "id": { @@ -26559,7 +26559,7 @@ "type": { "type": "string", "enum": [ - "question.v2.rejected" + "question.rejected" ] }, "location": { @@ -28068,483 +28068,6 @@ ], "additionalProperties": false }, - "permission.asked": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "permission.asked" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^per" - } - ] - }, - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "permission": { - "type": "string" - }, - "patterns": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "always": { - "type": "array", - "items": { - "type": "string" - } - }, - "tool": { - "anyOf": [ - { - "type": "object", - "properties": { - "messageID": { - "type": "string" - }, - "callID": { - "type": "string" - } - }, - "required": [ - "messageID", - "callID" - ], - "additionalProperties": false - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "id", - "sessionID", - "permission", - "patterns", - "metadata", - "always" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "permission.replied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "permission.replied" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "requestID": { - "type": "string", - "allOf": [ - { - "pattern": "^per" - } - ] - }, - "reply": { - "type": "string", - "enum": [ - "once", - "always", - "reject" - ] - } - }, - "required": [ - "sessionID", - "requestID", - "reply" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "QuestionOption": { - "type": "object", - "properties": { - "label": { - "type": "string", - "description": "Display text (1-5 words, concise)" - }, - "description": { - "type": "string", - "description": "Explanation of choice" - } - }, - "required": [ - "label", - "description" - ], - "additionalProperties": false - }, - "QuestionInfo": { - "type": "object", - "properties": { - "question": { - "type": "string", - "description": "Complete question" - }, - "header": { - "type": "string", - "description": "Very short label (max 30 chars)" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionOption" - }, - "description": "Available choices" - }, - "multiple": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "description": "Allow selecting multiple choices" - }, - "custom": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "description": "Allow typing a custom answer (default: true)" - } - }, - "required": [ - "question", - "header", - "options" - ], - "additionalProperties": false - }, - "QuestionTool": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg" - } - ] - }, - "callID": { - "type": "string" - } - }, - "required": [ - "messageID", - "callID" - ], - "additionalProperties": false - }, - "question.asked": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "question.asked" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^que" - } - ] - }, - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionInfo" - }, - "description": "Questions to ask" - }, - "tool": { - "anyOf": [ - { - "$ref": "#/components/schemas/QuestionTool" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "id", - "sessionID", - "questions" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "QuestionAnswer": { - "type": "array", - "items": { - "type": "string" - } - }, - "question.replied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "question.replied" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "requestID": { - "type": "string", - "allOf": [ - { - "pattern": "^que" - } - ] - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } - } - }, - "required": [ - "sessionID", - "requestID", - "answers" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "question.rejected": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "question.rejected" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "requestID": { - "type": "string", - "allOf": [ - { - "pattern": "^que" - } - ] - } - }, - "required": [ - "sessionID", - "requestID" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, "session.error": { "type": "object", "properties": { @@ -28865,10 +28388,10 @@ "$ref": "#/components/schemas/reference.updated" }, { - "$ref": "#/components/schemas/permission.v2.asked" + "$ref": "#/components/schemas/permission.asked" }, { - "$ref": "#/components/schemas/permission.v2.replied" + "$ref": "#/components/schemas/permission.replied" }, { "$ref": "#/components/schemas/plugin.added" @@ -28910,13 +28433,13 @@ "$ref": "#/components/schemas/shell.deleted" }, { - "$ref": "#/components/schemas/question.v2.asked" + "$ref": "#/components/schemas/question.asked" }, { - "$ref": "#/components/schemas/question.v2.replied" + "$ref": "#/components/schemas/question.replied" }, { - "$ref": "#/components/schemas/question.v2.rejected" + "$ref": "#/components/schemas/question.rejected" }, { "$ref": "#/components/schemas/form.created" @@ -28963,21 +28486,6 @@ { "$ref": "#/components/schemas/mcp.resources.changed" }, - { - "$ref": "#/components/schemas/permission.asked" - }, - { - "$ref": "#/components/schemas/permission.replied" - }, - { - "$ref": "#/components/schemas/question.asked" - }, - { - "$ref": "#/components/schemas/question.replied" - }, - { - "$ref": "#/components/schemas/question.rejected" - }, { "$ref": "#/components/schemas/session.error" }, @@ -29153,7 +28661,7 @@ ], "additionalProperties": false }, - "QuestionV2.Request": { + "Question.Request": { "type": "object", "properties": { "id": { @@ -29175,12 +28683,12 @@ "questions": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Info" + "$ref": "#/components/schemas/Question.Info" }, "description": "Questions to ask" }, "tool": { - "$ref": "#/components/schemas/QuestionV2.Tool" + "$ref": "#/components/schemas/Question.Tool" } }, "required": [ @@ -29190,13 +28698,13 @@ ], "additionalProperties": false }, - "QuestionV2.Reply": { + "Question.Reply": { "type": "object", "properties": { "answers": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Answer" + "$ref": "#/components/schemas/Question.Answer" }, "description": "User answers in order of questions (each answer is an array of selected labels)" } diff --git a/patches/effect@4.0.0-beta.98.patch b/patches/effect@4.0.0-beta.101.patch similarity index 100% rename from patches/effect@4.0.0-beta.98.patch rename to patches/effect@4.0.0-beta.101.patch From 5592f5225b52aaa85f6e6129944e142e8d970f02 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 26 Jul 2026 20:15:43 -0400 Subject: [PATCH 128/150] fix(app): update remote sdk contracts --- .../app/src/context/global-sync/event-reducer.ts | 4 ++-- .../src/context/global-sync/session-cache.test.ts | 6 +++--- .../app/src/context/global-sync/session-cache.ts | 4 ++-- packages/app/src/context/global-sync/types.ts | 4 ++-- packages/app/src/context/server-sdk.tsx | 14 +++++++++++--- packages/app/src/context/server-session.ts | 6 +++--- packages/app/src/pages/session.tsx | 9 ++------- packages/app/src/pages/session/review-tab.tsx | 5 +++-- .../app/src/pages/session/session-side-panel.tsx | 5 +++-- .../app/src/pages/session/v2/review-diff-kinds.ts | 5 +++-- .../app/src/pages/session/v2/review-panel-v2.tsx | 5 +++-- packages/app/src/utils/diffs.test.ts | 4 ++-- packages/app/src/utils/diffs.ts | 8 ++++---- packages/enterprise/src/core/share.ts | 6 ++++-- packages/enterprise/src/routes/share/[shareID].tsx | 4 ++-- packages/session-ui/src/components/session-diff.ts | 4 ++-- .../session-ui/src/components/session-review.tsx | 8 +++++--- .../session-ui/src/components/session-turn.tsx | 4 ++-- packages/session-ui/src/context/data.tsx | 4 ++-- .../components/session-review-file-preview-v2.tsx | 4 ++-- 20 files changed, 62 insertions(+), 51 deletions(-) diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts index 935fc14e3367..bd1cd4788ade 100644 --- a/packages/app/src/context/global-sync/event-reducer.ts +++ b/packages/app/src/context/global-sync/event-reducer.ts @@ -8,8 +8,8 @@ import type { QuestionRequest, Session, SessionStatus, - FileDiffInfo, } from "@opencode-ai/sdk/v2/client" +import type { SessionDiff } from "@/utils/diffs" import type { State, VcsCache } from "./types" import { trimSessions } from "./session-trim" import { dropSessionCaches } from "./session-cache" @@ -171,7 +171,7 @@ export function applyDirectoryEvent(input: { break } case "session.diff": { - const props = event.properties as { sessionID: string; diff: FileDiffInfo[] } + const props = event.properties as { sessionID: string; diff: SessionDiff[] } input.setStore("session_diff", props.sessionID, reconcile(list(props.diff), { key: "file" })) break } diff --git a/packages/app/src/context/global-sync/session-cache.test.ts b/packages/app/src/context/global-sync/session-cache.test.ts index 41034186e6fb..fbe1ccf1b1aa 100644 --- a/packages/app/src/context/global-sync/session-cache.test.ts +++ b/packages/app/src/context/global-sync/session-cache.test.ts @@ -5,8 +5,8 @@ import type { PermissionRequest, QuestionRequest, SessionStatus, - FileDiffInfo, } from "@opencode-ai/sdk/v2/client" +import type { SessionDiff } from "@/utils/diffs" import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache" const msg = (id: string, sessionID: string) => @@ -32,7 +32,7 @@ describe("app session cache", () => { test("dropSessionCaches clears orphaned parts without message rows", () => { const store: { session_status: Record - session_diff: Record + session_diff: Record message: Record part: Record permission: Record @@ -63,7 +63,7 @@ describe("app session cache", () => { const m = msg("msg_1", "ses_1") const store: { session_status: Record - session_diff: Record + session_diff: Record message: Record part: Record permission: Record diff --git a/packages/app/src/context/global-sync/session-cache.ts b/packages/app/src/context/global-sync/session-cache.ts index 6f2c3dc540bb..4255b5586d7b 100644 --- a/packages/app/src/context/global-sync/session-cache.ts +++ b/packages/app/src/context/global-sync/session-cache.ts @@ -4,14 +4,14 @@ import type { PermissionRequest, QuestionRequest, SessionStatus, - FileDiffInfo, } from "@opencode-ai/sdk/v2/client" +import type { SessionDiff } from "@/utils/diffs" export const SESSION_CACHE_LIMIT = 40 type SessionCache = { session_status: Record - session_diff: Record + session_diff: Record message: Record part: Record permission: Record diff --git a/packages/app/src/context/global-sync/types.ts b/packages/app/src/context/global-sync/types.ts index 53191b756dec..698942efd922 100644 --- a/packages/app/src/context/global-sync/types.ts +++ b/packages/app/src/context/global-sync/types.ts @@ -13,9 +13,9 @@ import type { ReferenceInfo, Session, SessionStatus, - FileDiffInfo, VcsInfo, } from "@opencode-ai/sdk/v2/client" +import type { SessionDiff } from "@/utils/diffs" import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" import type { Accessor } from "solid-js" import type { SetStoreFunction, Store } from "solid-js/store" @@ -50,7 +50,7 @@ export type State = { } session_working(id: string): boolean session_diff: { - [sessionID: string]: FileDiffInfo[] + [sessionID: string]: SessionDiff[] } permission: { [sessionID: string]: PermissionRequest[] diff --git a/packages/app/src/context/server-sdk.tsx b/packages/app/src/context/server-sdk.tsx index 4606363b7eda..b7fd0410dc07 100644 --- a/packages/app/src/context/server-sdk.tsx +++ b/packages/app/src/context/server-sdk.tsx @@ -1,5 +1,5 @@ import type { OpenCodeEvent } from "@opencode-ai/client/promise" -import type { Event } from "@opencode-ai/sdk/v2/client" +import type { Event, PermissionRequest } from "@opencode-ai/sdk/v2/client" import { createSimpleContext } from "@opencode-ai/ui/context" import { createGlobalEmitter } from "@solid-primitives/event-bus" import { makeEventListener } from "@solid-primitives/event-listener" @@ -18,7 +18,15 @@ const isAbortError = (error: unknown) => error !== null && typeof error === "object" && "name" in error && error.name === "AbortError" const isStreamClosed = (error: unknown, signal?: AbortSignal) => isAbortError(error) || signal?.aborted === true -export type ServerEvent = Event & { current?: OpenCodeEvent } +type PermissionEvent = { + id: string + type: "permission.v2.asked" + properties: PermissionRequest + current?: OpenCodeEvent +} +export type ServerEvent = (Event | PermissionEvent) & { + current?: OpenCodeEvent +} type QueuedServerEvent = { directory: string; payload: ServerEvent } type CurrentDelta = Extract< OpenCodeEvent, @@ -43,7 +51,7 @@ export function adaptServerEvent(event: OpenCodeEvent): ServerEvent { : undefined, }, current: event, - } as ServerEvent + } } if (event.type === "permission.v2.replied") return { id: event.id, type: "permission.v2.replied", properties: event.data, current: event } as ServerEvent diff --git a/packages/app/src/context/server-session.ts b/packages/app/src/context/server-session.ts index 623c95eb4dad..b2db827620a4 100644 --- a/packages/app/src/context/server-session.ts +++ b/packages/app/src/context/server-session.ts @@ -8,8 +8,8 @@ import type { QuestionRequest, Session, SessionStatus, - FileDiffInfo, } from "@opencode-ai/sdk/v2/client" +import type { SessionDiff } from "@/utils/diffs" import { batch } from "solid-js" import { createStore, produce, reconcile } from "solid-js/store" import { diffs as cleanDiffs, message as cleanMessage } from "@/utils/diffs" @@ -140,7 +140,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?: const [data, setData] = createStore({ info: {} as Record, session_status: {} as Record, - session_diff: {} as Record, + session_diff: {} as Record, permission: {} as Record, question: {} as Record, message: {} as Record, @@ -773,7 +773,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?: return } case "session.diff": { - const props = event.properties as { sessionID: string; diff: FileDiffInfo[] } + const props = event.properties as { sessionID: string; diff: SessionDiff[] } setData("session_diff", props.sessionID, reconcile(cleanDiffs(props.diff), { key: "file" })) return } diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 2fade924c415..f7fe46922e2a 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -909,13 +909,8 @@ export default function Page() { ) const stopVcs = sdk().event.listen((evt) => { - if (evt.details.type !== "filesystem.changed") return - const props = - typeof evt.details.properties === "object" && evt.details.properties - ? (evt.details.properties as Record) - : undefined - const file = typeof props?.file === "string" ? props.file : undefined - if (!file || file.startsWith(".git/")) return + if (evt.details.type !== "file.watcher.updated") return + if (evt.details.properties.file.startsWith(".git/")) return refreshVcs() }) onCleanup(stopVcs) diff --git a/packages/app/src/pages/session/review-tab.tsx b/packages/app/src/pages/session/review-tab.tsx index 586942399d88..65b0ebdfb6d5 100644 --- a/packages/app/src/pages/session/review-tab.tsx +++ b/packages/app/src/pages/session/review-tab.tsx @@ -1,6 +1,7 @@ import { createEffect, onCleanup, type JSX } from "solid-js" import { makeEventListener } from "@solid-primitives/event-listener" -import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { SessionDiff } from "@/utils/diffs" import { SessionReview } from "@opencode-ai/session-ui/session-review" import type { SessionReviewCommentActions, @@ -14,7 +15,7 @@ import type { LineComment } from "@/context/comments" export type DiffStyle = "unified" | "split" -type ReviewDiff = FileDiffInfo | VcsFileDiff +type ReviewDiff = SessionDiff | VcsFileDiff export interface SessionReviewTabProps { title?: JSX.Element diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 7bba5c803a34..cd20f56bbc92 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -23,7 +23,8 @@ import { Mark } from "@opencode-ai/ui/logo" import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" -import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { SessionDiff } from "@/utils/diffs" import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" import { useDialog } from "@opencode-ai/ui/context/dialog" @@ -59,7 +60,7 @@ import type { RenderDiff } from "@/pages/session/v2/review-diff-kinds" export function SessionSidePanel(props: { canReview: () => boolean - diffs: () => (FileDiffInfo | VcsFileDiff)[] + diffs: () => (SessionDiff | VcsFileDiff)[] diffsReady: () => boolean empty: () => string hasReview: () => boolean diff --git a/packages/app/src/pages/session/v2/review-diff-kinds.ts b/packages/app/src/pages/session/v2/review-diff-kinds.ts index 8b252b025867..2e68d9f3dd23 100644 --- a/packages/app/src/pages/session/v2/review-diff-kinds.ts +++ b/packages/app/src/pages/session/v2/review-diff-kinds.ts @@ -1,8 +1,9 @@ -import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { SessionDiff } from "@/utils/diffs" import type { Kind } from "@/components/file-tree-v2" import { normalizeFileTreeV2Path } from "@/components/file-tree-v2-model" -export type RenderDiff = FileDiffInfo | VcsFileDiff +export type RenderDiff = SessionDiff | VcsFileDiff export function normalizePath(p: string) { return normalizeFileTreeV2Path(p) diff --git a/packages/app/src/pages/session/v2/review-panel-v2.tsx b/packages/app/src/pages/session/v2/review-panel-v2.tsx index bcf53c1d7070..8aae1ba77618 100644 --- a/packages/app/src/pages/session/v2/review-panel-v2.tsx +++ b/packages/app/src/pages/session/v2/review-panel-v2.tsx @@ -1,5 +1,6 @@ import { createMemo, createResource, createSignal, Show, type JSX } from "solid-js" -import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { SessionDiff } from "@/utils/diffs" import { SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX, SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN, @@ -29,7 +30,7 @@ import { import type { ReviewPanelV2State } from "@/pages/session/v2/review-panel-v2-state" import { applyFileListKeyDown, SessionFileListV2 } from "@/pages/session/v2/session-file-list-v2" -type ReviewDiff = FileDiffInfo | VcsFileDiff +type ReviewDiff = SessionDiff | VcsFileDiff export type ReviewPanelV2Props = { title?: JSX.Element diff --git a/packages/app/src/utils/diffs.test.ts b/packages/app/src/utils/diffs.test.ts index f6d768e1de2d..5fbca469b713 100644 --- a/packages/app/src/utils/diffs.test.ts +++ b/packages/app/src/utils/diffs.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import type { FileDiffInfo } from "@opencode-ai/sdk/v2" +import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2" import type { Message } from "@opencode-ai/sdk/v2/client" import { diffs, message } from "./diffs" @@ -9,7 +9,7 @@ const item = { additions: 1, deletions: 1, status: "modified", -} satisfies FileDiffInfo +} satisfies SnapshotFileDiff describe("diffs", () => { test("keeps valid arrays", () => { diff --git a/packages/app/src/utils/diffs.ts b/packages/app/src/utils/diffs.ts index 60df039410bb..b477a33f12ae 100644 --- a/packages/app/src/utils/diffs.ts +++ b/packages/app/src/utils/diffs.ts @@ -1,9 +1,9 @@ -import type { FileDiffInfo } from "@opencode-ai/sdk/v2" +import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2" import type { Message } from "@opencode-ai/sdk/v2/client" -type Diff = FileDiffInfo +export type SessionDiff = SnapshotFileDiff & { file: string; patch: string } -function diff(value: unknown): value is Diff { +function diff(value: unknown): value is SessionDiff { if (!value || typeof value !== "object" || Array.isArray(value)) return false if (!("file" in value) || typeof value.file !== "string") return false if (!("patch" in value) || typeof value.patch !== "string") return false @@ -17,7 +17,7 @@ function object(value: unknown): value is Record { return !!value && typeof value === "object" && !Array.isArray(value) } -export function diffs(value: unknown): Diff[] { +export function diffs(value: unknown): SessionDiff[] { if (Array.isArray(value) && value.every(diff)) return value if (Array.isArray(value)) return value.filter(diff) if (diff(value)) return [value] diff --git a/packages/enterprise/src/core/share.ts b/packages/enterprise/src/core/share.ts index ce429323d88f..0232feff6467 100644 --- a/packages/enterprise/src/core/share.ts +++ b/packages/enterprise/src/core/share.ts @@ -1,4 +1,4 @@ -import { FileDiffInfo, Message, Model, Part, Session } from "@opencode-ai/sdk/v2" +import { Message, Model, Part, Session, SnapshotFileDiff } from "@opencode-ai/sdk/v2" import { iife } from "@opencode-ai/core/util/iife" import z from "zod" import { Storage } from "./storage" @@ -8,6 +8,8 @@ function fn(schema: T, cb: (input: z.infer) => R } export namespace Share { + export type SessionDiff = SnapshotFileDiff & { file: string; patch: string } + export const Info = z.object({ id: z.string(), secret: z.string(), @@ -30,7 +32,7 @@ export namespace Share { }), z.object({ type: z.literal("session_diff"), - data: z.custom(), + data: z.custom(), }), z.object({ type: z.literal("model"), diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx index 91cb50891def..dea83faed16a 100644 --- a/packages/enterprise/src/routes/share/[shareID].tsx +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -1,4 +1,4 @@ -import { FileDiffInfo, Message, Model, Part, Session, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2" +import { Message, Model, Part, Session, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2" import { SessionTurn } from "@opencode-ai/session-ui/session-turn" import { SessionReview } from "@opencode-ai/session-ui/session-review" import { DataProvider } from "@opencode-ai/session-ui/context" @@ -65,7 +65,7 @@ const getData = query(async (shareID) => { shareID: string session: Session[] session_diff: { - [sessionID: string]: FileDiffInfo[] + [sessionID: string]: Share.SessionDiff[] } session_status: { [sessionID: string]: SessionStatus diff --git a/packages/session-ui/src/components/session-diff.ts b/packages/session-ui/src/components/session-diff.ts index 6fccfc305de3..ba0d5097cd71 100644 --- a/packages/session-ui/src/components/session-diff.ts +++ b/packages/session-ui/src/components/session-diff.ts @@ -1,6 +1,6 @@ import { parseDiffFromFile, parsePatchFiles, type FileDiffMetadata } from "@pierre/diffs" import { parsePatch } from "diff" -import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" type LegacyDiff = { file: string @@ -12,7 +12,7 @@ type LegacyDiff = { status?: "added" | "deleted" | "modified" } -type ReviewDiff = FileDiffInfo | VcsFileDiff | LegacyDiff +type ReviewDiff = (SnapshotFileDiff | VcsFileDiff | LegacyDiff) & { file: string } export type DiffSource = Pick export type ViewDiff = { diff --git a/packages/session-ui/src/components/session-review.tsx b/packages/session-ui/src/components/session-review.tsx index ec94af2f2d00..07ac0156d902 100644 --- a/packages/session-ui/src/components/session-review.tsx +++ b/packages/session-ui/src/components/session-review.tsx @@ -15,7 +15,7 @@ import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import { checksum } from "@opencode-ai/core/util/encode" import { createEffect, createMemo, For, Match, onCleanup, Show, Switch, untrack, type JSX } from "solid-js" import { createStore } from "solid-js/store" -import { type FileContent, type FileDiffInfo, type VcsFileDiff } from "@opencode-ai/sdk/v2" +import { type FileContent, type SnapshotFileDiff, type VcsFileDiff } from "@opencode-ai/sdk/v2" import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" import { type SelectedLineRange } from "@pierre/diffs" import { Dynamic } from "solid-js/web" @@ -62,10 +62,12 @@ export type SessionReviewCommentActions = { export type SessionReviewFocus = { file: string; id: string } -type RawReviewDiff = (FileDiffInfo | VcsFileDiff) & { +type RawReviewDiff = (SnapshotFileDiff | VcsFileDiff) & { + file: string preloaded?: PreloadMultiFileDiffResult } -type ReviewDiff = (FileDiffInfo | VcsFileDiff) & { +type ReviewDiff = (SnapshotFileDiff | VcsFileDiff) & { + file: string preloaded?: PreloadMultiFileDiffResult } function diff(value: unknown): value is ReviewDiff { diff --git a/packages/session-ui/src/components/session-turn.tsx b/packages/session-ui/src/components/session-turn.tsx index a6d0f11efbc3..e52d050a9a85 100644 --- a/packages/session-ui/src/components/session-turn.tsx +++ b/packages/session-ui/src/components/session-turn.tsx @@ -1,6 +1,6 @@ import { AssistantMessage, - type FileDiffInfo, + type SnapshotFileDiff, Message as MessageType, Part as PartType, type UserMessage, @@ -92,7 +92,7 @@ function list(value: T[] | undefined | null, fallback: T[]) { } type SummaryDiffInput = NonNullable["diffs"]>[number] -type SummaryDiff = FileDiffInfo +type SummaryDiff = SnapshotFileDiff & { file: string; patch: string } function summaryDiff(value: SummaryDiffInput): value is SummaryDiff { return ( diff --git a/packages/session-ui/src/context/data.tsx b/packages/session-ui/src/context/data.tsx index d505249931fd..9d9dbbb4342b 100644 --- a/packages/session-ui/src/context/data.tsx +++ b/packages/session-ui/src/context/data.tsx @@ -1,4 +1,4 @@ -import type { FileDiffInfo, Message, Part, Provider, Session, SessionStatus } from "@opencode-ai/sdk/v2" +import type { Message, Part, Provider, Session, SessionStatus, SnapshotFileDiff } from "@opencode-ai/sdk/v2" import { createSimpleContext } from "@opencode-ai/ui/context" import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" @@ -21,7 +21,7 @@ type Data = { [sessionID: string]: SessionStatus } session_diff: { - [sessionID: string]: FileDiffInfo[] + [sessionID: string]: SnapshotFileDiff[] } session_diff_preload?: { [sessionID: string]: PreloadMultiFileDiffResult[] diff --git a/packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx b/packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx index deb1cabf2128..81f4238ab437 100644 --- a/packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx +++ b/packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx @@ -6,7 +6,7 @@ import { useFileComponent } from "@opencode-ai/ui/context/file" import { useI18n } from "@opencode-ai/ui/context/i18n" import { mediaKindFromPath } from "../../pierre/media" import { cloneSelectedLineRange, previewSelectedLines } from "../../pierre/selection-bridge" -import type { FileContent, FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { FileContent, SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" import { createEffect, createMemo, onCleanup, Show, untrack } from "solid-js" import { createStore } from "solid-js/store" import { Dynamic } from "solid-js/web" @@ -27,7 +27,7 @@ import { LineCommentV2OverflowIcon } from "@opencode-ai/ui/v2/line-comment-v2" import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" import "./session-review-v2.css" -type ReviewDiff = FileDiffInfo | VcsFileDiff +type ReviewDiff = (SnapshotFileDiff | VcsFileDiff) & { file: string } export type SessionReviewFilePreviewV2Props = { file: string From 863645c67165182aedef043218e901b564d20f28 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 26 Jul 2026 20:17:31 -0400 Subject: [PATCH 129/150] test(core): update grep error assertion --- packages/core/test/tool-search.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/tool-search.test.ts b/packages/core/test/tool-search.test.ts index abe59a75dfbb..b88f4e7e881d 100644 --- a/packages/core/test/tool-search.test.ts +++ b/packages/core/test/tool-search.test.ts @@ -214,7 +214,7 @@ describe("search tools", () => { status: "error", error: { type: "tool.execution" }, }) - if (result.status !== "error") return + if (result.status !== "error" || !result.error) return expect(result.error.message).toStartWith("Invalid regex pattern:") expect(result.error.message).toContain("unclosed character class") }), From 4216d35e4bd18ffe0e7921f2c00de629a500245f Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sun, 26 Jul 2026 22:46:46 -0500 Subject: [PATCH 130/150] fix(server): declare schema dependency (#39043) --- bun.lock | 1 + packages/server/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/bun.lock b/bun.lock index a5b8672ee44a..dd01295505c1 100644 --- a/bun.lock +++ b/bun.lock @@ -672,6 +672,7 @@ "@effect/platform-node": "catalog:", "@opencode-ai/core": "workspace:*", "@opencode-ai/protocol": "workspace:*", + "@opencode-ai/schema": "workspace:*", "@opencode-ai/simulation": "workspace:*", "@opencode-ai/util": "workspace:*", "drizzle-orm": "catalog:", diff --git a/packages/server/package.json b/packages/server/package.json index f661018fe8f9..1b4717ca0135 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -16,6 +16,7 @@ "@effect/platform-node": "catalog:", "@opencode-ai/core": "workspace:*", "@opencode-ai/protocol": "workspace:*", + "@opencode-ai/schema": "workspace:*", "@opencode-ai/simulation": "workspace:*", "@opencode-ai/util": "workspace:*", "drizzle-orm": "catalog:", From 93cb113cef7b46e719979e917d0b77406f2c930b Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sun, 26 Jul 2026 23:12:36 -0500 Subject: [PATCH 131/150] fix(util): declare node tracing dependency (#39050) --- bun.lock | 3 +++ packages/util/package.json | 1 + 2 files changed, 4 insertions(+) diff --git a/bun.lock b/bun.lock index dd01295505c1..a8ef37689e72 100644 --- a/bun.lock +++ b/bun.lock @@ -970,6 +970,7 @@ "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", + "@opentelemetry/sdk-trace-node": "2.6.1", "cross-spawn": "catalog:", "effect": "catalog:", "glob": "13.0.5", @@ -2110,6 +2111,8 @@ "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="], + "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw=="], + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], "@opentui/core": ["@opentui/core@0.4.5", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.5", "@opentui/core-darwin-x64": "0.4.5", "@opentui/core-linux-arm64": "0.4.5", "@opentui/core-linux-arm64-musl": "0.4.5", "@opentui/core-linux-x64": "0.4.5", "@opentui/core-linux-x64-musl": "0.4.5", "@opentui/core-win32-arm64": "0.4.5", "@opentui/core-win32-x64": "0.4.5" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-JsgRTPkA6e+Vxmumxai6SElOSlRQkbzNKHlCfemlArRiLhfC1IZ9RXJo2QH4xSu+uBOWAM90uss73/pPlkdEig=="], diff --git a/packages/util/package.json b/packages/util/package.json index 70dab458df61..156a9eaeacad 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -39,6 +39,7 @@ "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", + "@opentelemetry/sdk-trace-node": "2.6.1", "cross-spawn": "catalog:", "effect": "catalog:", "glob": "13.0.5", From 9b49e7bec9157bb5a0434199e62f3ffae33e61e0 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sun, 26 Jul 2026 23:32:33 -0500 Subject: [PATCH 132/150] test(core): implement catalog host model list (#39053) --- packages/core/test/plugin/host.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index 4087a38f9cd4..614ce2b323f5 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -155,7 +155,16 @@ export function catalogHost(catalog: Catalog.Interface): Plugin.Context["catalog get: () => Effect.die("unused catalog.provider.get"), }, model: { - list: () => Effect.die("unused catalog.model.list"), + list: () => + catalog.model.available().pipe( + Effect.map((data) => ({ + location: new Location.Info({ + directory: AbsolutePath.make("/"), + project: { id: Project.ID.make("test"), directory: AbsolutePath.make("/") }, + }), + data: data.map(modelInfo), + })), + ), default: () => Effect.die("unused catalog.model.default"), }, reload: catalog.reload, From 430547ed03b60c4a9665bb543c8bb43caa6cb5c7 Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 27 Jul 2026 01:03:11 -0700 Subject: [PATCH 133/150] fix(app): remove legacy server health fallback --- packages/app/src/utils/server-health.test.ts | 20 +++++++++----------- packages/app/src/utils/server-health.ts | 8 ++------ 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/packages/app/src/utils/server-health.test.ts b/packages/app/src/utils/server-health.test.ts index 69a8c7b3be2b..14cb705f943a 100644 --- a/packages/app/src/utils/server-health.test.ts +++ b/packages/app/src/utils/server-health.test.ts @@ -29,30 +29,28 @@ describe("checkServerHealth", () => { expect(request?.pathname).toBe("/api/health") }) - test("falls back to the V1 health endpoint", async () => { + test("returns unhealthy when the V2 health endpoint is unavailable", async () => { const paths: string[] = [] const fetch = (async (input: RequestInfo | URL) => { const url = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input) paths.push(url.pathname) - if (url.pathname === "/api/health") return new Response(undefined, { status: 404 }) - return Response.json({ healthy: true, version: "1.18.4" }) + return new Response(undefined, { status: 404 }) }) as unknown as typeof globalThis.fetch - expect(await checkServerHealth(server, fetch)).toEqual({ healthy: true, version: "1.18.4" }) - expect(paths).toEqual(["/api/health", "/global/health"]) + expect(await checkServerHealth(server, fetch)).toEqual({ healthy: false }) + expect(paths).toEqual(["/api/health"]) }) - test("falls back when the current health response is malformed", async () => { + test("returns unhealthy when the V2 health response is malformed", async () => { const paths: string[] = [] const fetch = (async (input: RequestInfo | URL) => { const url = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input) paths.push(url.pathname) - if (url.pathname === "/api/health") return Response.json({}) - return Response.json({ healthy: true, version: "1.18.4" }) + return Response.json({}) }) as unknown as typeof globalThis.fetch - expect(await checkServerHealth(server, fetch)).toEqual({ healthy: true, version: "1.18.4" }) - expect(paths).toEqual(["/api/health", "/global/health"]) + expect(await checkServerHealth(server, fetch)).toEqual({ healthy: false }) + expect(paths).toEqual(["/api/health"]) }) test("allows slow servers thirty seconds by default", async () => { @@ -172,7 +170,7 @@ describe("checkServerHealth", () => { retryDelayMs: 1, }) - expect(count).toBe(6) + expect(count).toBe(3) expect(result).toEqual({ healthy: false }) }) }) diff --git a/packages/app/src/utils/server-health.ts b/packages/app/src/utils/server-health.ts index 1d7d9e4b2ea6..af82227050e3 100644 --- a/packages/app/src/utils/server-health.ts +++ b/packages/app/src/utils/server-health.ts @@ -1,6 +1,6 @@ import { usePlatform } from "@/context/platform" import { ServerConnection } from "@/context/server" -import { authTokenFromCredentials, createSdkForServer } from "./server" +import { authTokenFromCredentials } from "./server" import { ClientError, OpenCode } from "@opencode-ai/client" import { Accessor, createEffect, onCleanup } from "solid-js" import { createStore, reconcile } from "solid-js/store" @@ -103,11 +103,7 @@ export async function checkServerHealth( .catch((error) => ({ error })) if ("data" in current && current.data) return current.data if (signal?.aborted) return { healthy: false } - - return createSdkForServer({ server, fetch, signal }) - .global.health() - .then((x) => (x.error ? next(count, x.error) : { healthy: x.data?.healthy === true, version: x.data?.version })) - .catch((error) => next(count, error)) + return next(count, current.error) } return attempt(0).finally(() => timeout?.clear?.()) } From 23c8d34d706c754563df88770c6283ce4419865c Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 27 Jul 2026 01:07:42 -0700 Subject: [PATCH 134/150] fix(plugin): migrate repository tools to v2 --- .opencode/plugins/github-pr-search.ts | 75 ++++++++++++++++++++ .opencode/{tool => plugins}/github-triage.ts | 46 +++++++----- .opencode/tool/github-pr-search.ts | 64 ----------------- 3 files changed, 102 insertions(+), 83 deletions(-) create mode 100644 .opencode/plugins/github-pr-search.ts rename .opencode/{tool => plugins}/github-triage.ts (56%) delete mode 100644 .opencode/tool/github-pr-search.ts diff --git a/.opencode/plugins/github-pr-search.ts b/.opencode/plugins/github-pr-search.ts new file mode 100644 index 000000000000..c90e3ab77a0f --- /dev/null +++ b/.opencode/plugins/github-pr-search.ts @@ -0,0 +1,75 @@ +/// +import { Plugin } from "@opencode-ai/plugin" +import { z } from "zod" + +async function githubFetch(endpoint: string, options: RequestInit = {}) { + const response = await fetch(`https://api.github.com${endpoint}`, { + ...options, + headers: { + Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, + Accept: "application/vnd.github+json", + "Content-Type": "application/json", + ...(options.headers instanceof Headers ? Object.fromEntries(options.headers.entries()) : options.headers), + }, + }) + if (!response.ok) { + throw new Error(`GitHub API error: ${response.status} ${response.statusText}`) + } + return response.json() +} + +interface PR { + title: string + html_url: string +} + +export default Plugin.define({ + id: "repository.github-pr-search", + setup: async (ctx) => { + await ctx.tool.transform((tools) => { + tools.add({ + name: "github-pr-search", + options: { codemode: false }, + description: `Use this tool to search GitHub pull requests by title and description. + +This tool searches PRs in the anomalyco/opencode repository and returns LLM-friendly results including: +- PR number and title +- Author +- State (open/closed/merged) +- Labels +- Description snippet + +Use the query parameter to search for keywords that might appear in PR titles or descriptions.`, + input: z.object({ + query: z.string().describe("Search query for PR titles and descriptions"), + limit: z.number().describe("Maximum number of results to return").default(10), + offset: z.number().describe("Number of results to skip for pagination").default(0), + }), + async execute(args) { + const owner = "anomalyco" + const repo = "opencode" + + const page = Math.floor(args.offset / args.limit) + 1 + const searchQuery = encodeURIComponent(`${args.query} repo:${owner}/${repo} type:pr state:open`) + const result = await githubFetch( + `/search/issues?q=${searchQuery}&per_page=${args.limit}&page=${page}&sort=updated&order=desc`, + ) + + if (result.total_count === 0) { + return { content: `No PRs found matching "${args.query}"` } + } + + const prs = result.items as PR[] + + if (prs.length === 0) { + return { content: `No other PRs found matching "${args.query}"` } + } + + const formatted = prs.map((pr) => `${pr.title}\n${pr.html_url}`).join("\n\n") + + return { content: `Found ${result.total_count} PRs (showing ${prs.length}):\n\n${formatted}` } + }, + }) + }) + }, +}) diff --git a/.opencode/tool/github-triage.ts b/.opencode/plugins/github-triage.ts similarity index 56% rename from .opencode/tool/github-triage.ts rename to .opencode/plugins/github-triage.ts index e861e1e467b2..0344b925df6a 100644 --- a/.opencode/tool/github-triage.ts +++ b/.opencode/plugins/github-triage.ts @@ -1,5 +1,6 @@ /// -import { tool } from "@opencode-ai/plugin" +import { Plugin } from "@opencode-ai/plugin" +import { z } from "zod" const TEAM = { tui: ["kommander", "simonklee"], @@ -35,26 +36,33 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) { return response.json() } -export default tool({ - description: `Use this tool to assign a GitHub issue. +export default Plugin.define({ + id: "repository.github-triage", + setup: async (ctx) => { + await ctx.tool.transform((tools) => { + tools.add({ + name: "github-triage", + options: { codemode: false }, + description: `Use this tool to assign a GitHub issue. Provide the team that should own the issue. This tool picks a random assignee from that team and does not apply labels.`, - args: { - team: tool.schema - .enum(Object.keys(TEAM) as [keyof typeof TEAM, ...(keyof typeof TEAM)[]]) - .describe("The owning team"), - }, - async execute(args) { - const issue = getIssueNumber() - const owner = "anomalyco" - const repo = "opencode" - const assignee = pick(TEAM[args.team]) - - await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/assignees`, { - method: "POST", - body: JSON.stringify({ assignees: [assignee] }), - }) + input: z.object({ + team: z.enum(Object.keys(TEAM) as [keyof typeof TEAM, ...(keyof typeof TEAM)[]]).describe("The owning team"), + }), + async execute(args) { + const issue = getIssueNumber() + const owner = "anomalyco" + const repo = "opencode" + const assignee = pick(TEAM[args.team]) - return `Assigned @${assignee} from ${args.team} to issue #${issue}` + await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/assignees`, { + method: "POST", + body: JSON.stringify({ assignees: [assignee] }), + }) + + return { content: `Assigned @${assignee} from ${args.team} to issue #${issue}` } + }, + }) + }) }, }) diff --git a/.opencode/tool/github-pr-search.ts b/.opencode/tool/github-pr-search.ts deleted file mode 100644 index 8bc8c554aaee..000000000000 --- a/.opencode/tool/github-pr-search.ts +++ /dev/null @@ -1,64 +0,0 @@ -/// -import { tool } from "@opencode-ai/plugin" -async function githubFetch(endpoint: string, options: RequestInit = {}) { - const response = await fetch(`https://api.github.com${endpoint}`, { - ...options, - headers: { - Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, - Accept: "application/vnd.github+json", - "Content-Type": "application/json", - ...(options.headers instanceof Headers ? Object.fromEntries(options.headers.entries()) : options.headers), - }, - }) - if (!response.ok) { - throw new Error(`GitHub API error: ${response.status} ${response.statusText}`) - } - return response.json() -} - -interface PR { - title: string - html_url: string -} - -export default tool({ - description: `Use this tool to search GitHub pull requests by title and description. - -This tool searches PRs in the anomalyco/opencode repository and returns LLM-friendly results including: -- PR number and title -- Author -- State (open/closed/merged) -- Labels -- Description snippet - -Use the query parameter to search for keywords that might appear in PR titles or descriptions.`, - args: { - query: tool.schema.string().describe("Search query for PR titles and descriptions"), - limit: tool.schema.number().describe("Maximum number of results to return").default(10), - offset: tool.schema.number().describe("Number of results to skip for pagination").default(0), - }, - async execute(args) { - const owner = "anomalyco" - const repo = "opencode" - - const page = Math.floor(args.offset / args.limit) + 1 - const searchQuery = encodeURIComponent(`${args.query} repo:${owner}/${repo} type:pr state:open`) - const result = await githubFetch( - `/search/issues?q=${searchQuery}&per_page=${args.limit}&page=${page}&sort=updated&order=desc`, - ) - - if (result.total_count === 0) { - return `No PRs found matching "${args.query}"` - } - - const prs = result.items as PR[] - - if (prs.length === 0) { - return `No other PRs found matching "${args.query}"` - } - - const formatted = prs.map((pr) => `${pr.title}\n${pr.html_url}`).join("\n\n") - - return `Found ${result.total_count} PRs (showing ${prs.length}):\n\n${formatted}` - }, -}) From 7a342990061543d258c67d550c80e06f81057517 Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 27 Jul 2026 01:52:20 -0700 Subject: [PATCH 135/150] fix(server): validate advertised URLs --- packages/server/src/server-info.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server-info.ts b/packages/server/src/server-info.ts index 08e2fa8030ed..9bd730b5aa15 100644 --- a/packages/server/src/server-info.ts +++ b/packages/server/src/server-info.ts @@ -32,7 +32,17 @@ export function connectionURLs(value: string, requestedHostname?: string) { } export function advertisedURLs(values: ReadonlyArray) { - return values.map((value) => new URL(value).toString().replace(/\/$/, "")) + return [...new Set(values.map(advertisedURL))] +} + +function advertisedURL(value: string) { + const url = new URL(value) + if (!["http:", "https:"].includes(url.protocol)) throw new Error("Advertised URLs must use HTTP or HTTPS") + if (url.username || url.password || url.search || url.hash || url.pathname !== "/") + throw new Error("Advertised URLs cannot contain userinfo, a path, query, or fragment") + if (url.protocol === "http:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) + throw new Error("Advertised HTTP URLs must be loopback") + return url.toString().replace(/\/$/, "") } export * as ServerInfo from "./server-info" From 12f5ece8c89e6c239bdb6a958ff8577aa0b2ff2c Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 27 Jul 2026 01:57:06 -0700 Subject: [PATCH 136/150] fix(core): preserve tool after-hook results --- packages/core/src/session/runner/llm.ts | 2 +- .../src/session/runner/publish-llm-event.ts | 16 ++++++++--- packages/core/src/tool.ts | 28 +++++++++++++++---- packages/core/test/plugin.test.ts | 5 ++-- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 42f871e8f704..d5c1d63f603a 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -309,7 +309,7 @@ const layer = Layer.effect( // finished execution always reaches its durable settlement. Effect.flatMap((outcome) => publisher.toolExecution(event.id, event.name, outcome)), Effect.catchTag("Tool.Error", (error) => - publisher.failTool(event.id, toSessionError(error)).pipe(Effect.asVoid), + publisher.failTool(event.id, toSessionError(error), error.metadata).pipe(Effect.asVoid), ), ), ).pipe(Effect.forkScoped), diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts index 99dc0ad77b54..bf9d0d73ac64 100644 --- a/packages/core/src/session/runner/publish-llm-event.ts +++ b/packages/core/src/session/runner/publish-llm-event.ts @@ -92,8 +92,12 @@ export const createLLMEventPublisher = (bus: Pick, inp progress?: Tool.Metadata } >() - const failureSnapshot = (tool: { readonly progress?: Tool.Metadata }) => - tool.progress === undefined ? {} : { metadata: tool.progress } + const failureSnapshot = (tool: { readonly progress?: Tool.Metadata }, metadata?: Tool.Metadata) => + metadata === undefined + ? tool.progress === undefined + ? {} + : { metadata: tool.progress } + : { metadata } const assistantMessageID = input.assistantMessageID let stepStarted = false let stepFailed = false @@ -272,7 +276,11 @@ export const createLLMEventPublisher = (bus: Pick, inp yield* flushFragments() }) - const failTool = Effect.fnUntraced(function* (callID: string, error: SessionError.Error) { + const failTool = Effect.fnUntraced(function* ( + callID: string, + error: SessionError.Error, + metadata?: Tool.Metadata, + ) { const tool = tools.get(callID) if (!tool || tool.settled) return false tool.settled = true @@ -281,7 +289,7 @@ export const createLLMEventPublisher = (bus: Pick, inp assistantMessageID: tool.assistantMessageID, callID, error, - ...failureSnapshot(tool), + ...failureSnapshot(tool, metadata), executed: tool.providerExecuted, }) return true diff --git a/packages/core/src/tool.ts b/packages/core/src/tool.ts index 43bc900c46a4..9c29d4ff2be6 100644 --- a/packages/core/src/tool.ts +++ b/packages/core/src/tool.ts @@ -119,21 +119,37 @@ const layer = Layer.effect( return yield* afterEvent.error } const content = yield* normalizeImages(execution.value.content) - const afterEvent: PluginHooks.Domains["tool"]["execute.after"] = { - ...base, - status: "completed", + const terminal: { result: Tool.Result; replaced: boolean } = { result: { ...(execution.value.output === undefined ? {} : { output: execution.value.output }), content: content.length > 0 ? content : execution.value.content, ...(execution.value.metadata === undefined ? {} : { metadata: execution.value.metadata }), }, + replaced: false, + } + const afterEvent: PluginHooks.Domains["tool"]["execute.after"] = { + ...base, + status: "completed", + get result() { + return { + ...terminal.result, + ...(Array.isArray(terminal.result.content) ? { content: [...terminal.result.content] } : {}), + ...(terminal.result.metadata === undefined ? {} : { metadata: { ...terminal.result.metadata } }), + } + }, + set result(value) { + terminal.replaced = true + terminal.result = value + }, } yield* hooks.trigger("tool", "execute.after", afterEvent) - const afterContent = yield* normalizeImages(normalizeContent(afterEvent.result.content, afterEvent.result.output)) + const afterContent = terminal.replaced + ? yield* normalizeImages(normalizeContent(terminal.result.content, execution.value.output)) + : content return { - ...(afterEvent.result.output === undefined ? {} : { output: afterEvent.result.output }), + ...(execution.value.output === undefined ? {} : { output: execution.value.output }), content: afterContent, - ...(afterEvent.result.metadata === undefined ? {} : { metadata: afterEvent.result.metadata }), + ...(terminal.result.metadata === undefined ? {} : { metadata: terminal.result.metadata }), } }) diff --git a/packages/core/test/plugin.test.ts b/packages/core/test/plugin.test.ts index e2f0b88a94f9..25077743d71a 100644 --- a/packages/core/test/plugin.test.ts +++ b/packages/core/test/plugin.test.ts @@ -358,6 +358,7 @@ describe("Plugin", () => { if (event.status !== "completed") return event.result = { ...event.result, + output: { text: "after-output" }, content: [{ type: "text", text: "after-mutated" }], metadata: { rewritten: true }, } @@ -393,8 +394,8 @@ describe("Plugin", () => { content: [{ type: "text", text: '{"text":"before-mutated"}' }], metadata: undefined, }) - expect(execution).toMatchObject({ - status: "completed", + expect(execution).toEqual({ + output: { text: "before-mutated" }, content: [{ type: "text", text: "after-mutated" }], metadata: { rewritten: true }, }) From 159ccab89e26082f19a554b657d1624d14376ea5 Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 27 Jul 2026 02:02:17 -0700 Subject: [PATCH 137/150] fix(core): fail drains on tool persistence errors --- packages/core/src/session/runner/llm.ts | 49 ++++++++++++++++--- .../src/session/runner/publish-llm-event.ts | 12 ++--- .../test/session-runner-tool-events.test.ts | 32 ++++++++++-- packages/core/test/session-runner.test.ts | 11 ++++- 4 files changed, 85 insertions(+), 19 deletions(-) diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index d5c1d63f603a..40fa90f53e9b 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -34,6 +34,10 @@ type CallOutcome = Data.TaggedEnum<{ }> const CallOutcome = Data.taggedEnum() +class ToolPersistenceError extends Data.TaggedError("SessionRunner.ToolPersistenceError")<{ + readonly cause: Cause.Cause +}> {} + // Declining an interactive prompt halts the drain instead of becoming model-facing tool output. const isDecline = ( error: SessionModelRequest.ExecuteError, @@ -47,7 +51,7 @@ const isDecline = ( * fail the assistant and then the drain. */ const classifyToolExits = ( - settled: Exit.Exit>, never>, + settled: Exit.Exit>, never>, calls: ReadonlyArray, ) => { // Exits align with calls by construction: one owned fiber per accepted local call. @@ -55,12 +59,25 @@ const classifyToolExits = ( const declines = exits.flatMap((exit, index) => exit._tag === "Failure" ? exit.cause.reasons.flatMap((reason) => - Cause.isFailReason(reason) && isDecline(reason.error) ? [{ call: calls[index], reason: reason.error }] : [], + Cause.isFailReason(reason) && + reason.error._tag !== "SessionRunner.ToolPersistenceError" && + isDecline(reason.error) + ? [{ call: calls[index], reason: reason.error }] + : [], ) : [], ) const causes = settled._tag === "Failure" ? [settled.cause] : exits.flatMap((exit) => (exit._tag === "Failure" ? [exit.cause] : [])) + const persistence = causes + .flatMap((cause) => + cause.reasons.flatMap((reason) => + Cause.isFailReason(reason) && reason.error._tag === "SessionRunner.ToolPersistenceError" + ? [reason.error.cause] + : [], + ), + ) + .at(0) // The first non-interrupt, non-decline failure, rebuilt without decline reasons so the // drain's error channel never carries a decline. const failure = causes.flatMap((cause) => { @@ -74,6 +91,7 @@ const classifyToolExits = ( interrupted: causes.some(Cause.hasInterrupts), declines, failure, + persistence, } } @@ -225,7 +243,7 @@ const layer = Layer.effect( // Every local tool call forked here is owned until it reaches one durable settlement. const toolRuns: Array<{ readonly call: ToolCall - readonly fiber: Fiber.Fiber + readonly fiber: Fiber.Fiber }> = [] const interruptTools = Effect.suspend(() => Fiber.interruptAll(toolRuns.map((run) => run.fiber))) const startSnapshot = yield* snapshots.capture() @@ -305,11 +323,18 @@ const layer = Layer.effect( progress: (update) => publisher.progress(event.id, update), }), ).pipe( - // The fiber owns its call: it publishes its own completion, masked so a - // finished execution always reaches its durable settlement. - Effect.flatMap((outcome) => publisher.toolExecution(event.id, event.name, outcome)), + // Only execution failures are model-visible. Failure to persist either + // terminal event is infrastructure failure owned by the active drain. + Effect.flatMap((outcome) => + publisher.toolExecution(event.id, event.name, outcome).pipe( + Effect.catchCause((cause) => Effect.fail(new ToolPersistenceError({ cause }))), + ), + ), Effect.catchTag("Tool.Error", (error) => - publisher.failTool(event.id, toSessionError(error), error.metadata).pipe(Effect.asVoid), + publisher.failTool(event.id, toSessionError(error), error.metadata).pipe( + Effect.asVoid, + Effect.catchCause((cause) => Effect.fail(new ToolPersistenceError({ cause }))), + ), ), ), ).pipe(Effect.forkScoped), @@ -390,6 +415,14 @@ const layer = Layer.effect( const error = toSessionError(Cause.squash(tools.failure)) yield* publisher.failUnsettledTools(error) } + if (tools.persistence !== undefined) { + const error = { + type: "unknown" as const, + message: `Failed to write tool output: ${Cause.pretty(tools.persistence)}`, + } + yield* publisher.failUnsettledTools(error) + yield* publisher.failAssistant(error) + } // Local calls have joined, so the remaining sweeps only close hosted calls the // provider promised but never resolved. if (publisher.record().providerFailed) yield* publisher.failUnsettledTools(TOOLS_INTERRUPTED) @@ -415,6 +448,8 @@ const layer = Layer.effect( if (tools.declines.length > 0) return yield* Effect.interrupt if (tools.interrupted && tools.failure) return yield* Effect.failCause(tools.failure) if (tools.interrupted && joined._tag === "Failure") return yield* Effect.failCause(joined.cause) + if (record.providerFailed && record.failure) return yield* new StepFailedError({ error: record.failure }) + if (tools.persistence) return yield* Effect.failCause(tools.persistence) if (record.failure) return yield* new StepFailedError({ error: record.failure }) return CallOutcome.Completed({ // A local call or malformed tool input requires another model step, unless diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts index bf9d0d73ac64..f961fdb7f85a 100644 --- a/packages/core/src/session/runner/publish-llm-event.ts +++ b/packages/core/src/session/runner/publish-llm-event.ts @@ -269,7 +269,7 @@ export const createLLMEventPublisher = (bus: Pick, inp }, ...failureSnapshot(tool), executed: false, - }) + }).pipe(Effect.onError(() => Effect.sync(() => (tool.settled = false)))) }) const flush = Effect.fn("SessionRunner.flush")(function* () { @@ -291,7 +291,7 @@ export const createLLMEventPublisher = (bus: Pick, inp error, ...failureSnapshot(tool, metadata), executed: tool.providerExecuted, - }) + }).pipe(Effect.onError(() => Effect.sync(() => (tool.settled = false)))) return true }) @@ -463,7 +463,7 @@ export const createLLMEventPublisher = (bus: Pick, inp ...failureSnapshot(tool), executed, resultState, - }) + }).pipe(Effect.onError(() => Effect.sync(() => (tool.settled = false)))) return } yield* bus.publish(SessionEvent.Tool.Success, { @@ -473,7 +473,7 @@ export const createLLMEventPublisher = (bus: Pick, inp content: hostedContent(event.result), executed, resultState, - }) + }).pipe(Effect.onError(() => Effect.sync(() => (tool.settled = false)))) return } case "tool-error": { @@ -494,7 +494,7 @@ export const createLLMEventPublisher = (bus: Pick, inp ...failureSnapshot(tool), executed: tool.providerExecuted, resultState: providerState(event.providerMetadata), - }) + }).pipe(Effect.onError(() => Effect.sync(() => (tool.settled = false)))) return } case "step-finish": @@ -555,7 +555,7 @@ export const createLLMEventPublisher = (bus: Pick, inp content: [content[0], ...content.slice(1)], ...(result.metadata === undefined ? {} : { metadata: result.metadata }), executed: tool.providerExecuted, - }) + }).pipe(Effect.onError(() => Effect.sync(() => (tool.settled = false)))) }) return { diff --git a/packages/core/test/session-runner-tool-events.test.ts b/packages/core/test/session-runner-tool-events.test.ts index 82c6a8691592..c5b3adfba785 100644 --- a/packages/core/test/session-runner-tool-events.test.ts +++ b/packages/core/test/session-runner-tool-events.test.ts @@ -17,7 +17,10 @@ import { createLLMEventPublisher } from "@opencode-ai/core/session/runner/publis const sessionID = Session.ID.make("ses_tool_event_test") const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB" -const capture = (providerMetadataKey = "anthropic", options?: { readonly interruptProgress?: boolean }) => { +const capture = ( + providerMetadataKey = "anthropic", + options?: { readonly interruptProgress?: boolean; readonly failToolSuccess?: boolean }, +) => { const published: Array<{ readonly type: string; readonly data: unknown }> = [] const bus: Pick = { publish: (definition, data) => { @@ -31,9 +34,11 @@ const capture = (providerMetadataKey = "anthropic", options?: { readonly interru }) return event }) - return definition.type === SessionEvent.Tool.Progress.type && options?.interruptProgress - ? publish.pipe(Effect.andThen(Effect.interrupt)) - : publish + if (definition.type === SessionEvent.Tool.Progress.type && options?.interruptProgress) + return publish.pipe(Effect.andThen(Effect.interrupt)) + if (definition.type === SessionEvent.Tool.Success.type && options?.failToolSuccess) + return Effect.die("tool success persistence failed") + return publish }, } return { @@ -233,6 +238,25 @@ test("binary failure emits no success event", async () => { expect(published.some((event) => event.type === "session.tool.failed.2")).toBe(true) }) +test("failed success persistence leaves the tool available for durable failure settlement", async () => { + const { published, publisher } = capture("anthropic", { failToolSuccess: true }) + await Effect.runPromise(publisher.publish(call)) + + expect( + Exit.isFailure( + await Effect.runPromiseExit( + publisher.toolExecution(call.id, call.name, { output: {}, content: "unpersisted output" }), + ), + ), + ).toBe(true) + await Effect.runPromise(publisher.failUnsettledTools({ type: "unknown", message: "persistence failed" })) + + expect(published.find((event) => event.type === "session.tool.failed.2")?.data).toMatchObject({ + callID: call.id, + error: { type: "unknown", message: "persistence failed" }, + }) +}) + test("success event data can carry provider-executed result state", () => { const decoded = Schema.decodeUnknownSync(SessionEvent.Tool.Success.data)({ sessionID, diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 2c4876235ccb..de96926b539d 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -284,7 +284,12 @@ const echo = Layer.effectDiscard( description: "Produce output that cannot be persisted", input: Schema.Struct({}), output: Schema.Struct({}), - execute: () => Effect.succeed({ output: {} }), + execute: () => + Effect.sync(() => { + const metadata: Record = {} + metadata.circular = metadata + return { output: {}, metadata } + }), }), }, { codemode: false }, @@ -4726,7 +4731,9 @@ describe("SessionRunnerLLM", () => { LLMEvent.providerError({ message: "Provider unavailable" }), ] - expect(yield* session.resume(sessionID).pipe(Effect.exit)).toMatchObject({ _tag: "Failure" }) + expect(yield* session.resume(sessionID).pipe(Effect.flip)).toMatchObject({ + error: { type: "provider.unknown", message: "Provider unavailable" }, + }) expect(requireAssistant(yield* session.context(sessionID))).toMatchObject({ error: { type: "provider.unknown", message: "Provider unavailable" }, From de79cc19248270c3de5f3d7f2bdaaeb85dfb1e22 Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 27 Jul 2026 02:06:17 -0700 Subject: [PATCH 138/150] test(core): align consolidated tool expectations --- .../core/test/codemode/instructions.test.ts | 6 +- packages/core/test/session-runner.test.ts | 94 +++++++++++++------ 2 files changed, 68 insertions(+), 32 deletions(-) diff --git a/packages/core/test/codemode/instructions.test.ts b/packages/core/test/codemode/instructions.test.ts index fcb6fe730a58..543938c26840 100644 --- a/packages/core/test/codemode/instructions.test.ts +++ b/packages/core/test/codemode/instructions.test.ts @@ -2,6 +2,8 @@ import { describe, expect } from "bun:test" import { CodeModeCatalog } from "@opencode-ai/core/codemode/catalog" import { CodeModeInstructions } from "@opencode-ai/core/codemode/instructions" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { Location } from "@opencode-ai/core/location" +import { AbsolutePath } from "@opencode-ai/core/schema" import { Tool } from "@opencode-ai/core/tool" import { Effect, Schema } from "effect" import { it } from "../lib/effect" @@ -79,7 +81,9 @@ describe("CodeModeInstructions", () => { output: Schema.String, execute: () => Effect.succeed({ output: "zeta" }), }) - const layer = AppNodeBuilder.build(Tool.node) + const layer = AppNodeBuilder.build(Tool.node, [ + [Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })], + ]) return Effect.gen(function* () { const tools = yield* Tool.Service diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index de96926b539d..b9c58ec4a49e 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -16,6 +16,7 @@ import { } from "@opencode-ai/ai" import * as OpenAIChat from "@opencode-ai/ai/protocols/openai-chat" import { Catalog } from "@opencode-ai/core/catalog" +import { CodeModeCatalog } from "@opencode-ai/core/codemode/catalog" import { Database } from "@opencode-ai/core/database/database" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" @@ -141,6 +142,10 @@ const reply = { } const model = Model.make({ id: "fake-model", provider: "fake", route: OpenAIChat.route }) const defaultSystem = PROMPT_DEFAULT +const emptyCodeModeGuidance = + "No Code Mode tools are currently available. Later Code Mode catalog updates may add or remove tools. Do not call `execute` unless there is at least one available Code Mode tool." +const withCodeModeGuidance = (...instructions: ReadonlyArray) => + [instructions[0], emptyCodeModeGuidance, ...instructions.slice(1)].join("\n\n") const replacementModel = Model.make({ id: "replacement", provider: "fake", route: OpenAIChat.route }) const compactModel = Model.make({ id: "compact", @@ -860,7 +865,7 @@ describe("SessionRunnerLLM", () => { { type: "tool", id: "call-removed", - state: { status: "error", error: { type: "tool.unknown" } }, + state: { status: "error", error: { type: "tool.execution" } }, }, ], }, @@ -1105,7 +1110,7 @@ describe("SessionRunnerLLM", () => { expect(requests).toHaveLength(1) expect(requests[0]?.model).toBe(model) - expect(requests[0]?.tools.map((tool) => tool.name)).toEqual(["defect", "echo", "storefail"]) + expect(requests[0]?.tools.map((tool) => tool.name)).toEqual(["defect", "echo", "storefail", "execute"]) expect(requests[0]?.messages.map((message) => ({ role: message.role, content: message.content }))).toEqual([ { role: "user", content: [{ type: "text", text: "First" }] }, { role: "user", content: [{ type: "text", text: "Second" }] }, @@ -1221,7 +1226,10 @@ describe("SessionRunnerLLM", () => { yield* session.prompt({ sessionID: forked.id, text: "Forked", resume: false }) yield* session.resume(forked.id) - expect(requests.at(-1)?.system.map((part) => part.text)).toEqual([defaultSystem, "Initial context"]) + expect(requests.at(-1)?.system.map((part) => part.text)).toEqual([ + defaultSystem, + withCodeModeGuidance("Initial context"), + ]) expect(systemTexts(requests.at(-1)!)).toContain("Changed context") expect(systemTexts(requests.at(-1)!)).toContain("Latest context") @@ -1293,7 +1301,10 @@ describe("SessionRunnerLLM", () => { yield* session.resume(sessionID) expect(requests).toHaveLength(1) - expect(requests[0]?.system.map((part) => part.text)).toEqual([defaultSystem, "Initial context"]) + expect(requests[0]?.system.map((part) => part.text)).toEqual([ + defaultSystem, + withCodeModeGuidance("Initial context"), + ]) expect(requests[0]?.messages.map((message) => message.role)).toEqual(["user", "user"]) expect( yield* db @@ -1320,8 +1331,8 @@ describe("SessionRunnerLLM", () => { yield* session.resume(sessionID) expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([ - [defaultSystem, "Initial context"], - [defaultSystem, "Initial context"], + [defaultSystem, withCodeModeGuidance("Initial context")], + [defaultSystem, withCodeModeGuidance("Initial context")], ]) expect(requests[1]?.messages.map((message) => message.role)).toEqual(["user", "system", "user"]) expect(requests[1]?.messages.at(1)?.content).toEqual([{ type: "text", text: "Changed context" }]) @@ -1337,7 +1348,10 @@ describe("SessionRunnerLLM", () => { expect(updates).toHaveLength(2) expect(updates[0]?.data).toEqual({ sessionID, - delta: { "test/context": Instructions.hash("Initial context") }, + delta: { + "test/context": Instructions.hash("Initial context"), + "core/codemode": Instructions.hash(CodeModeCatalog.summarize([])), + }, }) expect(updates[1]?.data).toEqual({ sessionID, @@ -1359,7 +1373,7 @@ describe("SessionRunnerLLM", () => { expect(requests.at(-1)?.system.map((part) => part.text)).toEqual([ expect.stringContaining("You are OpenCode, You and the user share the same workspace"), - "Initial context", + withCodeModeGuidance("Initial context"), ]) }), ) @@ -1382,7 +1396,7 @@ describe("SessionRunnerLLM", () => { expect(requests.at(-1)?.system.map((part) => part.text)).toEqual([ expect.stringContaining("You are OpenCode, You and the user share the same workspace"), - "Initial context", + withCodeModeGuidance("Initial context"), ]) }), ) @@ -1402,7 +1416,10 @@ describe("SessionRunnerLLM", () => { response = reply.text("Done", "text-build") yield* session.resume(sessionID) - expect(requests.at(-1)?.system.map((part) => part.text)).toEqual(["Build agent instructions", "Initial context"]) + expect(requests.at(-1)?.system.map((part) => part.text)).toEqual([ + "Build agent instructions", + withCodeModeGuidance("Initial context"), + ]) }), ) @@ -1426,7 +1443,10 @@ describe("SessionRunnerLLM", () => { response = reply.text("Done", "text-reviewer") yield* session.resume(sessionID) - expect(requests.at(-1)?.system.map((part) => part.text)).toEqual(["Reviewer instructions", "Initial context"]) + expect(requests.at(-1)?.system.map((part) => part.text)).toEqual([ + "Reviewer instructions", + withCodeModeGuidance("Initial context"), + ]) expect((yield* session.messages({ sessionID }))[0]).toMatchObject({ type: "assistant", agent: "reviewer" }) }), ) @@ -1446,7 +1466,10 @@ describe("SessionRunnerLLM", () => { response = reply.text("Done", "text-no-system") yield* session.resume(sessionID) - expect(requests.at(-1)?.system.map((part) => part.text)).toEqual(["Build agent instructions", "Initial context"]) + expect(requests.at(-1)?.system.map((part) => part.text)).toEqual([ + "Build agent instructions", + withCodeModeGuidance("Initial context"), + ]) }), ) @@ -1472,7 +1495,10 @@ describe("SessionRunnerLLM", () => { response = reply.text("Done", "text-selected") yield* session.resume(sessionID) - expect(requests.at(-1)?.system.map((part) => part.text)).toEqual(["Reviewer instructions", "Initial context"]) + expect(requests.at(-1)?.system.map((part) => part.text)).toEqual([ + "Reviewer instructions", + withCodeModeGuidance("Initial context"), + ]) expect((yield* session.messages({ sessionID }))[0]).toMatchObject({ type: "assistant", agent: "reviewer" }) }), ) @@ -1548,8 +1574,8 @@ describe("SessionRunnerLLM", () => { yield* session.resume(sessionID) expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([ - [defaultSystem, "Initial context\n\nBuild skills"], - [defaultSystem, "Initial context\n\nBuild skills"], + [defaultSystem, withCodeModeGuidance("Initial context", "Build skills")], + [defaultSystem, withCodeModeGuidance("Initial context", "Build skills")], ]) expect(systemTexts(requests[1]!)).toContainEqual(expect.stringContaining("Reviewer skills")) }), @@ -1577,7 +1603,7 @@ describe("SessionRunnerLLM", () => { yield* session.resume(sessionID) expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([ - [defaultSystem, "Initial context\n\nBuild skills"], + [defaultSystem, withCodeModeGuidance("Initial context", "Build skills")], ]) }), ) @@ -1602,7 +1628,7 @@ describe("SessionRunnerLLM", () => { yield* session.resume(sessionID) expect(requests.map((request) => request.model)).toEqual([model]) expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([ - [defaultSystem, "Initial context"], + [defaultSystem, withCodeModeGuidance("Initial context")], ]) }), ) @@ -1637,7 +1663,10 @@ describe("SessionRunnerLLM", () => { // String values render verbatim inside the initial tagged block. expect(requests[0]?.system.map((part) => part.text)).toEqual([ defaultSystem, - ["Initial context", "", '', "production", ""].join("\n"), + withCodeModeGuidance( + "Initial context", + ['', "production", ""].join("\n"), + ), ]) // Non-string JSON pretty-prints; the change narrates as a System update. @@ -1735,9 +1764,9 @@ describe("SessionRunnerLLM", () => { yield* session.resume(sessionID) expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([ - [defaultSystem, "Initial context"], - [defaultSystem, "Initial context"], - [defaultSystem, "Initial context"], + [defaultSystem, withCodeModeGuidance("Initial context")], + [defaultSystem, withCodeModeGuidance("Initial context")], + [defaultSystem, withCodeModeGuidance("Initial context")], ]) expect(requests[1]?.messages.map((message) => message.role)).toEqual(["user", "system", "user"]) expect(requests[2]?.messages.filter((message) => message.role === "system")).toHaveLength(2) @@ -1774,9 +1803,9 @@ describe("SessionRunnerLLM", () => { yield* session.resume(sessionID) expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([ - [defaultSystem, "Initial context"], - [defaultSystem, "Initial context"], - [defaultSystem, "Initial context"], + [defaultSystem, withCodeModeGuidance("Initial context")], + [defaultSystem, withCodeModeGuidance("Initial context")], + [defaultSystem, withCodeModeGuidance("Initial context")], ]) }), ) @@ -1804,8 +1833,8 @@ describe("SessionRunnerLLM", () => { yield* session.resume(sessionID) expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([ - [defaultSystem, "Initial context"], - [defaultSystem, "Initial context"], + [defaultSystem, withCodeModeGuidance("Initial context")], + [defaultSystem, withCodeModeGuidance("Initial context")], ]) expect(requests[1]?.messages.map((message) => message.role)).toEqual(["user", "system", "user"]) expect(requests[1]?.messages.at(1)?.content).toEqual([{ type: "text", text: "Replacement context" }]) @@ -2340,7 +2369,10 @@ describe("SessionRunnerLLM", () => { yield* session.resume(sessionID) // Compaction already moved current values into the new epoch before the unavailable read. - expect(requests.at(-1)?.system.map((part) => part.text)).toEqual([defaultSystem, "Changed context"]) + expect(requests.at(-1)?.system.map((part) => part.text)).toEqual([ + defaultSystem, + withCodeModeGuidance("Changed context"), + ]) expect(systemTexts(requests.at(-1)!)).not.toContain("Changed context") }), ) @@ -2398,7 +2430,7 @@ describe("SessionRunnerLLM", () => { yield* session.resume(sessionID) expect(requests).toHaveLength(1) - expect(requests[0]?.tools.map((tool) => tool.name)).toEqual(["defect", "echo", "storefail"]) + expect(requests[0]?.tools.map((tool) => tool.name)).toEqual(["defect", "echo", "storefail", "execute"]) expect(yield* session.context(sessionID)).toMatchObject([ { type: "user", text: "Use tools" }, { @@ -2506,8 +2538,8 @@ describe("SessionRunnerLLM", () => { expect(requests.map((request) => request.model)).toEqual([model, replacementModel]) expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([ - [defaultSystem, "Initial context"], - [defaultSystem, "Initial context"], + [defaultSystem, withCodeModeGuidance("Initial context")], + [defaultSystem, withCodeModeGuidance("Initial context")], ]) expect(systemTexts(requests[1]!)).toContain("Replacement context") }), @@ -3444,7 +3476,7 @@ describe("SessionRunnerLLM", () => { id: "call-missing", state: { status: "error", - error: { type: "tool.unknown", message: "Unknown tool: missing" }, + error: { type: "tool.execution", message: "Unknown tool: missing" }, }, }, ], From 0f0459b56e5be3362272c6479771afd1cb27cd4e Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 27 Jul 2026 02:09:46 -0700 Subject: [PATCH 139/150] test(core): isolate ambient config discovery --- packages/core/test/config/config.test.ts | 13 +++++++------ packages/core/test/config/plugin.test.ts | 13 +++++++++++-- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index 5c94cc59ea66..f3333ac33496 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -484,7 +484,7 @@ describe("Config", () => { const config = yield* Config.Service const entries = yield* config.entries() - expect(entries).toEqual([ + expect(entries.filter((entry) => entry.path?.startsWith(`${tmp.path}${path.sep}`) === true)).toEqual([ new Config.Directory({ type: "directory", path: AbsolutePath.make(path.join(tmp.path, "global")) }), ]) }).pipe(Effect.provide(testLayer(tmp.path))), @@ -1155,19 +1155,20 @@ describe("Config", () => { return yield* Effect.gen(function* () { const config = yield* Config.Service const entries = yield* config.entries() - const documents = entries.filter((entry) => entry.type === "document") + const owned = entries.filter((entry) => entry.path?.startsWith(`${tmp.path}${path.sep}`) === true) + const documents = owned.filter((entry) => entry.type === "document") - expect(entries.filter((entry) => entry.type === "directory").map((entry) => entry.path)).toEqual([ + expect(owned.filter((entry) => entry.type === "directory").map((entry) => entry.path)).toEqual([ AbsolutePath.make(global), AbsolutePath.make(path.join(root, ".opencode")), AbsolutePath.make(path.join(directory, ".opencode")), ]) - expect(entries.filter((entry) => entry.type === "agents").map((entry) => entry.path)).toEqual([ + expect(owned.filter((entry) => entry.type === "agents").map((entry) => entry.path)).toEqual([ AbsolutePath.make(globalAgents), AbsolutePath.make(path.join(directory, ".agents")), AbsolutePath.make(path.join(root, ".agents")), ]) - expect(entries.filter((entry) => entry.type === "claude").map((entry) => entry.path)).toEqual([ + expect(owned.filter((entry) => entry.type === "claude").map((entry) => entry.path)).toEqual([ AbsolutePath.make(globalClaude), AbsolutePath.make(path.join(directory, ".claude")), AbsolutePath.make(path.join(root, ".claude")), @@ -1181,7 +1182,7 @@ describe("Config", () => { "root-dot", "directory-dot", ]) - expect(entries.map((entry) => (entry.type === "document" ? entry.info.$schema : entry.path))).toEqual([ + expect(owned.map((entry) => (entry.type === "document" ? entry.info.$schema : entry.path))).toEqual([ AbsolutePath.make(globalClaude), AbsolutePath.make(path.join(directory, ".claude")), AbsolutePath.make(path.join(root, ".claude")), diff --git a/packages/core/test/config/plugin.test.ts b/packages/core/test/config/plugin.test.ts index c558dd869e81..90e306b7f5da 100644 --- a/packages/core/test/config/plugin.test.ts +++ b/packages/core/test/config/plugin.test.ts @@ -17,13 +17,22 @@ import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" import { Model } from "@opencode-ai/core/model" import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" +import { Global } from "@opencode-ai/util/global" import { Effect, Logger } from "effect" import { Database } from "../../src/database/database" import { tmpdir } from "../fixture/tmpdir" import { testEffect } from "../lib/effect" const it = testEffect( - AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SdkPlugins.node, LocationServiceMap.node])), + AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SdkPlugins.node, LocationServiceMap.node]), [ + [ + Global.node, + Global.layerWith({ + config: path.join(import.meta.dir, "fixtures", "global", "config"), + home: path.join(import.meta.dir, "fixtures", "global", "home"), + }), + ], + ]), ) describe("PluginSupervisor config", () => { @@ -303,7 +312,7 @@ function withLocation( } function mutablePlugin(description: string) { - const plugin = pathToFileURL(path.join(import.meta.dir, "../../../plugin/src/index.ts")).href + const plugin = pathToFileURL(path.join(import.meta.dir, "../../../plugin/src/promise/index.ts")).href return ` import { Plugin } from ${JSON.stringify(plugin)} From 9279fe08ab5f13c78759feccc856dfb96c55d0fc Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 27 Jul 2026 02:10:45 -0700 Subject: [PATCH 140/150] test(schema): align agent color contract --- packages/schema/test/contract-hygiene.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/schema/test/contract-hygiene.test.ts b/packages/schema/test/contract-hygiene.test.ts index f9b379e8e08f..58b7fe066c6b 100644 --- a/packages/schema/test/contract-hygiene.test.ts +++ b/packages/schema/test/contract-hygiene.test.ts @@ -20,12 +20,6 @@ import { PersistedRevert } from "../src/session-revert.js" import { optional } from "../src/schema.js" describe("contract hygiene", () => { - test("restricts agent colors to six-digit hex values", () => { - const decode = Schema.decodeUnknownSync(Agent.Color) - expect(decode("#ff6b6b")).toBe("#ff6b6b") - expect(() => decode("warning")).toThrow() - }) - test("keeps absolute costs distinct from model rates", () => { const usd = Money.USD.make(1) const rate = Money.USDPerMillionTokens.make(1) From bc016940ea3573c58be3f4e7075a8b11bc0d7999 Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 27 Jul 2026 02:14:08 -0700 Subject: [PATCH 141/150] fix(app): complete locale key parity --- packages/app/src/i18n/ar.ts | 5 +++++ packages/app/src/i18n/br.ts | 5 +++++ packages/app/src/i18n/bs.ts | 5 +++++ packages/app/src/i18n/da.ts | 5 +++++ packages/app/src/i18n/de.ts | 5 +++++ packages/app/src/i18n/es.ts | 5 +++++ packages/app/src/i18n/fr.ts | 5 +++++ packages/app/src/i18n/ja.ts | 5 +++++ packages/app/src/i18n/ko.ts | 5 +++++ packages/app/src/i18n/no.ts | 5 +++++ packages/app/src/i18n/pl.ts | 5 +++++ packages/app/src/i18n/ru.ts | 5 +++++ packages/app/src/i18n/th.ts | 5 +++++ packages/app/src/i18n/tr.ts | 5 +++++ packages/app/src/i18n/uk.ts | 5 +++++ packages/app/src/i18n/zh.ts | 5 +++++ packages/app/src/i18n/zht.ts | 5 +++++ 17 files changed, 85 insertions(+) diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 4fcd29ed8a83..764c40a9d66f 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -94,6 +94,7 @@ export const dict = { "dialog.provider.empty": "لم يتم العثور على موفرين", "dialog.provider.group.popular": "شائع", "dialog.provider.group.other": "آخر", + "dialog.provider.custom.label": "موفر مخصص متوافق مع OpenAI", "dialog.provider.tag.recommended": "موصى به", "dialog.provider.opencode.note": "نماذج مختارة تتضمن Claude و GPT و Gemini والمزيد", "dialog.provider.opencode.tagline": "نماذج موثوقة ومحسنة", @@ -112,6 +113,7 @@ export const dict = { "dialog.model.manage.provider.toggle": "تبديل جميع نماذج {{provider}}", "dialog.model.unpaid.freeModels.title": "نماذج مجانية مقدمة من OpenCode", "dialog.model.unpaid.addMore.title": "إضافة المزيد من النماذج من موفرين مشهورين", + "dialog.model.unpaid.viewMoreProviders": "عرض أكثر من 70 موفرًا إضافيًا", "dialog.provider.viewAll": "عرض المزيد من الموفرين", "provider.connect.title": "اتصال {{provider}}", "provider.connect.title.anthropicProMax": "تسجيل الدخول باستخدام Claude Pro/Max", @@ -942,6 +944,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "مستكشف الملفات", "session.header.open.fileManager": "مدير الملفات", + "session.header.reveal.finder": "إظهار في Finder", + "session.header.reveal.fileExplorer": "إظهار في مستكشف الملفات", + "session.header.reveal.containingFolder": "فتح المجلد الذي يحتوي على الملف", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index a254982cca52..a5b30dab7698 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -94,6 +94,7 @@ export const dict = { "dialog.provider.empty": "Nenhum provedor encontrado", "dialog.provider.group.popular": "Popular", "dialog.provider.group.other": "Outro", + "dialog.provider.custom.label": "Provedor personalizado compatível com OpenAI", "dialog.provider.tag.recommended": "Recomendado", "dialog.provider.opencode.note": "Modelos selecionados incluindo Claude, GPT, Gemini e mais", "dialog.provider.opencode.tagline": "Modelos otimizados e confiáveis", @@ -112,6 +113,7 @@ export const dict = { "dialog.model.manage.provider.toggle": "Alternar todos os modelos {{provider}}", "dialog.model.unpaid.freeModels.title": "Modelos gratuitos fornecidos pelo OpenCode", "dialog.model.unpaid.addMore.title": "Adicionar mais modelos de provedores populares", + "dialog.model.unpaid.viewMoreProviders": "Ver mais de 70 provedores", "dialog.provider.viewAll": "Ver mais provedores", "provider.connect.title": "Conectar {{provider}}", "provider.connect.title.anthropicProMax": "Entrar com Claude Pro/Max", @@ -956,6 +958,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "Explorador de Arquivos", "session.header.open.fileManager": "Gerenciador de Arquivos", + "session.header.reveal.finder": "Mostrar no Finder", + "session.header.reveal.fileExplorer": "Mostrar no Explorador de Arquivos", + "session.header.reveal.containingFolder": "Abrir pasta que contém o arquivo", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 190d1dfc0086..cd5bab148dd7 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -102,6 +102,7 @@ export const dict = { "dialog.provider.empty": "Nema pronađenih provajdera", "dialog.provider.group.popular": "Popularno", "dialog.provider.group.other": "Ostalo", + "dialog.provider.custom.label": "Prilagođeni provajder kompatibilan s OpenAI-jem", "dialog.provider.tag.recommended": "Preporučeno", "dialog.provider.opencode.note": "Kurirani modeli uključujući Claude, GPT, Gemini i druge", "dialog.provider.opencode.tagline": "Pouzdani optimizovani modeli", @@ -122,6 +123,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Besplatni modeli koje obezbjeđuje OpenCode", "dialog.model.unpaid.addMore.title": "Dodaj još modela od popularnih provajdera", + "dialog.model.unpaid.viewMoreProviders": "Pogledaj još 70+ provajdera", "dialog.provider.viewAll": "Prikaži više provajdera", @@ -1032,6 +1034,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "File Explorer", "session.header.open.fileManager": "File Manager", + "session.header.reveal.finder": "Prikaži u Finderu", + "session.header.reveal.fileExplorer": "Prikaži u File Exploreru", + "session.header.reveal.containingFolder": "Otvori mapu koja sadrži datoteku", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 09455b90a6ec..406459dce78c 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -102,6 +102,7 @@ export const dict = { "dialog.provider.empty": "Ingen udbydere fundet", "dialog.provider.group.popular": "Populære", "dialog.provider.group.other": "Andre", + "dialog.provider.custom.label": "Tilpasset OpenAI-kompatibel udbyder", "dialog.provider.tag.recommended": "Anbefalet", "dialog.provider.opencode.note": "Udvalgte modeller inklusive Claude, GPT, Gemini og flere", "dialog.provider.opencode.tagline": "Pålidelige optimerede modeller", @@ -122,6 +123,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Gratis modeller leveret af OpenCode", "dialog.model.unpaid.addMore.title": "Tilføj flere modeller fra populære udbydere", + "dialog.model.unpaid.viewMoreProviders": "Se mere end 70 yderligere udbydere", "dialog.provider.viewAll": "Vis flere udbydere", @@ -1024,6 +1026,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "Stifinder", "session.header.open.fileManager": "Filhåndtering", + "session.header.reveal.finder": "Vis i Finder", + "session.header.reveal.fileExplorer": "Vis i Stifinder", + "session.header.reveal.containingFolder": "Åbn mappen med filen", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 5e8dd9bace34..ae0baf0eb9ee 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -98,6 +98,7 @@ export const dict = { "dialog.provider.empty": "Keine Anbieter gefunden", "dialog.provider.group.popular": "Beliebt", "dialog.provider.group.other": "Andere", + "dialog.provider.custom.label": "Benutzerdefinierter OpenAI-kompatibler Anbieter", "dialog.provider.tag.recommended": "Empfohlen", "dialog.provider.opencode.note": "Kuratierte Modelle inklusive Claude, GPT, Gemini und mehr", "dialog.provider.opencode.tagline": "Zuverlässige, optimierte Modelle", @@ -116,6 +117,7 @@ export const dict = { "dialog.model.manage.provider.toggle": "Alle {{provider}}-Modelle umschalten", "dialog.model.unpaid.freeModels.title": "Kostenlose Modelle von OpenCode", "dialog.model.unpaid.addMore.title": "Weitere Modelle von beliebten Anbietern hinzufügen", + "dialog.model.unpaid.viewMoreProviders": "Über 70 weitere Anbieter anzeigen", "dialog.provider.viewAll": "Mehr Anbieter anzeigen", "provider.connect.title": "{{provider}} verbinden", "provider.connect.title.anthropicProMax": "Mit Claude Pro/Max anmelden", @@ -969,6 +971,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "Datei-Explorer", "session.header.open.fileManager": "Dateimanager", + "session.header.reveal.finder": "Im Finder anzeigen", + "session.header.reveal.fileExplorer": "Im Datei-Explorer anzeigen", + "session.header.reveal.containingFolder": "Enthaltenden Ordner öffnen", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 9a7bfe825f14..9248a0129a31 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -102,6 +102,7 @@ export const dict = { "dialog.provider.empty": "No se encontraron proveedores", "dialog.provider.group.popular": "Popular", "dialog.provider.group.other": "Otro", + "dialog.provider.custom.label": "Proveedor personalizado compatible con OpenAI", "dialog.provider.tag.recommended": "Recomendado", "dialog.provider.opencode.note": "Modelos seleccionados incluyendo Claude, GPT, Gemini y más", "dialog.provider.opencode.tagline": "Modelos optimizados y fiables", @@ -122,6 +123,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Modelos gratuitos proporcionados por OpenCode", "dialog.model.unpaid.addMore.title": "Añadir más modelos de proveedores populares", + "dialog.model.unpaid.viewMoreProviders": "Ver más de 70 proveedores", "dialog.provider.viewAll": "Ver más proveedores", @@ -1040,6 +1042,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "Explorador de archivos", "session.header.open.fileManager": "Gestor de archivos", + "session.header.reveal.finder": "Mostrar en Finder", + "session.header.reveal.fileExplorer": "Mostrar en el Explorador de archivos", + "session.header.reveal.containingFolder": "Abrir la carpeta contenedora", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 48d852a28152..11b3dbbc3acd 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -94,6 +94,7 @@ export const dict = { "dialog.provider.empty": "Aucun fournisseur trouvé", "dialog.provider.group.popular": "Populaire", "dialog.provider.group.other": "Autre", + "dialog.provider.custom.label": "Fournisseur personnalisé compatible avec OpenAI", "dialog.provider.tag.recommended": "Recommandé", "dialog.provider.opencode.note": "Modèles sélectionnés incluant Claude, GPT, Gemini et plus", "dialog.provider.opencode.tagline": "Modèles optimisés et fiables", @@ -112,6 +113,7 @@ export const dict = { "dialog.model.manage.provider.toggle": "Basculer tous les modèles {{provider}}", "dialog.model.unpaid.freeModels.title": "Modèles gratuits fournis par OpenCode", "dialog.model.unpaid.addMore.title": "Ajouter plus de modèles de fournisseurs populaires", + "dialog.model.unpaid.viewMoreProviders": "Voir plus de 70 fournisseurs supplémentaires", "dialog.provider.viewAll": "Voir plus de fournisseurs", "provider.connect.title": "Connecter {{provider}}", "provider.connect.title.anthropicProMax": "Connexion avec Claude Pro/Max", @@ -967,6 +969,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "Explorateur de fichiers", "session.header.open.fileManager": "Gestionnaire de fichiers", + "session.header.reveal.finder": "Afficher dans le Finder", + "session.header.reveal.fileExplorer": "Afficher dans l’Explorateur de fichiers", + "session.header.reveal.containingFolder": "Ouvrir le dossier contenant le fichier", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 20078866f873..9242283df318 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -94,6 +94,7 @@ export const dict = { "dialog.provider.empty": "プロバイダーが見つかりません", "dialog.provider.group.popular": "人気", "dialog.provider.group.other": "その他", + "dialog.provider.custom.label": "カスタム OpenAI 互換プロバイダー", "dialog.provider.tag.recommended": "推奨", "dialog.provider.opencode.note": "Claude, GPT, Geminiなどを含む厳選されたモデル", "dialog.provider.opencode.tagline": "信頼性の高い最適化モデル", @@ -112,6 +113,7 @@ export const dict = { "dialog.model.manage.provider.toggle": "すべての{{provider}}モデルを切り替え", "dialog.model.unpaid.freeModels.title": "OpenCodeが提供する無料モデル", "dialog.model.unpaid.addMore.title": "人気のプロバイダーからモデルを追加", + "dialog.model.unpaid.viewMoreProviders": "他の70以上のプロバイダーを表示", "dialog.provider.viewAll": "さらにプロバイダーを表示", "provider.connect.title": "{{provider}}を接続", "provider.connect.title.anthropicProMax": "Claude Pro/Maxでログイン", @@ -949,6 +951,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "エクスプローラー", "session.header.open.fileManager": "ファイルマネージャー", + "session.header.reveal.finder": "Finderで表示", + "session.header.reveal.fileExplorer": "エクスプローラーで表示", + "session.header.reveal.containingFolder": "保存先フォルダーを開く", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 40bf0453d74f..24412f8ddec4 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -90,6 +90,7 @@ export const dict = { "dialog.provider.empty": "공급자 없음", "dialog.provider.group.popular": "인기", "dialog.provider.group.other": "기타", + "dialog.provider.custom.label": "사용자 지정 OpenAI 호환 공급자", "dialog.provider.tag.recommended": "추천", "dialog.provider.opencode.note": "Claude, GPT, Gemini 등을 포함한 엄선된 모델", "dialog.provider.opencode.tagline": "신뢰할 수 있는 최적화 모델", @@ -108,6 +109,7 @@ export const dict = { "dialog.model.manage.provider.toggle": "모든 {{provider}} 모델 토글", "dialog.model.unpaid.freeModels.title": "OpenCode에서 제공하는 무료 모델", "dialog.model.unpaid.addMore.title": "인기 공급자의 모델 추가", + "dialog.model.unpaid.viewMoreProviders": "70개 이상의 공급자 더 보기", "dialog.provider.viewAll": "더 많은 공급자 보기", "provider.connect.title": "{{provider}} 연결", "provider.connect.title.anthropicProMax": "Claude Pro/Max로 로그인", @@ -790,6 +792,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "파일 탐색기", "session.header.open.fileManager": "파일 관리자", + "session.header.reveal.finder": "Finder에서 보기", + "session.header.reveal.fileExplorer": "파일 탐색기에서 보기", + "session.header.reveal.containingFolder": "파일이 있는 폴더 열기", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 294945710ac6..0adcd765f4ae 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -101,6 +101,7 @@ export const dict = { "dialog.provider.empty": "Ingen leverandører funnet", "dialog.provider.group.popular": "Populære", "dialog.provider.group.other": "Andre", + "dialog.provider.custom.label": "Tilpasset OpenAI-kompatibel leverandør", "dialog.provider.tag.recommended": "Anbefalt", "dialog.provider.opencode.note": "Utvalgte modeller inkludert Claude, GPT, Gemini og mer", "dialog.provider.opencode.tagline": "Pålitelige, optimaliserte modeller", @@ -121,6 +122,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Gratis modeller levert av OpenCode", "dialog.model.unpaid.addMore.title": "Legg til flere modeller fra populære leverandører", + "dialog.model.unpaid.viewMoreProviders": "Se over 70 flere leverandører", "dialog.provider.viewAll": "Vis flere leverandører", @@ -877,6 +879,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "Filutforsker", "session.header.open.fileManager": "Filbehandler", + "session.header.reveal.finder": "Vis i Finder", + "session.header.reveal.fileExplorer": "Vis i Filutforsker", + "session.header.reveal.containingFolder": "Åpne mappen som inneholder filen", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 29ac3822c913..221cbb6fdbeb 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -94,6 +94,7 @@ export const dict = { "dialog.provider.empty": "Nie znaleziono dostawców", "dialog.provider.group.popular": "Popularne", "dialog.provider.group.other": "Inne", + "dialog.provider.custom.label": "Niestandardowy dostawca zgodny z OpenAI", "dialog.provider.tag.recommended": "Zalecane", "dialog.provider.opencode.note": "Wyselekcjonowane modele, w tym Claude, GPT, Gemini i inne", "dialog.provider.opencode.tagline": "Niezawodne, zoptymalizowane modele", @@ -112,6 +113,7 @@ export const dict = { "dialog.model.manage.provider.toggle": "Przełącz wszystkie modele {{provider}}", "dialog.model.unpaid.freeModels.title": "Darmowe modele dostarczane przez OpenCode", "dialog.model.unpaid.addMore.title": "Dodaj więcej modeli od popularnych dostawców", + "dialog.model.unpaid.viewMoreProviders": "Zobacz ponad 70 dodatkowych dostawców", "dialog.provider.viewAll": "Zobacz więcej dostawców", "provider.connect.title": "Połącz {{provider}}", "provider.connect.title.anthropicProMax": "Zaloguj się z Claude Pro/Max", @@ -955,6 +957,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "Eksplorator plików", "session.header.open.fileManager": "Menedżer plików", + "session.header.reveal.finder": "Pokaż w Finderze", + "session.header.reveal.fileExplorer": "Pokaż w Eksploratorze plików", + "session.header.reveal.containingFolder": "Otwórz folder zawierający plik", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 25e9f9d1186e..1260ef47f3d8 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -102,6 +102,7 @@ export const dict = { "dialog.provider.empty": "Провайдеры не найдены", "dialog.provider.group.popular": "Популярные", "dialog.provider.group.other": "Другие", + "dialog.provider.custom.label": "Пользовательский провайдер, совместимый с OpenAI", "dialog.provider.tag.recommended": "Рекомендуемые", "dialog.provider.opencode.note": "Отобранные модели, включая Claude, GPT, Gemini и другие", "dialog.provider.opencode.tagline": "Надежные оптимизированные модели", @@ -122,6 +123,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Бесплатные модели от OpenCode", "dialog.model.unpaid.addMore.title": "Добавьте больше моделей от популярных провайдеров", + "dialog.model.unpaid.viewMoreProviders": "Показать ещё 70+ провайдеров", "dialog.provider.viewAll": "Показать больше провайдеров", @@ -1035,6 +1037,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "Проводник", "session.header.open.fileManager": "Файловый менеджер", + "session.header.reveal.finder": "Показать в Finder", + "session.header.reveal.fileExplorer": "Показать в Проводнике", + "session.header.reveal.containingFolder": "Открыть папку с файлом", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 8c73ddd8b385..815b86001e0a 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -102,6 +102,7 @@ export const dict = { "dialog.provider.empty": "ไม่พบผู้ให้บริการ", "dialog.provider.group.popular": "ยอดนิยม", "dialog.provider.group.other": "อื่น ๆ", + "dialog.provider.custom.label": "ผู้ให้บริการแบบกำหนดเองที่เข้ากันได้กับ OpenAI", "dialog.provider.tag.recommended": "แนะนำ", "dialog.provider.opencode.note": "โมเดลที่คัดสรร รวมถึง Claude, GPT, Gemini และอื่น ๆ", "dialog.provider.opencode.tagline": "โมเดลที่เชื่อถือได้และปรับให้เหมาะสม", @@ -122,6 +123,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "โมเดลฟรีที่จัดหาให้โดย OpenCode", "dialog.model.unpaid.addMore.title": "เพิ่มโมเดลเพิ่มเติมจากผู้ให้บริการยอดนิยม", + "dialog.model.unpaid.viewMoreProviders": "ดูผู้ให้บริการเพิ่มเติมกว่า 70 ราย", "dialog.provider.viewAll": "แสดงผู้ให้บริการเพิ่มเติม", @@ -1019,6 +1021,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "File Explorer", "session.header.open.fileManager": "File Manager", + "session.header.reveal.finder": "แสดงใน Finder", + "session.header.reveal.fileExplorer": "แสดงใน File Explorer", + "session.header.reveal.containingFolder": "เปิดโฟลเดอร์ที่มีไฟล์", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts index cd5126dfd078..1169f87cdaaf 100644 --- a/packages/app/src/i18n/tr.ts +++ b/packages/app/src/i18n/tr.ts @@ -106,6 +106,7 @@ export const dict = { "dialog.provider.empty": "Sağlayıcı bulunamadı", "dialog.provider.group.popular": "Popüler", "dialog.provider.group.other": "Diğer", + "dialog.provider.custom.label": "Özel OpenAI uyumlu sağlayıcı", "dialog.provider.tag.recommended": "Önerilen", "dialog.provider.opencode.note": "Claude, GPT, Gemini ve daha fazlasını içeren seçilmiş modeller", "dialog.provider.opencode.tagline": "Güvenilir optimize edilmiş modeller", @@ -126,6 +127,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "OpenCode tarafından sunulan ücretsiz modeller", "dialog.model.unpaid.addMore.title": "Popüler sağlayıcılardan daha fazla model ekleyin", + "dialog.model.unpaid.viewMoreProviders": "70'ten fazla sağlayıcıyı daha görüntüle", "dialog.provider.viewAll": "Daha fazla sağlayıcı göster", @@ -1039,6 +1041,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "Dosya Gezgini", "session.header.open.fileManager": "Dosya Yöneticisi", + "session.header.reveal.finder": "Finder'da Göster", + "session.header.reveal.fileExplorer": "Dosya Gezgini'nde Göster", + "session.header.reveal.containingFolder": "Dosyayı içeren klasörü aç", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/uk.ts b/packages/app/src/i18n/uk.ts index bddc2bdffb6d..b2b5793ae667 100644 --- a/packages/app/src/i18n/uk.ts +++ b/packages/app/src/i18n/uk.ts @@ -102,6 +102,7 @@ export const dict = { "dialog.provider.empty": "Провайдерів не знайдено", "dialog.provider.group.popular": "Популярні", "dialog.provider.group.other": "Інші", + "dialog.provider.custom.label": "Користувацький провайдер, сумісний з OpenAI", "dialog.provider.tag.recommended": "Рекомендовані", "dialog.provider.opencode.note": "Відібрані моделі, включаючи Claude, GPT, Gemini та інші", "dialog.provider.opencode.tagline": "Надійні оптимізовані моделі", @@ -122,6 +123,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Безкоштовні моделі від OpenCode", "dialog.model.unpaid.addMore.title": "Додати більше моделей від популярних провайдерів", + "dialog.model.unpaid.viewMoreProviders": "Показати ще понад 70 провайдерів", "dialog.provider.viewAll": "Показати більше провайдерів", @@ -712,6 +714,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "Провідник файлів", "session.header.open.fileManager": "Файловий менеджер", + "session.header.reveal.finder": "Показати у Finder", + "session.header.reveal.fileExplorer": "Показати у Провіднику", + "session.header.reveal.containingFolder": "Відкрити папку з файлом", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index daa22e214b91..d29d1acc171a 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -129,6 +129,7 @@ export const dict = { "dialog.provider.empty": "未找到提供商", "dialog.provider.group.popular": "热门", "dialog.provider.group.other": "其他", + "dialog.provider.custom.label": "自定义 OpenAI 兼容提供商", "dialog.provider.tag.recommended": "推荐", "dialog.provider.opencode.note": "使用 OpenCode Zen 或 API 密钥连接", "dialog.provider.opencode.tagline": "可靠的优化模型", @@ -148,6 +149,7 @@ export const dict = { "dialog.model.manage.provider.toggle": "切换所有 {{provider}} 模型", "dialog.model.unpaid.freeModels.title": "OpenCode 提供的免费模型", "dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型", + "dialog.model.unpaid.viewMoreProviders": "查看另外 70 多个提供商", "dialog.provider.viewAll": "查看更多提供商", @@ -1012,6 +1014,9 @@ export const dict = { "session.header.open.finder": "访达", "session.header.open.fileExplorer": "文件资源管理器", "session.header.open.fileManager": "文件管理器", + "session.header.reveal.finder": "在“访达”中显示", + "session.header.reveal.fileExplorer": "在文件资源管理器中显示", + "session.header.reveal.containingFolder": "打开所在文件夹", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index cab3f67137d2..966828ed3871 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -106,6 +106,7 @@ export const dict = { "dialog.provider.empty": "找不到提供者", "dialog.provider.group.popular": "熱門", "dialog.provider.group.other": "其他", + "dialog.provider.custom.label": "自訂 OpenAI 相容提供者", "dialog.provider.tag.recommended": "推薦", "dialog.provider.opencode.note": "精選模型,包含 Claude、GPT、Gemini 等等", "dialog.provider.opencode.tagline": "可靠的優化模型", @@ -126,6 +127,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "OpenCode 提供的免費模型", "dialog.model.unpaid.addMore.title": "從熱門提供者新增更多模型", + "dialog.model.unpaid.viewMoreProviders": "查看另外 70 多個提供者", "dialog.provider.viewAll": "查看更多提供者", @@ -1008,6 +1010,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "檔案總管", "session.header.open.fileManager": "檔案管理員", + "session.header.reveal.finder": "在 Finder 中顯示", + "session.header.reveal.fileExplorer": "在檔案總管中顯示", + "session.header.reveal.containingFolder": "開啟所在資料夾", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", From c32acef6278bc0770fae71c166554cc84a6524e4 Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 27 Jul 2026 02:22:49 -0700 Subject: [PATCH 142/150] test(core): fix consolidated tool fixtures --- packages/core/test/codemode.test.ts | 11 ++++++++++- packages/core/test/plugin/promise.test.ts | 1 - 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/core/test/codemode.test.ts b/packages/core/test/codemode.test.ts index 0de0096b188d..2c3776b74121 100644 --- a/packages/core/test/codemode.test.ts +++ b/packages/core/test/codemode.test.ts @@ -1,5 +1,7 @@ import { describe, expect } from "bun:test" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { Location } from "@opencode-ai/core/location" +import { AbsolutePath } from "@opencode-ai/core/schema" import { Tool } from "@opencode-ai/core/tool" import { Effect, Schema } from "effect" import { it } from "./lib/effect" @@ -27,6 +29,13 @@ describe("CodeMode", () => { signature: "tools.echo(input: {\n text: string,\n}): Promise", }, ]) - }).pipe(Effect.scoped, Effect.provide(AppNodeBuilder.build(Tool.node))), + }).pipe( + Effect.scoped, + Effect.provide( + AppNodeBuilder.build(Tool.node, [ + [Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })], + ]), + ), + ), ) }) diff --git a/packages/core/test/plugin/promise.test.ts b/packages/core/test/plugin/promise.test.ts index 2730680ccd4c..064e083ac738 100644 --- a/packages/core/test/plugin/promise.test.ts +++ b/packages/core/test/plugin/promise.test.ts @@ -346,7 +346,6 @@ describe("fromPromise", () => { call: { type: "tool-call", id: "call_promise_tool", name: "hello", input: { name: "world" } }, }), ).toMatchObject({ - status: "completed", output: "Hello, world!", content: [{ type: "text", text: "Hello, world!" }], }) From 0afb4415f1d58da38bc67eb95fa5c4665db0bd20 Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 27 Jul 2026 02:29:14 -0700 Subject: [PATCH 143/150] fix(core): validate tool result metadata --- packages/core/src/tool.ts | 36 +++++++- packages/core/test/plugin.test.ts | 102 ++++++++++++++++++++++ packages/core/test/session-runner.test.ts | 24 +++-- 3 files changed, 153 insertions(+), 9 deletions(-) diff --git a/packages/core/src/tool.ts b/packages/core/src/tool.ts index 9c29d4ff2be6..4d0c4d8b54a1 100644 --- a/packages/core/src/tool.ts +++ b/packages/core/src/tool.ts @@ -17,6 +17,9 @@ import { SessionSchema } from "./session/schema" import { definition, execute, normalizeContent } from "./tool/runtime" import { Wildcard } from "./util/wildcard" +const MAX_METADATA_BYTES = 64 * 1024 +const Metadata = Schema.Record(Schema.String, Schema.Json) + export class RegistrationError extends Schema.TaggedErrorClass()("Tool.RegistrationError", { name: Schema.String, message: Schema.String, @@ -49,6 +52,32 @@ const layer = Layer.effect( const hooks = yield* PluginHooks.Service const image = yield* Image.Service + const terminalMetadata = Effect.fn("Tool.terminalMetadata")(function* ( + tool: string, + callID: string, + metadata: Tool.Metadata | undefined, + ) { + if (metadata === undefined) return undefined + const validation = (() => { + try { + if (!Schema.is(Metadata)(metadata)) return { reason: "not valid JSON" } + const bytes = Buffer.byteLength(JSON.stringify(metadata), "utf8") + if (bytes > MAX_METADATA_BYTES) return { reason: "exceeds size limit", bytes } + return { metadata } + } catch { + return { reason: "not valid JSON" } + } + })() + if ("metadata" in validation) return validation.metadata + yield* Effect.logWarning("Dropping tool result metadata", { + tool, + callID, + reason: validation.reason, + ...(validation.bytes === undefined ? {} : { bytes: validation.bytes, limit: MAX_METADATA_BYTES }), + }) + return undefined + }) + type NormalizedItem = Tool.Content | "decode" | "size" const normalizeImages = Effect.fn("Tool.normalizeImages")(function* (content: ReadonlyArray) { const normalized = yield* Effect.forEach(content, (item): Effect.Effect => { @@ -116,7 +145,9 @@ const layer = Layer.effect( error: execution.failure, } yield* hooks.trigger("tool", "execute.after", afterEvent) - return yield* afterEvent.error + const metadata = yield* terminalMetadata(name, context.callID, afterEvent.error.metadata) + if (metadata === afterEvent.error.metadata) return yield* afterEvent.error + return yield* new Tool.Error({ message: afterEvent.error.message, error: afterEvent.error.error }) } const content = yield* normalizeImages(execution.value.content) const terminal: { result: Tool.Result; replaced: boolean } = { @@ -146,10 +177,11 @@ const layer = Layer.effect( const afterContent = terminal.replaced ? yield* normalizeImages(normalizeContent(terminal.result.content, execution.value.output)) : content + const metadata = yield* terminalMetadata(name, context.callID, terminal.result.metadata) return { ...(execution.value.output === undefined ? {} : { output: execution.value.output }), content: afterContent, - ...(terminal.result.metadata === undefined ? {} : { metadata: terminal.result.metadata }), + ...(metadata === undefined ? {} : { metadata }), } }) diff --git a/packages/core/test/plugin.test.ts b/packages/core/test/plugin.test.ts index 25077743d71a..d3015b34860d 100644 --- a/packages/core/test/plugin.test.ts +++ b/packages/core/test/plugin.test.ts @@ -6,6 +6,7 @@ import { Agent } from "@opencode-ai/core/agent" import { Bus } from "@opencode-ai/core/bus" import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" +import { PluginHooks } from "@opencode-ai/core/plugin/hooks" import { Session } from "@opencode-ai/core/session" import { SessionMessage } from "@opencode-ai/core/session/message" import { Tool } from "@opencode-ai/core/tool" @@ -401,4 +402,105 @@ describe("Plugin", () => { }) }), ) + + it.effect("preserves valid tool result metadata", () => + Effect.gen(function* () { + const registry = yield* Tool.Service + yield* registry.transform((draft) => + draft.add({ + name: "metadata", + options: { codemode: false }, + description: "Return metadata", + input: Schema.Struct({}), + execute: () => Effect.succeed({ content: "ok", metadata: { nested: { valid: true }, count: 2 } }), + }), + ) + const toolSet = yield* registry.snapshot() + + expect( + yield* toolSet.execute({ + sessionID: Session.ID.make("ses_metadata_valid"), + agent: Agent.ID.make("build"), + messageID: SessionMessage.ID.make("msg_metadata_valid"), + call: { type: "tool-call", id: "call-metadata-valid", name: "metadata", input: {} }, + }), + ).toMatchObject({ metadata: { nested: { valid: true }, count: 2 } }) + }), + ) + + it.effect("drops circular, non-JSON, and oversized tool result metadata", () => + Effect.gen(function* () { + const registry = yield* Tool.Service + yield* registry.transform((draft) => { + draft.add({ + name: "invalid_metadata", + options: { codemode: false }, + description: "Return invalid metadata", + input: Schema.Struct({ kind: Schema.Literals(["circular", "non-json"]) }), + execute: ({ kind }) => + Effect.sync(() => { + if (kind === "non-json") return { content: "ok", metadata: { value: 1n } } + const metadata: Record = {} + metadata.self = metadata + return { content: "ok", metadata } + }), + }) + draft.add({ + name: "oversized_metadata", + options: { codemode: false }, + description: "Return oversized metadata", + input: Schema.Struct({}), + execute: () => Effect.succeed({ content: "ok", metadata: { value: "x".repeat(64 * 1024) } }), + }) + }) + const toolSet = yield* registry.snapshot() + const execute = (name: string, input: Record) => + toolSet.execute({ + sessionID: Session.ID.make("ses_metadata_invalid"), + agent: Agent.ID.make("build"), + messageID: SessionMessage.ID.make("msg_metadata_invalid"), + call: { type: "tool-call", id: `call-${name}`, name, input }, + }) + + expect(yield* execute("invalid_metadata", { kind: "circular" })).not.toHaveProperty("metadata") + expect(yield* execute("invalid_metadata", { kind: "non-json" })).not.toHaveProperty("metadata") + expect(yield* execute("oversized_metadata", {})).not.toHaveProperty("metadata") + }), + ) + + it.effect("drops invalid failure metadata after execute.after hooks", () => + Effect.gen(function* () { + const registry = yield* Tool.Service + const hooks = yield* PluginHooks.Service + yield* hooks.register("tool", "execute.after", (event) => + Effect.sync(() => { + if (event.status !== "error") return + const metadata: Record = {} + metadata.self = metadata + event.error = new Tool.Error({ message: event.error.message, metadata }) + }), + ) + yield* registry.transform((draft) => + draft.add({ + name: "failure_metadata", + options: { codemode: false }, + description: "Fail with hook metadata", + input: Schema.Struct({}), + execute: () => new Tool.Error({ message: "failed" }), + }), + ) + const toolSet = yield* registry.snapshot() + const failure = yield* toolSet + .execute({ + sessionID: Session.ID.make("ses_metadata_failure"), + agent: Agent.ID.make("build"), + messageID: SessionMessage.ID.make("msg_metadata_failure"), + call: { type: "tool-call", id: "call-metadata-failure", name: "failure_metadata", input: {} }, + }) + .pipe(Effect.flip) + + expect(failure.message).toBe("failed") + expect(failure).not.toHaveProperty("metadata") + }), + ) }) diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index b9c58ec4a49e..3c99d78ced86 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -286,15 +286,10 @@ const echo = Layer.effectDiscard( }), storefail: ({ name: "storefail", - description: "Produce output that cannot be persisted", + description: "Produce output for a persistence failure test", input: Schema.Struct({}), output: Schema.Struct({}), - execute: () => - Effect.sync(() => { - const metadata: Record = {} - metadata.circular = metadata - return { output: {}, metadata } - }), + execute: () => Effect.succeed({ output: {} }), }), }, { codemode: false }, @@ -481,6 +476,19 @@ const sessionID = Session.ID.make("ses_runner_test") const otherSessionID = Session.ID.make("ses_runner_other") const admit = (session: Session.Interface, text: string) => session.prompt({ sessionID, text, resume: false }) +const failToolSuccessPersistence = Effect.gen(function* () { + const { db } = yield* Database.Service + yield* db.run(` + CREATE TEMP TRIGGER fail_tool_success_persistence + BEFORE INSERT ON event + WHEN NEW.type = 'session.tool.success.2' + BEGIN + SELECT RAISE(FAIL, 'injected tool success persistence failure'); + END + `) + yield* Effect.addFinalizer(() => db.run("DROP TRIGGER fail_tool_success_persistence").pipe(Effect.orDie)) +}) + const insertSession = (id: Session.ID) => Effect.gen(function* () { const { db } = yield* Database.Service @@ -3647,6 +3655,7 @@ describe("SessionRunnerLLM", () => { it.effect("fails the drain when tool output persistence fails", () => Effect.gen(function* () { const session = yield* setup + yield* failToolSuccessPersistence yield* admit(session, "Call storefail") responses = [reply.tool("call-storefail", "storefail", {}), []] @@ -4756,6 +4765,7 @@ describe("SessionRunnerLLM", () => { it.effect("preserves the provider failure when tool output persistence also fails", () => Effect.gen(function* () { const session = yield* setup + yield* failToolSuccessPersistence yield* admit(session, "Storage fails while provider fails") response = [ LLMEvent.stepStart({ index: 0 }), From 5341cdc5f65a517f9b5c8ad615c99e3ae3c5b759 Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 27 Jul 2026 02:32:18 -0700 Subject: [PATCH 144/150] fix(server): reject normalized advertised paths --- packages/server/src/server-info.ts | 3 +++ packages/server/test/server-info.test.ts | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/server/src/server-info.ts b/packages/server/src/server-info.ts index 9bd730b5aa15..f3c2da4a60ea 100644 --- a/packages/server/src/server-info.ts +++ b/packages/server/src/server-info.ts @@ -38,6 +38,9 @@ export function advertisedURLs(values: ReadonlyArray) { function advertisedURL(value: string) { const url = new URL(value) if (!["http:", "https:"].includes(url.protocol)) throw new Error("Advertised URLs must use HTTP or HTTPS") + const rawPath = value.trim().match(/^[a-z][a-z\d+.-]*:\/\/[^/\\?#]*([/\\][^?#]*)?/i)?.[1] + if (rawPath !== undefined && rawPath !== "/") + throw new Error("Advertised URLs cannot contain userinfo, a path, query, or fragment") if (url.username || url.password || url.search || url.hash || url.pathname !== "/") throw new Error("Advertised URLs cannot contain userinfo, a path, query, or fragment") if (url.protocol === "http:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) diff --git a/packages/server/test/server-info.test.ts b/packages/server/test/server-info.test.ts index 3eb4b8b79e6f..8c151ab1b3e5 100644 --- a/packages/server/test/server-info.test.ts +++ b/packages/server/test/server-info.test.ts @@ -3,10 +3,13 @@ import { ServerInfo } from "../src/server-info" describe("ServerInfo.advertisedURLs", () => { test("accepts HTTPS and loopback HTTP independently from the bind address", () => { - expect(ServerInfo.advertisedURLs(["https://shuvdev.example:10001", "http://127.0.0.1:4096"])).toEqual([ - "https://shuvdev.example:10001", - "http://127.0.0.1:4096", - ]) + expect( + ServerInfo.advertisedURLs([ + "https://shuvdev.example:10001", + "https://shuvdev.example:10001/", + "http://127.0.0.1:4096", + ]), + ).toEqual(["https://shuvdev.example:10001", "http://127.0.0.1:4096"]) }) test("rejects unsafe or ambiguous advertised URLs", () => { @@ -20,4 +23,10 @@ describe("ServerInfo.advertisedURLs", () => { expect(() => ServerInfo.advertisedURLs([value])).toThrow() } }) + + test("rejects dot-segment paths before URL normalization", () => { + for (const path of ["/a/..", "/.", "/..", "/%2e", "/%2e%2e"]) { + expect(() => ServerInfo.advertisedURLs([`https://shuvdev.example${path}`])).toThrow() + } + }) }) From e19c5f061fd4ce936ddafa9e7dbd03d61103469b Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 27 Jul 2026 02:39:38 -0700 Subject: [PATCH 145/150] fix(plugin): refresh repository plugin runtime --- .opencode/plugins/github-pr-search.ts | 2 +- .opencode/plugins/github-triage.ts | 2 +- bun.lock | 3 +- package.json | 3 +- .../plugin/test/repository-activation.test.ts | 34 +++++++++++++++++++ 5 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 packages/plugin/test/repository-activation.test.ts diff --git a/.opencode/plugins/github-pr-search.ts b/.opencode/plugins/github-pr-search.ts index c90e3ab77a0f..a40b76c5118c 100644 --- a/.opencode/plugins/github-pr-search.ts +++ b/.opencode/plugins/github-pr-search.ts @@ -1,5 +1,5 @@ /// -import { Plugin } from "@opencode-ai/plugin" +import { Plugin } from "../../packages/plugin/src/promise/index" import { z } from "zod" async function githubFetch(endpoint: string, options: RequestInit = {}) { diff --git a/.opencode/plugins/github-triage.ts b/.opencode/plugins/github-triage.ts index 0344b925df6a..0161571cc1f5 100644 --- a/.opencode/plugins/github-triage.ts +++ b/.opencode/plugins/github-triage.ts @@ -1,5 +1,5 @@ /// -import { Plugin } from "@opencode-ai/plugin" +import { Plugin } from "../../packages/plugin/src/promise/index" import { z } from "zod" const TEAM = { diff --git a/bun.lock b/bun.lock index 1f3efb0c2cef..f0dcbb79c12d 100644 --- a/bun.lock +++ b/bun.lock @@ -11,6 +11,7 @@ "@opencode-ai/sdk": "1.18.5", "heap-snapshot-toolkit": "1.1.3", "typescript": "catalog:", + "zod": "catalog:", }, "devDependencies": { "@actions/artifact": "5.0.1", @@ -6843,8 +6844,6 @@ "yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], - "zod-to-ts/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@actions/github/@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], "@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], diff --git a/package.json b/package.json index de344eac2781..efbb2289a6cc 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,8 @@ "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "1.18.5", "heap-snapshot-toolkit": "1.1.3", - "typescript": "catalog:" + "typescript": "catalog:", + "zod": "catalog:" }, "repository": { "type": "git", diff --git a/packages/plugin/test/repository-activation.test.ts b/packages/plugin/test/repository-activation.test.ts new file mode 100644 index 000000000000..2e0112c38d33 --- /dev/null +++ b/packages/plugin/test/repository-activation.test.ts @@ -0,0 +1,34 @@ +import { afterAll, expect, test } from "bun:test" +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" + +const directory = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-repository-plugin-")) + +afterAll(() => fs.rm(directory, { recursive: true, force: true })) + +test("repository plugins activate with a stale local V1 package", async () => { + const root = path.resolve(import.meta.dir, "../../..") + const plugins = path.join(directory, ".opencode", "plugins") + const stale = path.join(directory, ".opencode", "node_modules", "@opencode-ai", "plugin") + + await fs.mkdir(plugins, { recursive: true }) + await fs.mkdir(stale, { recursive: true }) + await fs.mkdir(path.join(directory, "packages"), { recursive: true }) + await fs.symlink(path.join(root, "packages", "plugin"), path.join(directory, "packages", "plugin"), "dir") + await fs.symlink(path.join(root, "node_modules"), path.join(directory, "node_modules"), "dir") + await fs.writeFile( + path.join(stale, "package.json"), + JSON.stringify({ name: "@opencode-ai/plugin", version: "1.17.17", type: "module", exports: "./index.js" }), + ) + await fs.writeFile(path.join(stale, "index.js"), 'throw new Error("stale V1 plugin package was loaded")\n') + + const ids = await Promise.all( + ["github-triage.ts", "github-pr-search.ts"].map(async (name) => { + await fs.copyFile(path.join(root, ".opencode", "plugins", name), path.join(plugins, name)) + return (await import(path.join(plugins, name))).default.id + }), + ) + + expect(ids).toEqual(["repository.github-triage", "repository.github-pr-search"]) +}) From 020e1edc37e172ca466ed3594baa65a8ad74ef5d Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 27 Jul 2026 02:49:58 -0700 Subject: [PATCH 146/150] fix(ci): restore service lifecycle smoke --- packages/cli/script/service-smoke.ts | 134 +++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 packages/cli/script/service-smoke.ts diff --git a/packages/cli/script/service-smoke.ts b/packages/cli/script/service-smoke.ts new file mode 100644 index 000000000000..391ef51a3e7e --- /dev/null +++ b/packages/cli/script/service-smoke.ts @@ -0,0 +1,134 @@ +#!/usr/bin/env bun + +import { Service } from "@opencode-ai/client/effect/service" +import { ServiceStatus } from "@opencode-ai/protocol/groups/health" +import { Schema } from "effect" +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" + +const nodeBuild = process.argv.includes("--node") +const target = `shuvcode${nodeBuild ? "-node" : ""}-${process.platform === "win32" ? "windows" : process.platform}-${process.arch}` +const directory = path.join(import.meta.dir, "..", "dist", ...(nodeBuild ? ["node"] : []), target, "bin") +const binary = path.join(directory, `shuvcode${nodeBuild ? "-node" : ""}${process.platform === "win32" ? ".exe" : ""}`) +if (!(await Bun.file(binary).exists())) throw new Error(`Missing compiled CLI in ${directory}`) + +const root = await fs.mkdtemp(path.join(os.tmpdir(), "shuvcode-service-smoke-")) +const env = { + ...process.env, + HOME: root, + USERPROFILE: root, + OPENCODE_DB: path.join(root, "opencode.db"), + OPENCODE_TEST_HOME: root, + XDG_CACHE_HOME: path.join(root, "cache"), + XDG_CONFIG_HOME: path.join(root, "config"), + XDG_DATA_HOME: path.join(root, "data"), + XDG_STATE_HOME: path.join(root, "state"), +} +const processes: Array> = [] +const errors: Array> = [] +let failure: unknown +try { + spawnService() + spawnService() + const registration = await waitForRegistration() + const info = await Schema.decodeUnknownPromise(Service.Info)(await Bun.file(registration).json()) + if (info.id === undefined || info.password === undefined) throw new Error("Registration is missing service identity") + const credential = btoa(`opencode:${info.password}`) + const headers = { authorization: "Basic " + credential } + const token = encodeURIComponent(credential) + const health = await waitForReady(info.url, headers) + if (health.pid !== info.pid) throw new Error("Health process does not match registration") + const tokenHealth = await fetch(new URL(`/api/health?auth_token=${token}`, info.url), { + signal: AbortSignal.timeout(5_000), + }) + if (tokenHealth.status !== 200) throw new Error("Compiled service rejected query authentication") + const tokenOpenApi = await fetch(new URL(`/openapi.json?auth_token=${token}`, info.url), { + signal: AbortSignal.timeout(5_000), + }) + if (tokenOpenApi.status !== 200) throw new Error("Compiled application rejected query authentication") + + const unauthorizedHealth = await fetch(new URL("/api/health", info.url), { + signal: AbortSignal.timeout(5_000), + }) + if (unauthorizedHealth.status !== 401) throw new Error("Compiled service exposed health without authentication") + const unauthorizedOpenApi = await fetch(new URL("/openapi.json", info.url), { + signal: AbortSignal.timeout(5_000), + }) + if (unauthorizedOpenApi.status !== 401) + throw new Error("Compiled service exposed application routes without authentication") + const unauthorizedStop = await fetch(new URL("/api/service/stop", info.url), { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ instanceID: info.id }), + signal: AbortSignal.timeout(5_000), + }) + if (unauthorizedStop.status !== 401) throw new Error("Compiled service accepted unauthenticated stop") + + const winner = processes.find((process) => process.pid === info.pid) + const loser = processes.find((process) => process.pid !== info.pid) + if (!winner || !loser) throw new Error("Compiled contenders did not elect one registered owner") + if (!(await exitsWithin(loser, 10_000))) throw new Error("Losing compiled contender did not exit") + + const stopped = await Schema.decodeUnknownPromise(ServiceStatus.StopResponse)( + await fetch(new URL("/api/service/stop", info.url), { + method: "POST", + headers: { ...headers, "content-type": "application/json" }, + body: JSON.stringify({ instanceID: info.id }), + signal: AbortSignal.timeout(5_000), + }).then((response) => response.json()), + ) + if (!stopped.accepted) throw new Error("Compiled service rejected exact-instance stop") + if (!(await exitsWithin(winner, 10_000))) throw new Error("Compiled service did not stop") + for (let attempt = 0; attempt < 200 && (await Bun.file(registration).exists()); attempt++) await Bun.sleep(25) + if (await Bun.file(registration).exists()) throw new Error("Compiled service registration was not removed") +} catch (cause) { + failure = cause +} finally { + processes.forEach((process) => process.kill()) + await Promise.all(processes.map((process) => process.exited)) +} + +const output = await Promise.all(errors) +await fs.rm(root, { recursive: true, force: true }) +if (failure) + throw new Error(output.filter(Boolean).join("\n") || "Compiled service lifecycle smoke test failed", { + cause: failure, + }) + +function spawnService() { + const process = Bun.spawn([binary, "serve", "--service"], { env, stdout: "ignore", stderr: "pipe" }) + processes.push(process) + errors.push(new Response(process.stderr).text()) + return process +} + +async function waitForRegistration() { + const directory = path.join(root, "state", "opencode") + for (let attempt = 0; attempt < 400; attempt++) { + const files = await fs.readdir(directory).catch(() => []) + const file = files.find( + (file) => file === "service.json" || (file.startsWith("service-") && file.endsWith(".json")), + ) + if (file) return path.join(directory, file) + await Bun.sleep(25) + } + throw new Error("Compiled service did not publish registration") +} + +async function waitForReady(url: string, headers: HeadersInit) { + const deadline = Date.now() + 20_000 + while (Date.now() < deadline) { + const response = await fetch(new URL("/api/health", url), { + headers, + signal: AbortSignal.timeout(1_000), + }).catch(() => undefined) + if (response?.ok) return Schema.decodeUnknownPromise(ServiceStatus.Health)(await response.json()) + await Bun.sleep(25) + } + throw new Error("Compiled service did not become ready") +} + +function exitsWithin(process: Bun.Subprocess, milliseconds: number) { + return Promise.race([process.exited.then(() => true), Bun.sleep(milliseconds).then(() => false)]) +} From 0992b5eb18f6acdea1517b7ba883e74b8d945660 Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 27 Jul 2026 02:54:59 -0700 Subject: [PATCH 147/150] fix(ci): align fork publish artifacts --- .github/workflows/publish.yml | 40 +++++++++++++++++------------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 180c6b77c05e..d2e86f28ff17 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -38,7 +38,7 @@ env: jobs: version: runs-on: blacksmith-4vcpu-ubuntu-2404 - if: github.repository == 'anomalyco/opencode' + if: github.repository == 'Latitudes-Dev/shuvcode' steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: @@ -65,7 +65,7 @@ jobs: OPENCODE_BUMP: ${{ inputs.bump }} OPENCODE_VERSION: ${{ inputs.version }} OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - GH_REPO: ${{ (github.ref_name == 'beta' && 'anomalyco/opencode-beta') || github.repository }} + GH_REPO: ${{ github.repository }} outputs: version: ${{ steps.version.outputs.version }} release: ${{ steps.version.outputs.release }} @@ -75,7 +75,7 @@ jobs: build-cli: needs: version runs-on: blacksmith-4vcpu-ubuntu-2404 - if: github.repository == 'anomalyco/opencode' + if: github.repository == 'Latitudes-Dev/shuvcode' steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: @@ -91,7 +91,7 @@ jobs: opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} - name: Build legacy CLI - if: github.ref_name != 'v2' + if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' run: ./packages/opencode/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }} env: OPENCODE_VERSION: ${{ needs.version.outputs.version }} @@ -109,7 +109,7 @@ jobs: GH_TOKEN: ${{ steps.committer.outputs.token }} - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - if: github.ref_name != 'v2' + if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' with: name: opencode-cli path: | @@ -117,22 +117,22 @@ jobs: packages/opencode/dist/opencode-linux* - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - if: github.ref_name != 'v2' + if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' with: name: opencode-cli-windows path: packages/opencode/dist/opencode-windows* - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: opencode-preview-cli - path: packages/cli/dist/cli-* + name: shuvcode-preview-cli + path: packages/cli/dist/shuvcode-* outputs: version: ${{ needs.version.outputs.version }} build-node-cli: needs: version - if: github.repository == 'anomalyco/opencode' + if: github.repository == 'Latitudes-Dev/shuvcode' strategy: fail-fast: false matrix: @@ -175,8 +175,8 @@ jobs: - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: opencode-node-cli-${{ matrix.settings.target }} - path: packages/cli/dist/node/cli-node-* + name: shuvcode-node-cli-${{ matrix.settings.target }} + path: packages/cli/dist/node/shuvcode-node-* if-no-files-found: error sign-cli-windows: @@ -474,7 +474,7 @@ jobs: - build-node-cli - sign-cli-windows - build-electron - if: always() && !failure() && !cancelled() + if: always() && needs.version.result == 'success' && needs.build-cli.result == 'success' && needs.build-node-cli.result == 'success' && !failure() && !cancelled() runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 @@ -500,42 +500,42 @@ jobs: registry-url: "https://registry.npmjs.org" - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - if: github.ref_name != 'v2' + if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' with: name: opencode-cli path: packages/opencode/dist - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - if: github.ref_name != 'v2' + if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' with: name: opencode-cli-windows path: packages/opencode/dist - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - if: github.ref_name != 'v2' + if: needs.sign-cli-windows.result == 'success' with: name: opencode-cli-signed-windows path: packages/opencode/dist - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - name: opencode-preview-cli + name: shuvcode-preview-cli path: packages/cli/dist - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - pattern: opencode-node-cli-* + pattern: shuvcode-node-cli-* path: packages/cli/dist/node merge-multiple: true - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - if: needs.version.outputs.release + if: needs.build-electron.result == 'success' && needs.version.outputs.release with: pattern: latest-yml-* path: /tmp/latest-yml - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - if: needs.version.outputs.release + if: needs.build-electron.result == 'success' && needs.version.outputs.release with: pattern: opencode-desktop-* path: /tmp/desktop @@ -568,7 +568,7 @@ jobs: ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true - name: Upload desktop release assets - if: needs.version.outputs.release + if: needs.build-electron.result == 'success' && needs.version.outputs.release env: GH_TOKEN: ${{ steps.committer.outputs.token }} run: | From 86d46e8d1d99cdf52b3d527bdd8faeb1e11e2414 Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 27 Jul 2026 03:00:17 -0700 Subject: [PATCH 148/150] fix(ci): isolate fork release publishing --- packages/cli/script/publish.ts | 17 ++++++----- script/publish-plan.test.ts | 25 ++++++++++++++++ script/publish-plan.ts | 26 ++++++++++++++++ script/publish.ts | 54 +++++++++++++--------------------- 4 files changed, 81 insertions(+), 41 deletions(-) create mode 100644 script/publish-plan.test.ts create mode 100644 script/publish-plan.ts diff --git a/packages/cli/script/publish.ts b/packages/cli/script/publish.ts index d7b2ebf7641d..f4c7222b0d04 100755 --- a/packages/cli/script/publish.ts +++ b/packages/cli/script/publish.ts @@ -4,6 +4,7 @@ import pkg from "../package.json" import { Script } from "@opencode-ai/script" import { fileURLToPath } from "url" import { UpdateArtifact } from "../../../script/update-artifact" +import { currentRepository, publishPlan } from "../../../script/publish-plan" const dir = fileURLToPath(new URL("..", import.meta.url)) process.chdir(dir) @@ -78,10 +79,12 @@ await publishDistribution({ binary: "shuvcode-node", packagePrefix: "shuvcode-node-", }) -await UpdateArtifact.publish({ - channel: Script.channel, - name: "cli", - distribution: "npm", - version: Script.version, - metadata: {}, -}) +if (publishPlan(currentRepository()).updateArtifacts) { + await UpdateArtifact.publish({ + channel: Script.channel, + name: "cli", + distribution: "npm", + version: Script.version, + metadata: {}, + }) +} diff --git a/script/publish-plan.test.ts b/script/publish-plan.test.ts new file mode 100644 index 000000000000..5af778d73504 --- /dev/null +++ b/script/publish-plan.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, test } from "bun:test" +import { forkRepository, publishPlan, upstreamRepository } from "./publish-plan" + +describe("publish plan", () => { + test("publishes only Shuvcode CLI distributions for the fork", () => { + expect(publishPlan(forkRepository)).toEqual({ + packages: ["cli"], + desktop: false, + updateArtifacts: false, + }) + }) + + test("keeps upstream package and finalizer behavior explicit", () => { + expect(publishPlan(upstreamRepository)).toEqual({ + packages: ["schema", "ai", "util", "protocol", "client", "cli", "plugin", "ui"], + desktop: true, + updateArtifacts: true, + }) + }) + + test("rejects publication from unconfigured repositories", () => { + expect(() => publishPlan("example/unknown")).toThrow("Publishing is not configured") + expect(() => publishPlan(undefined)).toThrow("Publishing is not configured") + }) +}) diff --git a/script/publish-plan.ts b/script/publish-plan.ts new file mode 100644 index 000000000000..4b1bb666c579 --- /dev/null +++ b/script/publish-plan.ts @@ -0,0 +1,26 @@ +export const forkRepository = "Latitudes-Dev/shuvcode" +export const upstreamRepository = "anomalyco/opencode" + +const upstreamPackages = ["schema", "ai", "util", "protocol", "client", "cli", "plugin", "ui"] as const + +export function publishPlan(repository: string | undefined) { + if (repository === forkRepository) { + return { + packages: ["cli"] as const, + desktop: false, + updateArtifacts: false, + } + } + if (repository === upstreamRepository) { + return { + packages: upstreamPackages, + desktop: true, + updateArtifacts: true, + } + } + throw new Error(`Publishing is not configured for repository: ${repository ?? "unknown"}`) +} + +export function currentRepository() { + return process.env.GITHUB_REPOSITORY ?? process.env.GH_REPO +} diff --git a/script/publish.ts b/script/publish.ts index 3623eeb77765..e1619bbeb4aa 100755 --- a/script/publish.ts +++ b/script/publish.ts @@ -4,12 +4,15 @@ import { Script } from "@opencode-ai/script" import { $ } from "bun" import { fileURLToPath } from "url" import { UpdateArtifact } from "./update-artifact" +import { currentRepository, forkRepository, publishPlan } from "./publish-plan" console.log("=== publishing ===\n") const dir = fileURLToPath(new URL("..", import.meta.url)) process.chdir(dir) const tag = `v${Script.version}` +const repository = currentRepository() +const plan = publishPlan(repository) const pkgjsons = await Array.fromAsync( new Bun.Glob("**/package.json").scan({ @@ -35,31 +38,12 @@ if (Script.release && !Script.preview) { await prepareReleaseFiles() -console.log("\n=== schema ===\n") -await $`bun ./packages/schema/script/publish.ts` - -console.log("\n=== ai ===\n") -await $`bun ./packages/ai/script/publish.ts` - -console.log("\n=== util ===\n") -await $`bun ./packages/util/script/publish.ts` - -console.log("\n=== protocol ===\n") -await $`bun ./packages/protocol/script/publish.ts` - -console.log("\n=== client ===\n") -await $`bun ./packages/client/script/publish.ts` - -console.log("\n=== cli ===\n") -await $`bun ./packages/cli/script/publish.ts` - -console.log("\n=== plugin ===\n") -await $`bun ./packages/plugin/script/publish.ts` - -console.log("\n=== ui ===\n") -await $`bun ./packages/ui/script/publish.ts` +for (const name of plan.packages) { + console.log(`\n=== ${name} ===\n`) + await $`bun ${`./packages/${name}/script/publish.ts`}` +} -if (Script.release) { +if (Script.release && plan.desktop) { await $`bun ./packages/desktop/scripts/finalize-latest-json.ts` await $`bun ./packages/desktop/scripts/finalize-latest-yml.ts` } @@ -78,14 +62,16 @@ if (Script.release && !Script.preview) { } if (Script.release) { - await $`gh release edit ${tag} --draft=false --repo ${process.env.GH_REPO}` - const repo = process.env.GH_REPO - if (!repo) throw new Error("GH_REPO is required") - await UpdateArtifact.publish({ - channel: Script.channel, - name: "desktop", - distribution: "github", - version: Script.version, - metadata: await UpdateArtifact.desktopMetadata(Script.version, repo), - }) + const repo = repository === forkRepository ? repository : (process.env.GH_REPO ?? repository) + if (!repo) throw new Error("Release repository is required") + await $`gh release edit ${tag} --draft=false --repo ${repo}` + if (plan.updateArtifacts) { + await UpdateArtifact.publish({ + channel: Script.channel, + name: "desktop", + distribution: "github", + version: Script.version, + metadata: await UpdateArtifact.desktopMetadata(Script.version, repo), + }) + } } From 55002d2ea4c05e15d9575b99ba1ae101bbbf8e65 Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 27 Jul 2026 03:16:52 -0700 Subject: [PATCH 149/150] fix(ci): preflight fork package ownership --- .github/workflows/publish.yml | 498 ++------------- PLAN-v2-release-publish.md | 654 +------------------- packages/cli/script/preflight-publish.ts | 6 + packages/cli/script/publish-ownership.ts | 106 ++++ packages/cli/script/publish.ts | 68 +- packages/cli/test/publish-ownership.test.ts | 81 +++ script/version.ts | 25 +- 7 files changed, 330 insertions(+), 1108 deletions(-) create mode 100644 packages/cli/script/preflight-publish.ts create mode 100644 packages/cli/script/publish-ownership.ts create mode 100644 packages/cli/test/publish-ownership.test.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d2e86f28ff17..65aec221ff60 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,15 +1,7 @@ name: publish -run-name: "${{ format('release {0}', inputs.bump) }}" +run-name: "${{ format('release {0}', inputs.version || inputs.bump) }}" on: - push: - branches: - - ci - - dev - - v2 - - beta - - fix/npm-native-binary-install - - snapshot-* workflow_dispatch: inputs: bump: @@ -25,20 +17,18 @@ on: required: false type: string -concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.bump }} +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.bump }} + cancel-in-progress: false permissions: - id-token: write contents: write - packages: write - -env: - OPENCODE_CHANNEL: ${{ (github.ref_name == 'v2' && 'next') || '' }} + id-token: write jobs: version: - runs-on: blacksmith-4vcpu-ubuntu-2404 if: github.repository == 'Latitudes-Dev/shuvcode' + runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: @@ -46,26 +36,33 @@ jobs: - uses: ./.github/actions/setup-bun - - name: Setup git committer - id: committer - uses: ./.github/actions/setup-git-committer + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - opencode-app-id: ${{ vars.OPENCODE_APP_ID }} - opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} + node-version: "24" - - name: Install OpenCode - if: inputs.bump || inputs.version - run: bun i -g opencode-ai + - name: Install npm with trusted publishing support + run: npm install --global npm@11.5.1 - - id: version + - name: Validate release input + env: + BUMP: ${{ inputs.bump }} + VERSION: ${{ inputs.version }} run: | - ./script/version.ts + if { [ -z "$BUMP" ] && [ -z "$VERSION" ]; } || { [ -n "$BUMP" ] && [ -n "$VERSION" ]; }; then + echo "Provide exactly one of bump or version" >&2 + exit 1 + fi + + - name: Preflight all fork npm packages + run: bun packages/cli/script/preflight-publish.ts + + - id: version + run: ./script/version.ts env: - GH_TOKEN: ${{ steps.committer.outputs.token }} + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} OPENCODE_BUMP: ${{ inputs.bump }} OPENCODE_VERSION: ${{ inputs.version }} - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - GH_REPO: ${{ github.repository }} outputs: version: ${{ steps.version.outputs.version }} release: ${{ steps.version.outputs.release }} @@ -75,64 +72,25 @@ jobs: build-cli: needs: version runs-on: blacksmith-4vcpu-ubuntu-2404 - if: github.repository == 'Latitudes-Dev/shuvcode' steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - with: - fetch-tags: true - uses: ./.github/actions/setup-bun - - name: Setup git committer - id: committer - uses: ./.github/actions/setup-git-committer - with: - opencode-app-id: ${{ vars.OPENCODE_APP_ID }} - opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} - - - name: Build legacy CLI - if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' - run: ./packages/opencode/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }} - env: - OPENCODE_VERSION: ${{ needs.version.outputs.version }} - OPENCODE_RELEASE: ${{ needs.version.outputs.release }} - GH_REPO: ${{ needs.version.outputs.repo }} - GH_TOKEN: ${{ steps.committer.outputs.token }} - - - name: Build preview CLI - id: build - run: ./packages/cli/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }} + - name: Build Bun CLI packages + run: ./packages/cli/script/build.ts env: OPENCODE_VERSION: ${{ needs.version.outputs.version }} OPENCODE_RELEASE: ${{ needs.version.outputs.release }} - GH_REPO: ${{ needs.version.outputs.repo }} - GH_TOKEN: ${{ steps.committer.outputs.token }} - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' with: - name: opencode-cli - path: | - packages/opencode/dist/opencode-darwin* - packages/opencode/dist/opencode-linux* - - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' - with: - name: opencode-cli-windows - path: packages/opencode/dist/opencode-windows* - - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: shuvcode-preview-cli + name: shuvcode-cli path: packages/cli/dist/shuvcode-* - - outputs: - version: ${{ needs.version.outputs.version }} + if-no-files-found: error build-node-cli: needs: version - if: github.repository == 'Latitudes-Dev/shuvcode' strategy: fail-fast: false matrix: @@ -162,7 +120,7 @@ jobs: with: node-version: "26.4.0" - - name: Build + - name: Build Node CLI package run: bun packages/cli/script/build-node.ts --target=${{ matrix.settings.target }} --skip-install --outdir=dist/node env: OPENCODE_VERSION: ${{ needs.version.outputs.version }} @@ -179,347 +137,29 @@ jobs: path: packages/cli/dist/node/shuvcode-node-* if-no-files-found: error - sign-cli-windows: - needs: - - build-cli - - version - runs-on: blacksmith-4vcpu-windows-2025 - if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' - env: - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} - AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }} - AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} - steps: - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - name: opencode-cli-windows - path: packages/opencode/dist - - - name: Setup git committer - id: committer - uses: ./.github/actions/setup-git-committer - with: - opencode-app-id: ${{ vars.OPENCODE_APP_ID }} - opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} - - - name: Azure login - uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0 - with: - client-id: ${{ env.AZURE_CLIENT_ID }} - tenant-id: ${{ env.AZURE_TENANT_ID }} - subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} - - - uses: azure/artifact-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 # v1.2.0 - with: - endpoint: ${{ env.AZURE_TRUSTED_SIGNING_ENDPOINT }} - signing-account-name: ${{ env.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} - certificate-profile-name: ${{ env.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }} - files: | - ${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64\bin\opencode.exe - ${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe - ${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe - exclude-environment-credential: true - exclude-workload-identity-credential: true - exclude-managed-identity-credential: true - exclude-shared-token-cache-credential: true - exclude-visual-studio-credential: true - exclude-visual-studio-code-credential: true - exclude-azure-cli-credential: false - exclude-azure-powershell-credential: true - exclude-azure-developer-cli-credential: true - exclude-interactive-browser-credential: true - - - name: Verify Windows CLI signatures - shell: pwsh - run: | - $files = @( - "${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64\bin\opencode.exe", - "${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe", - "${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe" - ) - - foreach ($file in $files) { - $sig = Get-AuthenticodeSignature $file - if ($sig.Status -ne "Valid") { - throw "Invalid signature for ${file}: $($sig.Status)" - } - } - - - name: Repack Windows CLI archives - working-directory: packages/opencode/dist - shell: pwsh - run: | - Compress-Archive -Path "opencode-windows-arm64\bin\*" -DestinationPath "opencode-windows-arm64.zip" -Force - Compress-Archive -Path "opencode-windows-x64\bin\*" -DestinationPath "opencode-windows-x64.zip" -Force - Compress-Archive -Path "opencode-windows-x64-baseline\bin\*" -DestinationPath "opencode-windows-x64-baseline.zip" -Force - - - name: Upload signed Windows CLI release assets - if: needs.version.outputs.release != '' - shell: pwsh - env: - GH_TOKEN: ${{ steps.committer.outputs.token }} - run: | - gh release upload "v${{ needs.version.outputs.version }}" ` - "${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64.zip" ` - "${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64.zip" ` - "${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline.zip" ` - --clobber ` - --repo "${{ needs.version.outputs.repo }}" - - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: opencode-cli-signed-windows - path: | - packages/opencode/dist/opencode-windows-arm64 - packages/opencode/dist/opencode-windows-x64 - packages/opencode/dist/opencode-windows-x64-baseline - - build-electron: - needs: - - build-cli - - version - if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' - continue-on-error: false - env: - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} - AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }} - AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} - strategy: - fail-fast: false - matrix: - settings: - - host: macos-26-intel - target: x86_64-apple-darwin - platform_flag: --mac --x64 - bun_install_flags: --os=darwin --cpu=x64 - - host: macos-26 - target: aarch64-apple-darwin - platform_flag: --mac --arm64 - bun_install_flags: --os=darwin --cpu=arm64 - # github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain - - host: "windows-2025" - target: aarch64-pc-windows-msvc - platform_flag: --win --arm64 - - host: "blacksmith-4vcpu-windows-2025" - target: x86_64-pc-windows-msvc - platform_flag: --win - - host: "blacksmith-4vcpu-ubuntu-2404" - target: x86_64-unknown-linux-gnu - platform_flag: --linux - - host: "blacksmith-4vcpu-ubuntu-2404-arm" - target: aarch64-unknown-linux-gnu - platform_flag: --linux --arm64 - runs-on: ${{ matrix.settings.host }} - steps: - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - - - uses: apple-actions/import-codesign-certs@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 # v2.0.0 - if: runner.os == 'macOS' - with: - keychain: build - p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }} - p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - - - name: Setup Apple API Key - if: runner.os == 'macOS' - run: echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8 - - - uses: ./.github/actions/setup-bun - with: - install-flags: ${{ matrix.settings.bun_install_flags }} - - - name: Azure login - if: runner.os == 'Windows' - uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0 - with: - client-id: ${{ env.AZURE_CLIENT_ID }} - tenant-id: ${{ env.AZURE_TENANT_ID }} - subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} - - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: "24" - - - name: Cache apt packages - if: contains(matrix.settings.host, 'ubuntu') - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: ~/apt-cache - key: ${{ runner.os }}-${{ matrix.settings.target }}-apt-electron-${{ hashFiles('.github/workflows/publish.yml') }} - restore-keys: | - ${{ runner.os }}-${{ matrix.settings.target }}-apt-electron- - - - name: Install dependencies (ubuntu only) - if: contains(matrix.settings.host, 'ubuntu') - run: | - mkdir -p ~/apt-cache && chmod -R a+rw ~/apt-cache - sudo apt-get update - sudo apt-get install -y --no-install-recommends -o dir::cache::archives="$HOME/apt-cache" rpm - sudo chmod -R a+rw ~/apt-cache - - - name: Setup git committer - id: committer - uses: ./.github/actions/setup-git-committer - with: - opencode-app-id: ${{ vars.OPENCODE_APP_ID }} - opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} - - - name: Prepare - run: bun ./scripts/prepare.ts - working-directory: packages/desktop - env: - OPENCODE_VERSION: ${{ needs.version.outputs.version }} - OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} - OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-windows') || 'opencode-cli' }} - RUST_TARGET: ${{ matrix.settings.target }} - GH_TOKEN: ${{ github.token }} - GITHUB_RUN_ID: ${{ github.run_id }} - - - name: Build - run: bun run build - working-directory: packages/desktop - env: - NODE_OPTIONS: --max-old-space-size=4096 - OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_ORG: ${{ vars.SENTRY_ORG }} - SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }} - SENTRY_RELEASE: desktop@${{ needs.version.outputs.version }} - VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }} - VITE_SENTRY_ENVIRONMENT: ${{ (github.ref_name == 'beta' && 'beta') || 'production' }} - VITE_SENTRY_RELEASE: desktop@${{ needs.version.outputs.version }} - - - name: Package - if: needs.version.outputs.release - run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish never --config electron-builder.config.ts - working-directory: packages/desktop - timeout-minutes: 60 - env: - OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} - GH_TOKEN: ${{ steps.committer.outputs.token }} - CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} - CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - APPLE_API_KEY: ${{ runner.temp }}/apple-api-key.p8 - APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY }} - APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} - - - name: Package (no publish) - if: ${{ !needs.version.outputs.release }} - run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish never --config electron-builder.config.ts - working-directory: packages/desktop - timeout-minutes: 60 - env: - OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} - - - name: Create macOS .app.tar.gz - if: runner.os == 'macOS' && needs.version.outputs.release - working-directory: packages/desktop/dist - run: | - if [[ "${{ matrix.settings.target }}" == "x86_64-apple-darwin" ]]; then - APP_DIR="mac" - OUT_NAME="opencode-desktop-mac-x64.app.tar.gz" - elif [[ "${{ matrix.settings.target }}" == "aarch64-apple-darwin" ]]; then - APP_DIR="mac-arm64" - OUT_NAME="opencode-desktop-mac-arm64.app.tar.gz" - else - echo "Unknown macOS target: ${{ matrix.settings.target }}" - exit 1 - fi - APP_PATH=$(find "$APP_DIR" -maxdepth 1 -name "*.app" -type d | head -1) - if [ -z "$APP_PATH" ]; then - echo "No .app bundle found in $APP_DIR" - exit 1 - fi - tar -czf "$OUT_NAME" -C "$(dirname "$APP_PATH")" "$(basename "$APP_PATH")" - - - name: Verify signed Windows Electron artifacts - if: runner.os == 'Windows' - shell: pwsh - run: | - $files = @() - $files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*.exe" | Select-Object -ExpandProperty FullName - $files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*unpacked\*.exe" | Select-Object -ExpandProperty FullName - $files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*unpacked\resources\opencode-cli.exe" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName - - foreach ($file in $files | Select-Object -Unique) { - $sig = Get-AuthenticodeSignature $file - if ($sig.Status -ne "Valid") { - throw "Invalid signature for ${file}: $($sig.Status)" - } - } - - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: opencode-desktop-${{ matrix.settings.target }} - path: packages/desktop/dist/* - - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - if: needs.version.outputs.release - with: - name: latest-yml-${{ matrix.settings.target }} - path: packages/desktop/dist/latest*.yml - publish: needs: - version - build-cli - build-node-cli - - sign-cli-windows - - build-electron - if: always() && needs.version.result == 'success' && needs.build-cli.result == 'success' && needs.build-node-cli.result == 'success' && !failure() && !cancelled() runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - - - uses: ./.github/actions/setup-bun - - - name: Login to GitHub Container Registry - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 + fetch-depth: 0 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + - uses: ./.github/actions/setup-bun - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "24" - registry-url: "https://registry.npmjs.org" - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' - with: - name: opencode-cli - path: packages/opencode/dist - - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' - with: - name: opencode-cli-windows - path: packages/opencode/dist + - name: Install npm with trusted publishing support + run: npm install --global npm@11.5.1 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - if: needs.sign-cli-windows.result == 'success' with: - name: opencode-cli-signed-windows - path: packages/opencode/dist - - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - name: shuvcode-preview-cli + name: shuvcode-cli path: packages/cli/dist - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 @@ -528,66 +168,16 @@ jobs: path: packages/cli/dist/node merge-multiple: true - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - if: needs.build-electron.result == 'success' && needs.version.outputs.release - with: - pattern: latest-yml-* - path: /tmp/latest-yml - - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - if: needs.build-electron.result == 'success' && needs.version.outputs.release - with: - pattern: opencode-desktop-* - path: /tmp/desktop - merge-multiple: true - - - name: Setup git committer - id: committer - uses: ./.github/actions/setup-git-committer - with: - opencode-app-id: ${{ vars.OPENCODE_APP_ID }} - opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} - - - name: Cache apt packages (AUR) - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: /var/cache/apt/archives - key: ${{ runner.os }}-apt-aur-${{ hashFiles('.github/workflows/publish.yml') }} - restore-keys: | - ${{ runner.os }}-apt-aur- - - - name: Setup SSH for AUR + - name: Configure GitHub Actions committer run: | - sudo apt-get update - sudo apt-get install -y pacman-package-manager - mkdir -p ~/.ssh - echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - git config --global user.email "opencode@sst.dev" - git config --global user.name "opencode" - ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - name: Upload desktop release assets - if: needs.build-electron.result == 'success' && needs.version.outputs.release - env: - GH_TOKEN: ${{ steps.committer.outputs.token }} - run: | - shopt -s nullglob - files=(/tmp/desktop/*.{exe,blockmap,dmg,zip,AppImage,deb,rpm} /tmp/desktop/*.app.tar.gz) - if (( ${#files[@]} == 0 )); then - echo "No desktop release assets found" - exit 1 - fi - gh release upload "v${{ needs.version.outputs.version }}" "${files[@]}" --clobber --repo "${{ needs.version.outputs.repo }}" - - - run: ./script/publish.ts + - name: Publish fork release + run: ./script/publish.ts env: + GH_REPO: ${{ needs.version.outputs.repo }} + GH_TOKEN: ${{ github.token }} + GITHUB_TOKEN: ${{ github.token }} OPENCODE_VERSION: ${{ needs.version.outputs.version }} OPENCODE_RELEASE: ${{ needs.version.outputs.release }} - AUR_KEY: ${{ secrets.AUR_KEY }} - GITHUB_TOKEN: ${{ steps.committer.outputs.token }} - GH_REPO: ${{ needs.version.outputs.repo }} - NPM_CONFIG_PROVENANCE: false - LATEST_YML_DIR: /tmp/latest-yml - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} diff --git a/PLAN-v2-release-publish.md b/PLAN-v2-release-publish.md index e4314ad703b9..5457703ae206 100644 --- a/PLAN-v2-release-publish.md +++ b/PLAN-v2-release-publish.md @@ -1,28 +1,10 @@ -# PLAN: Cut and publish shuvcode v2.0.0-1 +# Fork release runbook -## Goal +Shuvcode releases are manual and fork-only. Dispatch `.github/workflows/publish.yml` in `Latitudes-Dev/shuvcode` with either a version or bump. The workflow builds and publishes CLI npm packages only; it does not publish `@opencode-ai/*`, desktop applications, containers, AUR packages, or `update.opencode.ai` artifacts. -Publish the first V2 fork release of `shuvcode` to npm under `latest` and to GitHub at `Latitudes-Dev/shuvcode`, without publishing upstream-owned `@opencode-ai/*` packages or invoking the anomalyco release pipeline. +## npm packages -| Release field | Locked value | -| --------------------- | ---------------------------------------------- | -| Version | `2.0.0-1` | -| npm dist-tag | `latest` | -| GitHub tag/release | `v2.0.0-1` | -| Product source branch | `integration-v2` | -| Product source commit | `17e77b56eb1f00210910628a102bbb301adfe603` | -| Release trigger | Manual GitHub Actions dispatch | -| npm CI authentication | Trusted publishing with OIDC | -| GitHub authentication | Built-in `GITHUB_TOKEN` with `contents: write` | -| GitHub binary assets | Omitted for this cut | - -The workflow-restoration commit will land after the locked product source commit. The release workflow must therefore accept and check out the explicit product source SHA rather than implicitly building its own workflow commit. - -## Release boundary - -### Publish exactly these 13 npm packages - -The V2 CLI build creates 12 platform packages. `packages/cli/script/publish.ts` then creates and publishes the umbrella package last. +Every release preflights and publishes exactly these 19 public packages: 1. `shuvcode` 2. `shuvcode-linux-arm64` @@ -37,614 +19,42 @@ The V2 CLI build creates 12 platform packages. `packages/cli/script/publish.ts` 11. `shuvcode-windows-arm64` 12. `shuvcode-windows-x64` 13. `shuvcode-windows-x64-baseline` +14. `shuvcode-node` +15. `shuvcode-node-linux-arm64` +16. `shuvcode-node-linux-x64` +17. `shuvcode-node-darwin-arm64` +18. `shuvcode-node-windows-arm64` +19. `shuvcode-node-windows-x64` -`shuvcode-linux-x64-baseline-musl` is required even though it was missing from the original credential checklist: the build emits it, the npm wrapper selects it for baseline musl hosts, and npm already contains the V1 package. - -### Never use these release paths - -| Path | Reason | -| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `.github/workflows/publish.yml` | Jobs are gated to `github.repository == 'anomalyco/opencode'` and target upstream release surfaces. | -| Root `script/publish.ts` | Publishes schema, protocol, client, CLI, SDK, plugin, and UI packages, including upstream-owned `@opencode-ai/*` names. | -| `script/release` | Dispatches the upstream-only `publish.yml`. | -| Root tests | Repo guard intentionally rejects tests from the root. Run tests from package directories or through the existing CI workflow. | - -The safe build and publish entrypoints are: - -- `packages/cli/script/build.ts` -- `packages/cli/script/publish.ts` - -## Validated current state - -The following facts were rechecked on 2026-07-13 before this plan was revised: - -| Check | Current result | -| ------------------------------ | ------------------------------------------------------------------------------------------------------ | -| `origin/integration-v2` | `17e77b56eb1f00210910628a102bbb301adfe603` | -| npm `shuvcode@latest` | `1.2.27-4` | -| npm owners | `kcrommett` is the sole maintainer of all 13 packages | -| Local `npm whoami` | `403`; the local credential is unusable | -| GitHub default branch | `integration`, the frozen V1 line | -| Latest GitHub release | `v1.2.27-4`; no V2 tag/release exists | -| Current product SHA checks | No GitHub check rollup exists | -| Last `integration-v2` test run | Ran against older commit `8d9b20b6d`; unit jobs failed | -| Local Bun | `1.3.11` | -| Required Bun | `^1.3.14` from root `packageManager` | -| Local npm / Node | npm `10.9.7`, Node `22.22.2` | -| OpenShuv host | Active and authenticated `/api/health` reports `2.0.0-1` | -| Blacksmith | Successfully ran this repo in March and July 2026; current availability still requires a real dispatch | - -GitHub exposes the `NPM_TOKEN` and `PAT_TOKEN` secret names but not their values or validity. This plan does not treat secret age as proof that either credential works. - -## Version and channel invariants - -`packages/script/src/index.ts` derives the channel from the branch unless an environment override is present. On `integration-v2`, an unconfigured build becomes a preview: - -```text -CHANNEL=integration-v2 -VERSION=0.0.0-integration-v2- -``` - -The release workflow must set both values explicitly: - -```yaml -env: - OPENCODE_CHANNEL: latest - OPENCODE_VERSION: 2.0.0-1 -``` - -`packages/script/src/version.ts` and `packages/script/test/version.test.ts` confirm that an unpinned latest release would also calculate `1.2.27-4 -> 2.0.0-1`, but the first cut must remain pinned to avoid registry races and ambiguity. - -## Authentication decisions - -### npm: trusted publishing is primary - -npm recommends trusted publishing for GitHub Actions. It removes the long-lived npm write token from the primary release path and uses a short-lived OIDC identity tied to the repository and workflow filename. - -The npm maintainer must configure a trusted publisher separately for all 13 packages with: - -| Trusted publisher field | Value | -| ----------------------- | ----------------- | -| Provider | GitHub Actions | -| Organization | `Latitudes-Dev` | -| Repository | `shuvcode` | -| Workflow filename | `snapshot.yml` | -| Environment | None for this cut | -| Allowed action | `npm publish` | - -Trusted publishing requires: - -- workflow permission `id-token: write` -- npm CLI `>=11.5.1` -- Node `>=22.14.0` -- no `NODE_AUTH_TOKEN` in the OIDC publish job - -Primary reference: https://docs.npmjs.com/trusted-publishers/ - -### npm: granular token is local fallback only - -If a local publish fallback must remain available, `kcrommett` may create a granular token with: - -- read/write package access -- bypass 2FA enabled for automated publishing -- all 13 packages included -- the shortest practical expiration - -Store that token locally in `~/.npmrc`. Do not add it to GitHub unless OIDC is unavailable and the plan is deliberately switched to token-based CI. - -Legacy or “automation” tokens are not an option; npm supports granular access tokens and has removed legacy access tokens. References: - -- https://docs.npmjs.com/about-access-tokens/ -- https://docs.npmjs.com/using-private-packages-in-a-ci-cd-workflow/ - -### GitHub: use the built-in token - -The release job only needs to read the source and create a tag/release in the same repository. Use: - -```yaml -permissions: - contents: write - id-token: write -``` - -Set `GH_TOKEN: ${{ github.token }}` for `gh`. Do not require `PAT_TOKEN` for checkout, tagging, or release creation. - -A PAT is only justified later if an event created by the release must trigger another workflow. Discord notification is out of scope for this cut, so it does not justify PAT rotation now. - -Reference: https://docs.github.com/en/actions/tutorials/authenticate-with-github_token - -## Workflow topology decision - -The first V2 release is manual-only. - -Do not add `workflow_run` publishing yet because: - -1. GitHub requires the triggered workflow file to exist on the default branch. -2. The default branch is the frozen V1 `integration` line. -3. The default-branch `snapshot.yml` still describes the V1 `integration` release. -4. Current `.github/workflows/test.yml` does not push-trigger on `integration-v2`. -5. A privileged `workflow_run` publish would add avoidable default-branch and untrusted-checkout complexity. - -The existing `snapshot.yml` registration on default branch `integration` must remain until the default branch or workflow topology is deliberately migrated. Once the V2 workflow is restored on `integration-v2`, manual dispatch with `--ref integration-v2` can select that branch’s workflow implementation. - -References: - -- https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#workflow-dispatch -- https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#workflow-run - -## Implementation order - -### Milestone 0: Human npm authorization - -#### 0.1 Configure trusted publishing - -- [ ] Sign in to npm as `kcrommett`, the current sole maintainer. -- [ ] Configure the `snapshot.yml` trusted publisher on each of the 13 packages listed in this plan. -- [ ] Select `npm publish` as the allowed action. -- [ ] Do not configure an npm environment name for this cut. -- [ ] Record completion without copying tokens or sensitive npm account data into the repo. - -**Done when:** all 13 npm package settings pages authorize `Latitudes-Dev/shuvcode` + `snapshot.yml` for `npm publish`. - -#### 0.2 Optional local fallback credential - -- [ ] Decide whether a local emergency publish path is actually required. -- [ ] If required, create a granular read/write token with bypass 2FA for all 13 packages. -- [ ] Store it in local `~/.npmrc`, never in the plan or repository. -- [ ] Verify identity: - - ```bash - npm whoami - # expected: kcrommett - ``` - -- [ ] Verify the account still owns all packages: - - ```bash - npm view shuvcode maintainers --json - npm view shuvcode-linux-x64-baseline-musl maintainers --json - ``` - -`npm publish --dry-run` validates packing behavior; it is not accepted as definitive proof that a real registry publish will be authorized. - -**Done when:** OIDC is configured; local token work is complete only if the fallback was explicitly retained. - -### Milestone 1: Restore a guarded manual release workflow - -Restore `.github/workflows/snapshot.yml` on `integration-v2`. Use historical content only as reference: - -- `git show 8d9b20b6d:.github/workflows/snapshot.yml` -- `git show origin/integration:.github/workflows/snapshot.yml` - -The restored workflow must not copy their V1 or incomplete V2 behavior blindly. - -#### 1.1 Trigger and inputs - -- [ ] Use `workflow_dispatch` only. -- [ ] Add required string input `release_sha`. -- [ ] Add required string input `confirm_version`. -- [ ] Add boolean input `publish`, defaulting to `false`. -- [ ] Use a fixed concurrency group and never cancel an active release: - - ```yaml - concurrency: - group: shuvcode-v2-release - cancel-in-progress: false - ``` - -- [ ] Reject the run unless: - - `release_sha == 17e77b56eb1f00210910628a102bbb301adfe603` - - `confirm_version == 2.0.0-1` - - the resolved commit is contained by `origin/integration-v2` - -The fixed SHA checks are intentional for the first cut. Subsequent releases must revise the plan/workflow rather than silently accepting a new commit. - -#### 1.2 Runner and checkout - -- [ ] Use `blacksmith-4vcpu-ubuntu-2404` initially. -- [ ] Document `ubuntu-latest` as a manual edit-and-redispatch fallback if the Blacksmith job cannot acquire a runner. -- [ ] Checkout `inputs.release_sha`, not the workflow run SHA. -- [ ] Use `fetch-depth: 0` and fetch tags. -- [ ] Use the built-in GitHub token; do not pass `PAT_TOKEN` to checkout. -- [ ] Verify that product code after the locked SHA differs from the workflow-prep branch only in approved release-plan/workflow/docs paths before proceeding. - -#### 1.3 Toolchain and permissions - -- [ ] Grant only: - - ```yaml - permissions: - contents: write - id-token: write - ``` - -- [ ] Run `.github/actions/setup-bun`; it pins Bun from root `packageManager` and sets up Node 24. -- [ ] Install or select npm 11 and verify requirements: - - ```bash - npm install --global npm@11 - node --version - npm --version - # Node must be >=22.14.0; npm must be >=11.5.1 - ``` - -- [ ] Set: - - ```yaml - env: - OPENCODE_CHANNEL: latest - OPENCODE_VERSION: 2.0.0-1 - GH_REPO: Latitudes-Dev/shuvcode - GH_TOKEN: ${{ github.token }} - ``` - -- [ ] Do not set `NODE_AUTH_TOKEN` on the OIDC path. - -#### 1.4 Pre-publish registry guard - -- [ ] Before building, fail if any of these already exist: - - npm `shuvcode@2.0.0-1` - - Git tag `v2.0.0-1` - - GitHub release `v2.0.0-1` -- [ ] Print current `shuvcode` dist-tags and the locked source SHA. -- [ ] Fail if npm `latest` is no longer `1.2.27-4`; a changed registry baseline requires a new review. - -#### 1.5 Build and validate all artifacts - -- [ ] Run only the CLI build: - - ```bash - bun ./packages/cli/script/build.ts - ``` - -- [ ] Verify `packages/cli/dist` contains exactly the 12 expected platform package directories. -- [ ] Verify every platform `package.json` has: - - a package name from the locked list - - version `2.0.0-1` - - repository `git+https://github.com/Latitudes-Dev/shuvcode.git` - - expected `os` and `cpu` metadata -- [ ] Run `bun pm pack` or `npm pack --dry-run` on representative glibc, musl, macOS, and Windows packages. -- [ ] Reconfirm from `packages/cli/script/publish.ts` that the umbrella manifest derives its optional dependencies from the scanned platform manifests and that the umbrella publish remains after all platform publishes. - -The workflow’s `publish=false` run stops after platform build/package checks and uploads no GitHub release assets. It must not invoke `packages/cli/script/publish.ts`, because that script has no non-publishing mode. - -#### 1.6 Publish and create the exact release - -Only when `inputs.publish == true`: - -- [ ] Run only: - - ```bash - bun ./packages/cli/script/publish.ts - ``` - -- [ ] Confirm logs show platform packages first and umbrella `shuvcode` last. -- [ ] Verify all 12 platform packages now exist at `2.0.0-1` before creating the GitHub release. -- [ ] Generate manual V2 release notes that mention: - - first V2 fork release - - install command `npm i -g shuvcode@latest` - - binary name `shuvcode` - - source commit -- [ ] Create the tag and release against the explicit source SHA: - - ```bash - gh release create v2.0.0-1 \ - --repo Latitudes-Dev/shuvcode \ - --target "$RELEASE_SHA" \ - --title "v2.0.0-1" \ - --notes-file "$RELEASE_NOTES" - ``` - -- [ ] Do not create the release before specifying the target. -- [ ] Do not separately push a conflicting local tag after `gh release create`. -- [ ] Do not upload raw `dist/*/bin/*` files. Their repeated `shuvcode` and `shuvcode.exe` basenames collide as GitHub release assets. - -GitHub binary assets are omitted because npm platform packages are the supported distribution surface for this cut. A later release may attach uniquely named archives such as `shuvcode-linux-x64.tar.gz` and `shuvcode-windows-x64.zip`. - -**Done when:** the workflow supports a safe build-only dispatch and a separately confirmed publish dispatch, both pinned to the locked source and version. - -### Milestone 2: Local plan validation, no registry writes - -#### 2.1 Upgrade the local Bun toolchain - -- [ ] Upgrade local Bun from `1.3.11` to a version satisfying `^1.3.14`. -- [ ] Verify the script import no longer fails: - - ```bash - bun --version - cd packages/script - bun -e 'await import("./src/index.ts")' - ``` - -- [ ] Run the focused version tests from their package directory: - - ```bash - cd packages/script - bun test test/version.test.ts - ``` - -#### 2.2 Single-platform packaging smoke - -From repo root: - -```bash -export OPENCODE_CHANNEL=latest -export OPENCODE_VERSION=2.0.0-1 - -bun ./packages/cli/script/build.ts --single -cat packages/cli/dist/shuvcode-linux-x64/package.json - -cd packages/cli/dist/shuvcode-linux-x64 -bun pm pack -npm publish *.tgz --dry-run --access public --tag latest -``` - -- [ ] Build succeeds for the current platform. -- [ ] Manifest name and version are correct. -- [ ] Tarball contents contain the compiled `bin/shuvcode` and expected metadata. -- [ ] Treat the dry run as package-shape evidence only, not registry authorization evidence. - -#### 2.3 Full build-only CI dispatch - -After the workflow commit is on `origin/integration-v2`: - -```bash -gh workflow run snapshot.yml \ - --ref integration-v2 \ - --repo Latitudes-Dev/shuvcode \ - -f release_sha=17e77b56eb1f00210910628a102bbb301adfe603 \ - -f confirm_version=2.0.0-1 \ - -f publish=false -``` - -- [ ] Record the exact run ID. -- [ ] Require successful checkout, toolchain, registry guard, full cross-build, and manifest/package validation. -- [ ] If Blacksmith cannot acquire the job, change the runner to `ubuntu-latest`, commit the change, and repeat the build-only dispatch. - -**Done when:** local single-platform packaging and CI full-build validation both pass without registry or release writes. - -### Milestone 3: Current-tree test gate - -The locked product commit itself has no GitHub check rollup. Before publishing, validate the release-prep branch that contains the same product code plus approved workflow/plan/docs changes. - -- [ ] Verify the only changes after the locked product commit are release workflow, plan, release notes, or documentation changes: - - ```bash - git diff --name-only 17e77b56eb1f00210910628a102bbb301adfe603..origin/integration-v2 - ``` - -- [ ] Manually dispatch the existing test workflow: - - ```bash - gh workflow run test.yml \ - --ref integration-v2 \ - --repo Latitudes-Dev/shuvcode - ``` - -- [ ] Record the run ID and tested commit. -- [ ] Require all current unit, generated-client, and e2e jobs to pass. -- [ ] Do not accept the older failed `8d9b20b6d` run as evidence for the release. -- [ ] If CI fails, diagnose and resolve the failure before publishing; do not bypass checks or hooks. - -**Done when:** current `integration-v2` release-prep HEAD is green and its product-code diff from the locked release SHA is empty. - -### Milestone 4: Real publish - -#### 4.1 Final preflight - -- [ ] Reconfirm npm `latest` is still `1.2.27-4`. -- [ ] Reconfirm `shuvcode@2.0.0-1` does not exist. -- [ ] Reconfirm `v2.0.0-1` does not exist as a tag or GitHub release. -- [ ] Reconfirm the build-only workflow run passed. -- [ ] Reconfirm the current-tree test run passed. -- [ ] Reconfirm trusted publishing is configured for all 13 packages. -- [ ] Review and finalize release notes. - -#### 4.2 Publish through CI - -```bash -gh workflow run snapshot.yml \ - --ref integration-v2 \ - --repo Latitudes-Dev/shuvcode \ - -f release_sha=17e77b56eb1f00210910628a102bbb301adfe603 \ - -f confirm_version=2.0.0-1 \ - -f publish=true -``` - -- [ ] Capture the exact run ID instead of watching an arbitrary recent run. -- [ ] Watch that run to completion. -- [ ] Stop on any unexpected attempt to publish `@opencode-ai/*`. -- [ ] Accept `already published` only when retrying after a confirmed partial platform-package publish. -- [ ] Never force-push or move `v2.0.0-1`. - -#### 4.3 Local fallback, only if CI publishing is unavailable - -The local fallback requires the optional granular token from Milestone 0 and must use the same locked SHA/version. - -```bash -export OPENCODE_CHANNEL=latest -export OPENCODE_VERSION=2.0.0-1 - -bun ./packages/cli/script/build.ts -bun ./packages/cli/script/publish.ts -``` - -Then create the release with the explicit target: - -```bash -gh release create v2.0.0-1 \ - --repo Latitudes-Dev/shuvcode \ - --target 17e77b56eb1f00210910628a102bbb301adfe603 \ - --title "v2.0.0-1" \ - --notes-file RELEASE-NOTES-2.0.0-1.md -``` - -- [ ] Do not use the local fallback merely because CI takes longer than expected. -- [ ] If CI partially published platform packages, inspect registry state first; the publish script skips versions that already exist and can safely retry missing packages before the umbrella. - -**Done when:** npm and GitHub both contain the intended immutable release from the locked source SHA. - -### Milestone 5: Verification - -#### 5.1 Verify all npm packages - -```bash -npm view shuvcode version -npm view shuvcode dist-tags --json -npm view shuvcode optionalDependencies --json -npm view shuvcode-linux-x64-baseline-musl version -``` - -- [ ] `shuvcode` reports `2.0.0-1`. -- [ ] `latest` points to `2.0.0-1`. -- [ ] The umbrella contains exactly 12 optional platform dependencies at `2.0.0-1`. -- [ ] Every package in the locked 13-package list exists at `2.0.0-1`. -- [ ] No `@opencode-ai/*` package version was published by this workflow. - -#### 5.2 Verify GitHub tag and release - -```bash -gh release view v2.0.0-1 \ - --repo Latitudes-Dev/shuvcode \ - --json tagName,isDraft,isPrerelease,targetCommitish,url - -gh api repos/Latitudes-Dev/shuvcode/git/ref/tags/v2.0.0-1 -``` - -- [ ] Release is published, not draft, and not prerelease. -- [ ] Tag resolves to `17e77b56eb1f00210910628a102bbb301adfe603`. -- [ ] Release notes contain the install command, binary name, and source SHA. -- [ ] No duplicate or raw binary assets were attached. - -#### 5.3 Install into an isolated prefix - -Do not overwrite the working global installation during the first smoke: - -```bash -tmp="$(mktemp -d)" -npm install --prefix "$tmp" shuvcode@2.0.0-1 -"$tmp/node_modules/.bin/shuvcode" --version -``` - -- [ ] Installation selects a compatible platform package. -- [ ] Binary reports `shuvcode v2.0.0-1`. -- [ ] Basic non-mutating CLI help/version commands run. - -#### 5.4 Verify the existing wrapper and shared host - -The shared host already reports `2.0.0-1`. Treat it as a post-release consistency check, not a reason to restart a healthy service. - -```bash -readlink -f "$(command -v shuvcode)" -shuvcode --version -systemctl --user status shuvcode.service --no-pager - -set -a -source ~/.config/openshuv/shuvcode.env -set +a -curl -fsS \ - -u "opencode:$OPENCODE_SERVER_PASSWORD" \ - http://100.126.224.77:4096/api/health -``` - -- [ ] Wrapper resolves to a real binary and reports `2.0.0-1`. -- [ ] Authenticated health remains 200 and reports `2.0.0-1`. -- [ ] Local TUI attaches without `Failed to start server`. -- [ ] Do not restart the unit or rewrite registration files unless the installed binary or authentication state is intentionally changed. - -**Done when:** registry, tag, release, isolated install, wrapper, and shared-server checks all agree on `2.0.0-1`. - -### Milestone 6: Post-release follow-ups - -These are separate from the first release and must not delay it after all success criteria pass. - -- [ ] Document the fork release procedure in repo guidance so future operators never use root `script/publish.ts` or upstream `publish.yml`. -- [ ] Decide whether future `2.0.0-N` releases should remain manually pinned or use `nextForkVersion` against npm latest. -- [ ] Decide whether `integration-v2` should eventually become the default branch or whether a release workflow should be installed on the existing default branch. -- [ ] Only after that topology decision, reconsider automatic `workflow_run` publishing. -- [ ] Keep the default-branch `snapshot.yml` registration until manual V2 dispatch no longer depends on it. -- [ ] If GitHub binary assets become desirable, add uniquely named archives and checksums rather than raw repeated basenames. -- [ ] If Discord notification is restored, decide whether to use a PAT/GitHub App so the release event can trigger downstream automation; keep it non-blocking. -- [ ] Revisit fork-scoped publication of shared SDK packages separately; never publish upstream-owned package names. -- [ ] Source-control the OpenShuv systemd unit in its owning dotfiles/setup repository if it must survive reprovisioning. - -## Failure and retry behavior - -`packages/cli/script/publish.ts` publishes platform packages concurrently, waits for them, and publishes the umbrella package last. Before each publish it checks whether that exact package version already exists. - -Consequences: - -- A failed run can leave a subset of platform packages published. -- npm versions are immutable; there is no rollback of an already published package version. -- A retry may safely skip packages already at `2.0.0-1` and publish missing platform packages. -- The umbrella should not publish until every platform publish in the current run succeeds. -- If a bad umbrella package ships, do not move or overwrite `2.0.0-1`; prepare `2.0.0-2` under a new reviewed plan. +The first 13 names already exist under the expected npm maintainer, `kcrommett`. The six `shuvcode-node*` names are currently unowned and are a human prerequisite: `kcrommett` must manually bootstrap each package using a short-lived granular npm token. Do not use the release workflow to bootstrap names. -## Risk register +After every package exists, configure its npm trusted publisher with these exact values: -| Risk | Impact | Mitigation | -| ------------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------- | -| Trusted publisher missing on one package | Partial platform publish | Configure and check all 13 package settings before the real dispatch. | -| Missing baseline-musl package authorization | Linux baseline musl installs fail | Keep `shuvcode-linux-x64-baseline-musl` in every package list and validation. | -| Missing channel/version env | Preview version published under wrong tag | Pin both `OPENCODE_CHANNEL` and `OPENCODE_VERSION`. | -| Workflow builds its own prep commit | Tag source differs from approved product source | Require `release_sha` and checkout the locked SHA explicitly. | -| GitHub release tags default branch | V2 tag points to frozen V1 | Always pass `--target "$RELEASE_SHA"`. | -| Raw asset basename collisions | GitHub release creation/upload fails | Omit assets for this cut; later archive with unique platform names. | -| No current CI evidence | Broken release reaches npm | Require a build-only dispatch and a green manual test run before publishing. | -| Blacksmith unavailable | Job remains queued | Commit a runner change to `ubuntu-latest` and repeat build-only validation. | -| Concurrent real dispatches | Duplicate/partial registry work | Fixed concurrency group, `cancel-in-progress: false`, explicit confirmation inputs. | -| Partial platform publish | Registry temporarily inconsistent | Inspect package state and retry; umbrella remains last. | -| Force-moving a tag | Release history corruption | Never move `v2.0.0-1`; issue `2.0.0-2` for shipped defects. | -| Restarting healthy OpenShuv service unnecessarily | Avoidable operator outage | Verify first; restart only after an intentional binary/auth change. | +| Field | Value | +| --- | --- | +| Provider | GitHub Actions | +| Organization | `Latitudes-Dev` | +| Repository | `shuvcode` | +| Workflow filename | `publish.yml` | +| Environment | none | -## Key file references +The workflow has `id-token: write`, runs Node 24 with npm 11.5.1, and does not set `NODE_AUTH_TOKEN`. GitHub release, generated-note, tag, and push operations use the built-in `github.token`; no OPENCODE_APP credential or AI changelog secret is required. -| Path | Role | -| -------------------------------------- | -------------------------------------------------------- | -| `packages/script/src/index.ts` | Channel/version resolution and Bun version guard | -| `packages/script/src/version.ts` | Fork `2.0.0-N` counter | -| `packages/script/test/version.test.ts` | Focused version behavior tests | -| `packages/cli/script/build.ts` | Produces 12 platform package directories | -| `packages/cli/script/publish.ts` | Creates/publishes platform packages and umbrella package | -| `packages/cli/bin/shuvcode.cjs` | Resolves platform packages, including baseline-musl | -| `.github/actions/setup-bun/action.yml` | Pins Bun from root metadata and installs dependencies | -| `.github/workflows/test.yml` | Existing manual current-tree test gate | -| `.github/workflows/snapshot.yml` | Missing V2 manual release workflow to restore | -| `.github/workflows/publish.yml` | Upstream-only workflow; excluded | -| `script/publish.ts` | Multi-package upstream publisher; excluded | -| `script/version.ts` | Reference for creating a release with an explicit target | -| `script/release` | Upstream publish dispatcher; excluded | -| `.github/last-synced-tag` | V2 upstream sync commit marker, not a release tag | -| `AGENTS.md` | Fork boundaries, branch rules, hooks, and test guidance | +## Preflight and retry behavior -Historical references: +`packages/cli/script/preflight-publish.ts` queries the maintainer response for all 19 package names and validates the complete set before the workflow creates a draft release. `packages/cli/script/publish.ts` repeats that same complete preflight, then validates the exact 12 Bun and five Node platform manifests and their versions before changing `dist`, packing, checking already-published versions, or publishing. -- `8d9b20b6d`: earlier V2 snapshot/Discord workflow draft -- `655a3d6aa`: V2 fork release-readiness changes -- `17e77b56e`: locked first-release product source and V2 counter -- `origin/integration:.github/workflows/snapshot.yml`: last working V1 publish workflow +The release fails closed when the repository is missing or unknown, a package is missing or unavailable, npm returns malformed ownership data, a package response is omitted or duplicated, or `kcrommett` is not a maintainer. The six Node names therefore block every release until the manual bootstrap is complete. -## Out of scope +Version idempotence applies only after ownership succeeds. A retry skips an exact package version that already exists, publishes missing versions, and publishes each umbrella package after its platform packages. -- Publishing desktop, app, mobile, container, or package-manager artifacts -- Publishing any upstream-owned `@opencode-ai/*` package -- Uploading raw GitHub binary assets -- Automatic publishing after `test` -- Moving the repository default branch -- Restoring Discord notifications -- Upstream synchronization or merge work -- Changing `packages/core/src/global.ts` from `app = "opencode"` -- Transferring npm package ownership away from `kcrommett` -- Restarting or rewriting the healthy OpenShuv service without an intentional deployment change +## Release procedure -## Success criteria +1. Confirm all 19 package pages list `kcrommett` and authorize the `Latitudes-Dev/shuvcode` + `publish.yml` trusted publisher. +2. Ensure the intended branch and commit have passed normal tests and review. +3. Dispatch `publish.yml` manually with exactly one of `version` or `bump`. +4. Confirm the preflight reports 19 packages before any release or publish step proceeds. +5. Verify all 19 versions and dist-tags on npm, then verify the GitHub tag/release targets the intended commit. +6. If publication stopped after a subset reached npm, resolve the failure and redispatch; never overwrite an immutable npm version or move a published tag. -1. All 13 npm packages exist at `2.0.0-1`. -2. `npm view shuvcode dist-tags.latest` resolves to `2.0.0-1`. -3. The umbrella package has exactly 12 optional platform dependencies at `2.0.0-1`. -4. GitHub tag `v2.0.0-1` resolves to `17e77b56eb1f00210910628a102bbb301adfe603`. -5. GitHub release `v2.0.0-1` is published, non-draft, and contains correct V2 notes. -6. An isolated `npm install shuvcode@2.0.0-1` runs `shuvcode v2.0.0-1`. -7. No new `@opencode-ai/*` version was published by this effort. -8. The existing wrapper and authenticated shared-server health remain functional at `2.0.0-1`. -9. The release can be retried safely after a partial platform-package publish without moving the tag or republishing immutable versions. +Local tests must use synthetic npm responses. Do not use tests to call the live registry, publish packages, bootstrap names, or mutate GitHub releases. diff --git a/packages/cli/script/preflight-publish.ts b/packages/cli/script/preflight-publish.ts new file mode 100644 index 000000000000..59c2f100e2e8 --- /dev/null +++ b/packages/cli/script/preflight-publish.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env bun +import { currentRepository } from "../../../script/publish-plan" +import { preflightForkNpmOwnership } from "./publish-ownership" + +const result = await preflightForkNpmOwnership(currentRepository()) +console.log(`npm ownership preflight passed for ${result.packages.length} fork packages`) diff --git a/packages/cli/script/publish-ownership.ts b/packages/cli/script/publish-ownership.ts new file mode 100644 index 000000000000..06903dbf8fac --- /dev/null +++ b/packages/cli/script/publish-ownership.ts @@ -0,0 +1,106 @@ +import { $ } from "bun" +import { forkRepository, publishPlan } from "../../../script/publish-plan" + +export const expectedNpmMaintainer = "kcrommett" +export const forkBunPlatformPackages = [ + "shuvcode-linux-arm64", + "shuvcode-linux-arm64-musl", + "shuvcode-linux-x64", + "shuvcode-linux-x64-baseline", + "shuvcode-linux-x64-musl", + "shuvcode-linux-x64-baseline-musl", + "shuvcode-darwin-arm64", + "shuvcode-darwin-x64", + "shuvcode-darwin-x64-baseline", + "shuvcode-windows-arm64", + "shuvcode-windows-x64", + "shuvcode-windows-x64-baseline", +] as const +export const forkNodePlatformPackages = [ + "shuvcode-node-linux-arm64", + "shuvcode-node-linux-x64", + "shuvcode-node-darwin-arm64", + "shuvcode-node-windows-arm64", + "shuvcode-node-windows-x64", +] as const +export const forkNpmPackages = [ + "shuvcode", + ...forkBunPlatformPackages, + "shuvcode-node", + ...forkNodePlatformPackages, +] as const + +export type NpmOwnershipResponse = { + name: string + exitCode: number + stdout: string +} + +export function parseNpmMaintainers(stdout: string) { + const value: unknown = JSON.parse(stdout) + const entries = Array.isArray(value) ? value : [value] + return entries.map((entry) => { + if (typeof entry === "string") { + const name = entry.match(/^([^\s<]+)/)?.[1] + if (name) return name + } + if (entry && typeof entry === "object" && "name" in entry && typeof entry.name === "string" && entry.name) { + return entry.name + } + throw new Error("npm returned an invalid maintainer entry") + }) +} + +export function validateForkNpmOwnership(responses: readonly NpmOwnershipResponse[]) { + const expected = new Set(forkNpmPackages) + const seen = new Set() + for (const response of responses) { + if (!expected.has(response.name)) throw new Error(`Unexpected npm package in ownership preflight: ${response.name}`) + if (seen.has(response.name)) throw new Error(`Duplicate npm ownership response: ${response.name}`) + seen.add(response.name) + if (response.exitCode !== 0) { + throw new Error(`npm package is missing or unavailable: ${response.name}`) + } + let maintainers: string[] + try { + maintainers = parseNpmMaintainers(response.stdout) + } catch (error) { + throw new Error(`Could not parse npm maintainers for ${response.name}`, { cause: error }) + } + if (!maintainers.includes(expectedNpmMaintainer)) { + throw new Error(`npm package is not maintained by ${expectedNpmMaintainer}: ${response.name}`) + } + } + const missing = forkNpmPackages.filter((name) => !seen.has(name)) + if (missing.length) throw new Error(`Missing npm ownership responses: ${missing.join(", ")}`) + return forkNpmPackages +} + +export function planPlatformPackages(expected: readonly string[], binaries: Readonly>) { + const names = Object.keys(binaries) + const unexpected = names.filter((name) => !expected.includes(name)) + if (unexpected.length) throw new Error(`Unexpected platform packages: ${unexpected.join(", ")}`) + const missing = expected.filter((name) => !names.includes(name)) + if (missing.length) throw new Error(`Missing platform packages: ${missing.join(", ")}`) + const versions = new Set(Object.values(binaries)) + if (versions.size !== 1) throw new Error("Platform package versions do not match") + const version = versions.values().next().value + if (!version) throw new Error("No platform package versions found") + return { binaries, version } +} + +export function planForkNpmPublish(repository: string | undefined, responses: readonly NpmOwnershipResponse[]) { + const plan = publishPlan(repository) + if (repository !== forkRepository) throw new Error(`Fork npm publishing is not configured for repository: ${repository}`) + return { plan, packages: validateForkNpmOwnership(responses) } +} + +export async function preflightForkNpmOwnership(repository: string | undefined) { + const responses = await Promise.all( + forkNpmPackages.map(async (name) => { + const result = await $`npm view ${name} maintainers --json`.quiet().nothrow() + return { name, exitCode: result.exitCode, stdout: result.stdout.toString() } + }), + ) + return planForkNpmPublish(repository, responses) +} diff --git a/packages/cli/script/publish.ts b/packages/cli/script/publish.ts index f4c7222b0d04..6881a5caec37 100755 --- a/packages/cli/script/publish.ts +++ b/packages/cli/script/publish.ts @@ -5,6 +5,16 @@ import { Script } from "@opencode-ai/script" import { fileURLToPath } from "url" import { UpdateArtifact } from "../../../script/update-artifact" import { currentRepository, publishPlan } from "../../../script/publish-plan" +import { + forkBunPlatformPackages, + forkNodePlatformPackages, + planPlatformPackages, + preflightForkNpmOwnership, +} from "./publish-ownership" + +const repository = currentRepository() +const plan = publishPlan(repository) +await preflightForkNpmOwnership(repository) const dir = fileURLToPath(new URL("..", import.meta.url)) process.chdir(dir) @@ -14,13 +24,19 @@ async function published(name: string, version: string) { } async function publish(dir: string, name: string, version: string) { - if (process.platform !== "win32") await $`chmod -R 755 .`.cwd(dir) if (await published(name, version)) return console.log(`already published ${name}@${version}`) + if (process.platform !== "win32") await $`chmod -R 755 .`.cwd(dir) await $`bun pm pack`.cwd(dir) await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir) } -async function publishDistribution(input: { root: string; name: string; binary: string; packagePrefix: string }) { +async function planDistribution(input: { + root: string + name: string + binary: string + packagePrefix: string + packages: readonly string[] +}) { const binaries: Record = {} for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: input.root })) { const item = await Bun.file(`${input.root}/${filepath}`).json() @@ -28,11 +44,10 @@ async function publishDistribution(input: { root: string; name: string; binary: binaries[item.name] = item.version } console.log(input.name, "binaries", binaries) - const versions = new Set(Object.values(binaries)) - if (versions.size > 1) throw new Error(`Binary package versions do not match for ${input.name}`) - const version = versions.values().next().value - if (!version) throw new Error(`No binary packages found for ${input.name}`) + return { ...input, ...planPlatformPackages(input.packages, binaries) } +} +async function publishDistribution(input: Awaited>) { await $`mkdir -p ${input.root}/${input.name}/bin` await $`cp ./script/postinstall.mjs ${input.root}/${input.name}/postinstall.mjs` await Bun.file(`${input.root}/${input.name}/bin/${input.binary}.exe`).write( @@ -51,35 +66,42 @@ async function publishDistribution(input: { root: string; name: string; binary: name: input.name, bin: { [input.binary]: `./bin/${input.binary}.exe` }, scripts: { postinstall: "node ./postinstall.mjs" }, - version, + version: input.version, license: pkg.license, repository: { type: "git", url: "git+https://github.com/Latitudes-Dev/shuvcode.git" }, os: ["darwin", "linux", "win32"], cpu: ["arm64", "x64"], - optionalDependencies: binaries, + optionalDependencies: input.binaries, }, null, 2, ), ) - await Promise.all(Object.entries(binaries).map(([name, version]) => publish(`${input.root}/${name}`, name, version))) - await publish(`${input.root}/${input.name}`, input.name, version) + await Promise.all( + Object.entries(input.binaries).map(([name, version]) => publish(`${input.root}/${name}`, name, version)), + ) + await publish(`${input.root}/${input.name}`, input.name, input.version) } -await publishDistribution({ - root: "./dist", - name: pkg.name, - binary: "shuvcode", - packagePrefix: "shuvcode-", -}) -await publishDistribution({ - root: "./dist/node", - name: "shuvcode-node", - binary: "shuvcode-node", - packagePrefix: "shuvcode-node-", -}) -if (publishPlan(currentRepository()).updateArtifacts) { +const distributions = await Promise.all([ + planDistribution({ + root: "./dist", + name: pkg.name, + binary: "shuvcode", + packagePrefix: "shuvcode-", + packages: forkBunPlatformPackages, + }), + planDistribution({ + root: "./dist/node", + name: "shuvcode-node", + binary: "shuvcode-node", + packagePrefix: "shuvcode-node-", + packages: forkNodePlatformPackages, + }), +]) +for (const distribution of distributions) await publishDistribution(distribution) +if (plan.updateArtifacts) { await UpdateArtifact.publish({ channel: Script.channel, name: "cli", diff --git a/packages/cli/test/publish-ownership.test.ts b/packages/cli/test/publish-ownership.test.ts new file mode 100644 index 000000000000..e61686eb483b --- /dev/null +++ b/packages/cli/test/publish-ownership.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, test } from "bun:test" +import { forkRepository } from "../../../script/publish-plan" +import { + expectedNpmMaintainer, + forkNpmPackages, + parseNpmMaintainers, + planForkNpmPublish, + planPlatformPackages, + validateForkNpmOwnership, +} from "../script/publish-ownership" + +const owned = () => + forkNpmPackages.map((name) => ({ + name, + exitCode: 0, + stdout: JSON.stringify([`${expectedNpmMaintainer} `]), + })) + +describe("fork npm publish ownership", () => { + test("defines the complete 19-package publication", () => { + expect(forkNpmPackages).toHaveLength(19) + expect(new Set(forkNpmPackages).size).toBe(19) + expect(forkNpmPackages.filter((name) => name.startsWith("shuvcode-node"))).toEqual([ + "shuvcode-node", + "shuvcode-node-linux-arm64", + "shuvcode-node-linux-x64", + "shuvcode-node-darwin-arm64", + "shuvcode-node-windows-arm64", + "shuvcode-node-windows-x64", + ]) + }) + + test("parses npm string and object maintainer responses", () => { + expect(parseNpmMaintainers(JSON.stringify(["kcrommett ", { name: "other" }]))).toEqual([ + "kcrommett", + "other", + ]) + expect(parseNpmMaintainers(JSON.stringify({ name: "kcrommett", email: "release@example.com" }))).toEqual([ + "kcrommett", + ]) + expect(() => parseNpmMaintainers("not json")).toThrow() + expect(() => parseNpmMaintainers(JSON.stringify([{ email: "release@example.com" }]))).toThrow( + "npm returned an invalid maintainer entry", + ) + }) + + test("accepts only a complete owned package set", () => { + expect(validateForkNpmOwnership(owned())).toEqual(forkNpmPackages) + expect(() => validateForkNpmOwnership(owned().slice(1))).toThrow("Missing npm ownership responses") + + const unavailable = owned() + unavailable[0] = { ...unavailable[0], exitCode: 1, stdout: "" } + expect(() => validateForkNpmOwnership(unavailable)).toThrow("npm package is missing or unavailable: shuvcode") + + const hijacked = owned() + hijacked[0] = { ...hijacked[0], stdout: JSON.stringify(["other "]) } + expect(() => validateForkNpmOwnership(hijacked)).toThrow("npm package is not maintained by kcrommett: shuvcode") + }) + + test("plans a complete coherent artifact set before publication", () => { + expect(planPlatformPackages(["a", "b"], { a: "1.0.0", b: "1.0.0" })).toEqual({ + binaries: { a: "1.0.0", b: "1.0.0" }, + version: "1.0.0", + }) + expect(() => planPlatformPackages(["a", "b"], { a: "1.0.0" })).toThrow("Missing platform packages: b") + expect(() => planPlatformPackages(["a"], { a: "1.0.0", b: "1.0.0" })).toThrow( + "Unexpected platform packages: b", + ) + expect(() => planPlatformPackages(["a", "b"], { a: "1.0.0", b: "2.0.0" })).toThrow( + "Platform package versions do not match", + ) + }) + + test("fails repository planning before ownership parsing", () => { + const invalid = [{ name: "shuvcode", exitCode: 0, stdout: "not json" }] + expect(() => planForkNpmPublish(undefined, invalid)).toThrow( + "Publishing is not configured for repository: unknown", + ) + expect(planForkNpmPublish(forkRepository, owned()).packages).toEqual(forkNpmPackages) + }) +}) diff --git a/script/version.ts b/script/version.ts index 0582f49a3f91..2e87ea772b85 100755 --- a/script/version.ts +++ b/script/version.ts @@ -2,20 +2,27 @@ import { Script } from "@opencode-ai/script" import { $ } from "bun" +import { currentRepository, forkRepository } from "./publish-plan" const output = [`version=${Script.version}`] const sha = process.env.GITHUB_SHA ?? (await $`git rev-parse HEAD`.text()).trim() if (!Script.preview) { - await $`bun script/changelog.ts --to ${sha}`.cwd(process.cwd()) - const file = `${process.cwd()}/UPCOMING_CHANGELOG.md` - const body = await Bun.file(file) - .text() - .catch(() => "No notable changes") - const dir = process.env.RUNNER_TEMP ?? "/tmp" - const notesFile = `${dir}/opencode-release-notes.txt` - await Bun.write(notesFile, body) - await $`gh release create v${Script.version} -d --target ${sha} --title "v${Script.version}" --notes-file ${notesFile}` + const fork = currentRepository() === forkRepository + if (fork) { + await $`gh release create v${Script.version} -d --target ${sha} --title "v${Script.version}" --generate-notes` + } + if (!fork) { + await $`bun script/changelog.ts --to ${sha}`.cwd(process.cwd()) + const file = `${process.cwd()}/UPCOMING_CHANGELOG.md` + const body = await Bun.file(file) + .text() + .catch(() => "No notable changes") + const dir = process.env.RUNNER_TEMP ?? "/tmp" + const notesFile = `${dir}/opencode-release-notes.txt` + await Bun.write(notesFile, body) + await $`gh release create v${Script.version} -d --target ${sha} --title "v${Script.version}" --notes-file ${notesFile}` + } const release = await $`gh release view v${Script.version} --json tagName,databaseId`.json() output.push(`release=${release.databaseId}`) output.push(`tag=${release.tagName}`) From 09aa64f2202b1f514fb538eb4721b56aa1da9005 Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 27 Jul 2026 03:28:00 -0700 Subject: [PATCH 150/150] fix(ci): make fork releases retryable --- PLAN-v2-release-publish.md | 20 ++--- packages/cli/script/publish-order.ts | 19 +++++ packages/cli/script/publish-ownership.ts | 87 ++++++++++++++++++--- packages/cli/script/publish.ts | 53 ++----------- packages/cli/test/publish-order.test.ts | 44 +++++++++++ packages/cli/test/publish-ownership.test.ts | 29 +++++-- script/publish.ts | 4 +- script/version-plan.test.ts | 29 +++++++ script/version-plan.ts | 26 ++++++ script/version.ts | 49 +++++++++--- 10 files changed, 271 insertions(+), 89 deletions(-) create mode 100644 packages/cli/script/publish-order.ts create mode 100644 packages/cli/test/publish-order.test.ts create mode 100644 script/version-plan.test.ts create mode 100644 script/version-plan.ts diff --git a/PLAN-v2-release-publish.md b/PLAN-v2-release-publish.md index 5457703ae206..0b429fb05d0a 100644 --- a/PLAN-v2-release-publish.md +++ b/PLAN-v2-release-publish.md @@ -30,23 +30,25 @@ The first 13 names already exist under the expected npm maintainer, `kcrommett`. After every package exists, configure its npm trusted publisher with these exact values: -| Field | Value | -| --- | --- | -| Provider | GitHub Actions | -| Organization | `Latitudes-Dev` | -| Repository | `shuvcode` | -| Workflow filename | `publish.yml` | -| Environment | none | +| Field | Value | +| ----------------- | --------------- | +| Provider | GitHub Actions | +| Organization | `Latitudes-Dev` | +| Repository | `shuvcode` | +| Workflow filename | `publish.yml` | +| Environment | none | The workflow has `id-token: write`, runs Node 24 with npm 11.5.1, and does not set `NODE_AUTH_TOKEN`. GitHub release, generated-note, tag, and push operations use the built-in `github.token`; no OPENCODE_APP credential or AI changelog secret is required. ## Preflight and retry behavior -`packages/cli/script/preflight-publish.ts` queries the maintainer response for all 19 package names and validates the complete set before the workflow creates a draft release. `packages/cli/script/publish.ts` repeats that same complete preflight, then validates the exact 12 Bun and five Node platform manifests and their versions before changing `dist`, packing, checking already-published versions, or publishing. +`packages/cli/script/preflight-publish.ts` queries the maintainer response for all 19 package names and validates the complete set before the workflow creates a draft release. The root publisher repeats the authoritative CLI preflight before git detach, package rewrites, installation, or any other mutation. That preflight also requires every one of the exact 12 Bun and five Node platform manifests to match the requested release version. The CLI then prepares both wrappers, publishes all 17 platform packages, and only after every platform succeeds publishes `shuvcode` followed by `shuvcode-node`. The release fails closed when the repository is missing or unknown, a package is missing or unavailable, npm returns malformed ownership data, a package response is omitted or duplicated, or `kcrommett` is not a maintainer. The six Node names therefore block every release until the manual bootstrap is complete. -Version idempotence applies only after ownership succeeds. A retry skips an exact package version that already exists, publishes missing versions, and publishes each umbrella package after its platform packages. +Version idempotence applies only after the complete preflight succeeds. Redispatching the same explicit version, or the same bump while the latest published version is unchanged, reuses an existing fork draft only when its tag name, title, target commit, and any existing tag target exactly match. A published release, a mismatched draft, or an unrelated tag fails without being overwritten. Reused drafts retain their generated notes because the target release is unchanged. npm retries skip exact package versions that already exist, publish missing platform versions, and defer both umbrellas until all platform versions exist. + +npm publication is unavoidably non-transactional: a failure can leave an immutable subset of the 19 versions published. After correcting the cause, rerun the same release input so exact versions are reconciled and only missing packages are published; never choose a new version merely to hide a partial publication. ## Release procedure diff --git a/packages/cli/script/publish-order.ts b/packages/cli/script/publish-order.ts new file mode 100644 index 000000000000..42aef3ca3403 --- /dev/null +++ b/packages/cli/script/publish-order.ts @@ -0,0 +1,19 @@ +import type { ForkDistribution } from "./publish-ownership" + +export async function publishDistributions( + distributions: readonly ForkDistribution[], + operation: { + prepare(distribution: ForkDistribution): Promise + publish(root: string, name: string, version: string): Promise + }, +) { + for (const distribution of distributions) await operation.prepare(distribution) + for (const distribution of distributions) { + for (const name of distribution.packages) { + await operation.publish(`${distribution.root}/${name}`, name, distribution.binaries[name]) + } + } + for (const distribution of distributions) { + await operation.publish(`${distribution.root}/${distribution.name}`, distribution.name, distribution.version) + } +} diff --git a/packages/cli/script/publish-ownership.ts b/packages/cli/script/publish-ownership.ts index 06903dbf8fac..2998ab202247 100644 --- a/packages/cli/script/publish-ownership.ts +++ b/packages/cli/script/publish-ownership.ts @@ -1,4 +1,5 @@ import { $ } from "bun" +import { fileURLToPath } from "url" import { forkRepository, publishPlan } from "../../../script/publish-plan" export const expectedNpmMaintainer = "kcrommett" @@ -36,6 +37,16 @@ export type NpmOwnershipResponse = { stdout: string } +export type ForkDistribution = { + root: string + name: string + binary: string + packagePrefix: string + packages: readonly string[] + binaries: Readonly> + version: string +} + export function parseNpmMaintainers(stdout: string) { const value: unknown = JSON.parse(stdout) const entries = Array.isArray(value) ? value : [value] @@ -76,31 +87,81 @@ export function validateForkNpmOwnership(responses: readonly NpmOwnershipRespons return forkNpmPackages } -export function planPlatformPackages(expected: readonly string[], binaries: Readonly>) { +export function planPlatformPackages( + expected: readonly string[], + binaries: Readonly>, + version: string, +) { const names = Object.keys(binaries) const unexpected = names.filter((name) => !expected.includes(name)) if (unexpected.length) throw new Error(`Unexpected platform packages: ${unexpected.join(", ")}`) const missing = expected.filter((name) => !names.includes(name)) if (missing.length) throw new Error(`Missing platform packages: ${missing.join(", ")}`) - const versions = new Set(Object.values(binaries)) - if (versions.size !== 1) throw new Error("Platform package versions do not match") - const version = versions.values().next().value - if (!version) throw new Error("No platform package versions found") + const mismatched = names.filter((name) => binaries[name] !== version) + if (mismatched.length) { + throw new Error(`Platform package versions do not match release ${version}: ${mismatched.join(", ")}`) + } return { binaries, version } } export function planForkNpmPublish(repository: string | undefined, responses: readonly NpmOwnershipResponse[]) { const plan = publishPlan(repository) - if (repository !== forkRepository) throw new Error(`Fork npm publishing is not configured for repository: ${repository}`) + if (repository !== forkRepository) + throw new Error(`Fork npm publishing is not configured for repository: ${repository}`) return { plan, packages: validateForkNpmOwnership(responses) } } -export async function preflightForkNpmOwnership(repository: string | undefined) { - const responses = await Promise.all( - forkNpmPackages.map(async (name) => { - const result = await $`npm view ${name} maintainers --json`.quiet().nothrow() - return { name, exitCode: result.exitCode, stdout: result.stdout.toString() } - }), - ) +export async function preflightForkNpmOwnership( + repository: string | undefined, + viewMaintainers = async (name: string) => { + const result = await $`npm view ${name} maintainers --json`.quiet().nothrow() + return { name, exitCode: result.exitCode, stdout: result.stdout.toString() } + }, +) { + publishPlan(repository) + if (repository !== forkRepository) + throw new Error(`Fork npm publishing is not configured for repository: ${repository}`) + const responses = await Promise.all(forkNpmPackages.map((name) => viewMaintainers(name))) return planForkNpmPublish(repository, responses) } + +export async function planDistribution( + input: Omit, + version: string, +): Promise { + const binaries: Record = {} + for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: input.root })) { + const item = await Bun.file(`${input.root}/${filepath}`).json() + if (!item.name.startsWith(input.packagePrefix)) continue + binaries[item.name] = item.version + } + return { ...input, ...planPlatformPackages(input.packages, binaries, version) } +} + +export async function preflightForkPublish(repository: string | undefined, version: string) { + const ownership = await preflightForkNpmOwnership(repository) + const cli = fileURLToPath(new URL("..", import.meta.url)) + const distributions = await Promise.all([ + planDistribution( + { + root: `${cli}/dist`, + name: "shuvcode", + binary: "shuvcode", + packagePrefix: "shuvcode-", + packages: forkBunPlatformPackages, + }, + version, + ), + planDistribution( + { + root: `${cli}/dist/node`, + name: "shuvcode-node", + binary: "shuvcode-node", + packagePrefix: "shuvcode-node-", + packages: forkNodePlatformPackages, + }, + version, + ), + ]) + return { ...ownership, distributions } +} diff --git a/packages/cli/script/publish.ts b/packages/cli/script/publish.ts index 6881a5caec37..8b0d876af758 100755 --- a/packages/cli/script/publish.ts +++ b/packages/cli/script/publish.ts @@ -5,16 +5,12 @@ import { Script } from "@opencode-ai/script" import { fileURLToPath } from "url" import { UpdateArtifact } from "../../../script/update-artifact" import { currentRepository, publishPlan } from "../../../script/publish-plan" -import { - forkBunPlatformPackages, - forkNodePlatformPackages, - planPlatformPackages, - preflightForkNpmOwnership, -} from "./publish-ownership" +import { preflightForkPublish, type ForkDistribution } from "./publish-ownership" +import { publishDistributions } from "./publish-order" const repository = currentRepository() const plan = publishPlan(repository) -await preflightForkNpmOwnership(repository) +const preflight = await preflightForkPublish(repository, Script.version) const dir = fileURLToPath(new URL("..", import.meta.url)) process.chdir(dir) @@ -30,24 +26,8 @@ async function publish(dir: string, name: string, version: string) { await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir) } -async function planDistribution(input: { - root: string - name: string - binary: string - packagePrefix: string - packages: readonly string[] -}) { - const binaries: Record = {} - for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: input.root })) { - const item = await Bun.file(`${input.root}/${filepath}`).json() - if (!item.name.startsWith(input.packagePrefix)) continue - binaries[item.name] = item.version - } - console.log(input.name, "binaries", binaries) - return { ...input, ...planPlatformPackages(input.packages, binaries) } -} - -async function publishDistribution(input: Awaited>) { +async function prepareDistribution(input: ForkDistribution) { + console.log(input.name, "binaries", input.binaries) await $`mkdir -p ${input.root}/${input.name}/bin` await $`cp ./script/postinstall.mjs ${input.root}/${input.name}/postinstall.mjs` await Bun.file(`${input.root}/${input.name}/bin/${input.binary}.exe`).write( @@ -77,30 +57,9 @@ async function publishDistribution(input: Awaited publish(`${input.root}/${name}`, name, version)), - ) - await publish(`${input.root}/${input.name}`, input.name, input.version) } -const distributions = await Promise.all([ - planDistribution({ - root: "./dist", - name: pkg.name, - binary: "shuvcode", - packagePrefix: "shuvcode-", - packages: forkBunPlatformPackages, - }), - planDistribution({ - root: "./dist/node", - name: "shuvcode-node", - binary: "shuvcode-node", - packagePrefix: "shuvcode-node-", - packages: forkNodePlatformPackages, - }), -]) -for (const distribution of distributions) await publishDistribution(distribution) +await publishDistributions(preflight.distributions, { prepare: prepareDistribution, publish }) if (plan.updateArtifacts) { await UpdateArtifact.publish({ channel: Script.channel, diff --git a/packages/cli/test/publish-order.test.ts b/packages/cli/test/publish-order.test.ts new file mode 100644 index 000000000000..aabcf2c66c2f --- /dev/null +++ b/packages/cli/test/publish-order.test.ts @@ -0,0 +1,44 @@ +import { expect, test } from "bun:test" +import { publishDistributions } from "../script/publish-order" +import type { ForkDistribution } from "../script/publish-ownership" + +test("prepares every wrapper, then publishes all platforms before either umbrella", async () => { + const events: string[] = [] + const distributions: ForkDistribution[] = [ + { + root: "/bun", + name: "shuvcode", + binary: "shuvcode", + packagePrefix: "shuvcode-", + packages: ["shuvcode-a", "shuvcode-b"], + binaries: { "shuvcode-a": "1.2.3", "shuvcode-b": "1.2.3" }, + version: "1.2.3", + }, + { + root: "/node", + name: "shuvcode-node", + binary: "shuvcode-node", + packagePrefix: "shuvcode-node-", + packages: ["shuvcode-node-a"], + binaries: { "shuvcode-node-a": "1.2.3" }, + version: "1.2.3", + }, + ] + await publishDistributions(distributions, { + prepare: async (distribution) => { + events.push(`prepare:${distribution.name}`) + }, + publish: async (_root, name) => { + events.push(`publish:${name}`) + }, + }) + expect(events).toEqual([ + "prepare:shuvcode", + "prepare:shuvcode-node", + "publish:shuvcode-a", + "publish:shuvcode-b", + "publish:shuvcode-node-a", + "publish:shuvcode", + "publish:shuvcode-node", + ]) +}) diff --git a/packages/cli/test/publish-ownership.test.ts b/packages/cli/test/publish-ownership.test.ts index e61686eb483b..5afe6264ce2d 100644 --- a/packages/cli/test/publish-ownership.test.ts +++ b/packages/cli/test/publish-ownership.test.ts @@ -6,6 +6,7 @@ import { parseNpmMaintainers, planForkNpmPublish, planPlatformPackages, + preflightForkNpmOwnership, validateForkNpmOwnership, } from "../script/publish-ownership" @@ -58,24 +59,36 @@ describe("fork npm publish ownership", () => { }) test("plans a complete coherent artifact set before publication", () => { - expect(planPlatformPackages(["a", "b"], { a: "1.0.0", b: "1.0.0" })).toEqual({ + expect(planPlatformPackages(["a", "b"], { a: "1.0.0", b: "1.0.0" }, "1.0.0")).toEqual({ binaries: { a: "1.0.0", b: "1.0.0" }, version: "1.0.0", }) - expect(() => planPlatformPackages(["a", "b"], { a: "1.0.0" })).toThrow("Missing platform packages: b") - expect(() => planPlatformPackages(["a"], { a: "1.0.0", b: "1.0.0" })).toThrow( + expect(() => planPlatformPackages(["a", "b"], { a: "1.0.0" }, "1.0.0")).toThrow("Missing platform packages: b") + expect(() => planPlatformPackages(["a"], { a: "1.0.0", b: "1.0.0" }, "1.0.0")).toThrow( "Unexpected platform packages: b", ) - expect(() => planPlatformPackages(["a", "b"], { a: "1.0.0", b: "2.0.0" })).toThrow( - "Platform package versions do not match", + expect(() => planPlatformPackages(["a", "b"], { a: "1.0.0", b: "2.0.0" }, "1.0.0")).toThrow( + "Platform package versions do not match release 1.0.0: b", + ) + expect(() => planPlatformPackages(["a", "b"], { a: "2.0.0", b: "2.0.0" }, "1.0.0")).toThrow( + "Platform package versions do not match release 1.0.0: a, b", ) }) test("fails repository planning before ownership parsing", () => { const invalid = [{ name: "shuvcode", exitCode: 0, stdout: "not json" }] - expect(() => planForkNpmPublish(undefined, invalid)).toThrow( - "Publishing is not configured for repository: unknown", - ) + expect(() => planForkNpmPublish(undefined, invalid)).toThrow("Publishing is not configured for repository: unknown") expect(planForkNpmPublish(forkRepository, owned()).packages).toEqual(forkNpmPackages) }) + + test("fails a missing repository before querying npm", async () => { + const queried: string[] = [] + await expect( + preflightForkNpmOwnership(undefined, async (name) => { + queried.push(name) + throw new Error("must not query") + }), + ).rejects.toThrow("Publishing is not configured for repository: unknown") + expect(queried).toEqual([]) + }) }) diff --git a/script/publish.ts b/script/publish.ts index e1619bbeb4aa..62da54cf4132 100755 --- a/script/publish.ts +++ b/script/publish.ts @@ -5,14 +5,16 @@ import { $ } from "bun" import { fileURLToPath } from "url" import { UpdateArtifact } from "./update-artifact" import { currentRepository, forkRepository, publishPlan } from "./publish-plan" +import { preflightForkPublish } from "../packages/cli/script/publish-ownership" console.log("=== publishing ===\n") const dir = fileURLToPath(new URL("..", import.meta.url)) -process.chdir(dir) const tag = `v${Script.version}` const repository = currentRepository() const plan = publishPlan(repository) +if (repository === forkRepository) await preflightForkPublish(repository, Script.version) +process.chdir(dir) const pkgjsons = await Array.fromAsync( new Bun.Glob("**/package.json").scan({ diff --git a/script/version-plan.test.ts b/script/version-plan.test.ts new file mode 100644 index 000000000000..920c90181ab9 --- /dev/null +++ b/script/version-plan.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from "bun:test" +import { planDraftRelease } from "./version-plan" + +const tag = "v1.2.3" +const target = "0123456789abcdef" +const draft = { tagName: tag, name: tag, targetCommitish: target, isDraft: true } + +describe("draft release planning", () => { + test("creates only when neither release nor tag exists", () => { + expect(planDraftRelease(tag, target, undefined, undefined)).toBe("create") + expect(() => planDraftRelease(tag, target, undefined, target)).toThrow( + "already exists without a matching draft release", + ) + }) + + test("reuses an exact matching draft with or without its exact tag", () => { + expect(planDraftRelease(tag, target, draft, undefined)).toBe("reuse") + expect(planDraftRelease(tag, target, draft, target)).toBe("reuse") + }) + + test("never overwrites a published or unrelated release or tag", () => { + expect(() => planDraftRelease(tag, target, { ...draft, isDraft: false }, undefined)).toThrow("already published") + expect(() => planDraftRelease(tag, target, { ...draft, targetCommitish: "other" }, undefined)).toThrow( + "targets other", + ) + expect(() => planDraftRelease(tag, target, { ...draft, name: "other" }, undefined)).toThrow("does not match") + expect(() => planDraftRelease(tag, target, draft, "other")).toThrow("targets other") + }) +}) diff --git a/script/version-plan.ts b/script/version-plan.ts new file mode 100644 index 000000000000..ea4bff7cd134 --- /dev/null +++ b/script/version-plan.ts @@ -0,0 +1,26 @@ +export type DraftRelease = { + tagName: string + name: string + targetCommitish: string + isDraft: boolean +} + +export function planDraftRelease( + tag: string, + target: string, + release: DraftRelease | undefined, + tagTarget: string | undefined, +) { + if (!release) { + if (tagTarget) throw new Error(`Tag ${tag} already exists without a matching draft release`) + return "create" as const + } + if (!release.isDraft) throw new Error(`Release ${tag} is already published`) + if (release.tagName !== tag || release.name !== tag) + throw new Error(`Draft release ${tag} does not match the target release`) + if (release.targetCommitish !== target) { + throw new Error(`Draft release ${tag} targets ${release.targetCommitish}, not ${target}`) + } + if (tagTarget && tagTarget !== target) throw new Error(`Tag ${tag} targets ${tagTarget}, not ${target}`) + return "reuse" as const +} diff --git a/script/version.ts b/script/version.ts index 2e87ea772b85..9056229cb526 100755 --- a/script/version.ts +++ b/script/version.ts @@ -2,15 +2,43 @@ import { Script } from "@opencode-ai/script" import { $ } from "bun" -import { currentRepository, forkRepository } from "./publish-plan" +import { currentRepository, forkRepository, publishPlan } from "./publish-plan" +import { planDraftRelease, type DraftRelease } from "./version-plan" +const repository = currentRepository() +publishPlan(repository) const output = [`version=${Script.version}`] const sha = process.env.GITHUB_SHA ?? (await $`git rev-parse HEAD`.text()).trim() +const tag = `v${Script.version}` + +async function prepareForkDraft() { + const releaseFilter = "{tagName:.tag_name,name:.name,targetCommitish:.target_commitish,isDraft:.draft}" + const releaseResult = await $`gh api ${`repos/${repository}/releases/tags/${tag}`} --jq ${releaseFilter}` + .quiet() + .nothrow() + if (releaseResult.exitCode !== 0 && !releaseResult.stderr.toString().includes("HTTP 404")) { + throw new Error(`Could not inspect release ${tag}: ${releaseResult.stderr.toString().trim()}`) + } + const release = + releaseResult.exitCode === 0 ? (JSON.parse(releaseResult.stdout.toString()) as DraftRelease) : undefined + const tagResult = await $`gh api ${`repos/${repository}/git/ref/tags/${tag}`} --jq .object.sha`.quiet().nothrow() + if (tagResult.exitCode !== 0 && !tagResult.stderr.toString().includes("HTTP 404")) { + throw new Error(`Could not inspect tag ${tag}: ${tagResult.stderr.toString().trim()}`) + } + const tagTarget = tagResult.exitCode === 0 ? tagResult.stdout.toString().trim() : undefined + const decision = planDraftRelease(tag, sha, release, tagTarget) + if (decision === "create") { + await $`gh release create ${tag} -d --repo ${repository} --target ${sha} --title ${tag} --generate-notes` + } + return await $`gh release view ${tag} --repo ${repository} --json tagName,databaseId`.json() +} if (!Script.preview) { - const fork = currentRepository() === forkRepository + const fork = repository === forkRepository if (fork) { - await $`gh release create v${Script.version} -d --target ${sha} --title "v${Script.version}" --generate-notes` + const release = await prepareForkDraft() + output.push(`release=${release.databaseId}`) + output.push(`tag=${release.tagName}`) } if (!fork) { await $`bun script/changelog.ts --to ${sha}`.cwd(process.cwd()) @@ -21,20 +49,19 @@ if (!Script.preview) { const dir = process.env.RUNNER_TEMP ?? "/tmp" const notesFile = `${dir}/opencode-release-notes.txt` await Bun.write(notesFile, body) - await $`gh release create v${Script.version} -d --target ${sha} --title "v${Script.version}" --notes-file ${notesFile}` + await $`gh release create ${tag} -d --repo ${repository} --target ${sha} --title ${tag} --notes-file ${notesFile}` + const release = await $`gh release view ${tag} --repo ${repository} --json tagName,databaseId`.json() + output.push(`release=${release.databaseId}`) + output.push(`tag=${release.tagName}`) } - const release = await $`gh release view v${Script.version} --json tagName,databaseId`.json() - output.push(`release=${release.databaseId}`) - output.push(`tag=${release.tagName}`) } else if (Script.channel === "beta") { - await $`gh release create v${Script.version} -d --title "v${Script.version}" --repo ${process.env.GH_REPO}` - const release = - await $`gh release view v${Script.version} --json tagName,databaseId --repo ${process.env.GH_REPO}`.json() + await $`gh release create ${tag} -d --title ${tag} --repo ${repository}` + const release = await $`gh release view ${tag} --json tagName,databaseId --repo ${repository}`.json() output.push(`release=${release.databaseId}`) output.push(`tag=${release.tagName}`) } -output.push(`repo=${process.env.GH_REPO}`) +output.push(`repo=${repository}`) if (process.env.GITHUB_OUTPUT) { await Bun.write(process.env.GITHUB_OUTPUT, output.join("\n"))